diff options
author | Matt A. Tobin <email@mattatobin.com> | 2022-03-26 20:18:05 -0500 |
---|---|---|
committer | Matt A. Tobin <email@mattatobin.com> | 2022-03-26 20:18:05 -0500 |
commit | c3dc8a1f81c2148a64bc99a194da4c10614e9b95 (patch) | |
tree | 6915845b08018db4ee37f09a7a8ea9b4c17ebb27 /calendar/lightning | |
parent | c0d30f29a0a1d418442c9dc05c83ac6ef2921d15 (diff) | |
download | aura-central-c3dc8a1f81c2148a64bc99a194da4c10614e9b95.tar.gz |
Manually re-add calendar
Diffstat (limited to 'calendar/lightning')
73 files changed, 13791 insertions, 0 deletions
diff --git a/calendar/lightning/Makefile.in b/calendar/lightning/Makefile.in new file mode 100644 index 000000000..fb6d888ff --- /dev/null +++ b/calendar/lightning/Makefile.in @@ -0,0 +1,88 @@ +# 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/. + +# Calendar builders currently use STRIP_XPI to reduce the binary component in +# Lightning. + +XPI_PKGNAME = lightning-$(LIGHTNING_VERSION).$(AB_CD).$(MOZ_PKG_PLATFORM) +XPI_VERSION = $(LIGHTNING_VERSION) + +XPI_EM_ID = {e2fda1a4-762b-4020-b5ad-a41df1933103} + +ifneq (,$(findstring a,$(LIGHTNING_VERSION))) +DEFINES += -DLIGHTNING_PRERELEASE_VERSION=1 +endif + +# Enable nightly updates on aurora and nightly channel +ifeq (nightly,$(subst aurora,nightly,$(MOZ_UPDATE_CHANNEL))) +DEFINES += -DLIGHTNING_UPDATE_LOCATION=https://calendar.mozilla.org/update.php +endif + +# Gecko milestone +GRE_MILESTONE = $(shell $(PYTHON) $(MOZILLA_SRCDIR)/config/printconfigsetting.py $(DIST)/bin/platform.ini Build Milestone) +ifdef GRE_MILESTONE +DEFINES += -DGRE_MILESTONE=$(GRE_MILESTONE) +endif + +# comm-central source repo and stamp +SOURCE_STAMP ?= $(firstword $(shell hg -R $(topsrcdir) parent --template='{node}\n' 2>/dev/null)) +ifdef SOURCE_STAMP +DEFINES += -DSOURCE_STAMP='$(SOURCE_STAMP)' +endif + +SOURCE_REPO := $(shell hg -R $(topsrcdir) showconfig paths.default 2>/dev/null | sed -e 's/^ssh:/http:/') +ifdef SOURCE_REPO +DEFINES += -DSOURCE_REPO='$(SOURCE_REPO)' +endif + +# Mozilla source repo and stamps +MOZ_SOURCE_STAMP = $(firstword $(shell hg -R $(MOZILLA_SRCDIR) parent --template='{node}\n' 2>/dev/null)) +ifdef MOZ_SOURCE_STAMP +DEFINES += -DMOZ_SOURCE_STAMP='$(MOZ_SOURCE_STAMP)' +endif + +MOZ_SOURCE_REPO := $(shell hg -R $(MOZILLA_SRCDIR) showconfig paths.default 2>/dev/null | sed -e 's/^ssh:/http:/') +ifdef MOZ_SOURCE_REPO +DEFINES += -DMOZ_SOURCE_REPO='$(MOZ_SOURCE_REPO)' +endif + +# Install as a global extension in +# dist/bin/extensions/ +XPI_INSTALL_EXTENSION = $(XPI_EM_ID) + +DEFINES += -DTHUNDERBIRD_VERSION=$(THUNDERBIRD_VERSION) \ + -DTHUNDERBIRD_MAXVERSION=$(THUNDERBIRD_MAXVERSION) \ + -DSEAMONKEY_VERSION=$(SEAMONKEY_VERSION) \ + -DSEAMONKEY_MAXVERSION=$(SEAMONKEY_MAXVERSION) \ + -DLIGHTNING_VERSION=$(LIGHTNING_VERSION) \ + -DTARGET_PLATFORM=$(OS_TARGET)_$(TARGET_XPCOM_ABI) \ + -DXPI_EM_ID=$(XPI_EM_ID) \ + $(NULL) + +MOZ_BUILDID = $(shell $(PYTHON) $(MOZILLA_SRCDIR)/config/printconfigsetting.py $(DIST)/bin/application.ini App BuildID) +DEFINES += -DMOZ_BUILDID=$(MOZ_BUILDID) + +include $(topsrcdir)/config/rules.mk +include $(srcdir)/versions.mk +include $(srcdir)/lightning-packager.mk +include $(srcdir)/lightning-tests.mk + +# For Lightning, we also need to preprocess the l10n prefs. Pull in the en-US +# copy if the files doesn't exist. +repack-process-extrafiles: lightning-extrafiles +lightning-extrafiles: LTN_ABCD_L10NJS=$(call EXPAND_LOCALE_SRCDIR,calendar/locales)/lightning-l10n.js +lightning-extrafiles: LTN_ANY_L10NJS=$(if $(wildcard $(LTN_ABCD_L10NJS)),$(LTN_ABCD_L10NJS),$(topsrcdir)/calendar/locales/en-US/lightning-l10n.js) +lightning-extrafiles: + $(call py_action,preprocessor,$(PREF_PPFLAGS) $(DEFINES) $(ACDEFINES) $(XULPPFLAGS) $(LTN_ANY_L10NJS) -o $(DIST)/$(UNIVERSAL_PATH)xpi-stage/$(L10N_XPI_NAME)/$(PREF_DIR)/lightning-l10n.js) + +ident: + @printf 'comm_revision ' + @$(PYTHON) $(MOZILLA_SRCDIR)/config/printconfigsetting.py \ + $(FINAL_TARGET)/app.ini App SourceStamp + @printf 'moz_revision ' + @$(PYTHON) $(MOZILLA_SRCDIR)/config/printconfigsetting.py \ + $(FINAL_TARGET)/app.ini Build SourceStamp + @printf 'buildid ' + @$(PYTHON) $(MOZILLA_SRCDIR)/config/printconfigsetting.py \ + $(FINAL_TARGET)/app.ini App BuildID diff --git a/calendar/lightning/app.ini b/calendar/lightning/app.ini new file mode 100644 index 000000000..47c4f6d1c --- /dev/null +++ b/calendar/lightning/app.ini @@ -0,0 +1,35 @@ +; 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/. + +; This is a mock application.ini for Lightning. It allows easy access to +; extension properties by the build system. + +#filter substitution +[App] +Vendor=Mozilla +Name=Lightning +Version=@LIGHTNING_VERSION@ +BuildID=@MOZ_BUILDID@ + +#ifdef SOURCE_REPO +SourceRepository=@SOURCE_REPO@ +#endif +#ifdef SOURCE_STAMP +SourceStamp=@SOURCE_STAMP@ +#endif + +Copyright=Copyright (c) 1998 - 2010 mozilla.org +ID=@XPI_EM_ID@ + +[Build] +#ifdef MOZ_SOURCE_REPO +SourceRepository=@MOZ_SOURCE_REPO@ +#endif +#ifdef MOZ_SOURCE_STAMP +SourceStamp=@MOZ_SOURCE_STAMP@ +#endif + +[Gecko] +MinVersion=@GRE_MILESTONE@ +MaxVersion=@GRE_MILESTONE@ diff --git a/calendar/lightning/build/get-platform.py b/calendar/lightning/build/get-platform.py new file mode 100644 index 000000000..3c0e4fa4f --- /dev/null +++ b/calendar/lightning/build/get-platform.py @@ -0,0 +1,13 @@ +#!/usr/bin/python +# 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/. + +# Get the target platform from a set an install.rdf file + +import sys +from xml.dom.minidom import parse + +doc = parse(sys.argv[1] + "/install.rdf") +elem = doc.getElementsByTagName("em:targetPlatform")[0] +print elem.firstChild.nodeValue diff --git a/calendar/lightning/build/makeversion.py b/calendar/lightning/build/makeversion.py new file mode 100644 index 000000000..f6e30754c --- /dev/null +++ b/calendar/lightning/build/makeversion.py @@ -0,0 +1,20 @@ +# 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/. + +import sys +import re + +# Change this number to add an amount to the minor, i.e with MINOR_ADD=2, +# 24.2.0 becomes 2.6.4 instead of 2.6.2 +MINOR_ADD=0 + +def makeversion(x): + parts = x.split('.') + major = str((int(parts[0]) + 2)) + parts[0] = major[:-1] + "." + major[-1] + if len(parts) > 1 and parts[1].isdigit(): + parts[1] = str(int(parts[1]) + MINOR_ADD) + return re.sub(r'.0([ab][0-9]*|)$', r'\1', '.'.join(parts)) + +print(makeversion(sys.argv[1])) diff --git a/calendar/lightning/build/universal.mk b/calendar/lightning/build/universal.mk new file mode 100644 index 000000000..baf98f317 --- /dev/null +++ b/calendar/lightning/build/universal.mk @@ -0,0 +1,45 @@ +# 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/. + +ifndef OBJDIR +OBJDIR_ARCH_1 = $(MOZ_OBJDIR)/$(firstword $(MOZ_BUILD_PROJECTS)) +OBJDIR_ARCH_2 = $(MOZ_OBJDIR)/$(word 2,$(MOZ_BUILD_PROJECTS)) +DIST_ARCH_1 = $(OBJDIR_ARCH_1)/dist +DIST_ARCH_2 = $(OBJDIR_ARCH_2)/dist +DIST_UNI = $(DIST_ARCH_1)/universal +OBJDIR = $(OBJDIR_ARCH_1) +endif + +topsrcdir = $(TOPSRCDIR) +DEPTH = $(OBJDIR) + +include $(DEPTH)/config/autoconf.mk +include $(topsrcdir)/platform/system/installer/package-name.mk + +THUNDERBIRD_VERSION := $(shell cat $(topsrcdir)/mail/config/version.txt) +LIGHTNING_VERSION := $(shell $(PYTHON) $(topsrcdir)/calendar/lightning/build/makeversion.py $(word 1,$(MOZ_PKG_VERSION) $(THUNDERBIRD_VERSION))) +XPI_PKGNAME = lightning-$(LIGHTNING_VERSION).$(AB_CD).$(MOZ_PKG_PLATFORM) + +STANDALONE_MAKEFILE := 1 +include $(TOPSRCDIR)/config/config.mk + +define unify_lightning +mkdir -p $(DIST_UNI)/$1 +rm -rf $(DIST_UNI)/$1/$2* +cp -R $(DIST_ARCH_1)/$1/$2 $(DIST_UNI)/$1 +grep -v em:targetPlatform $(DIST_ARCH_1)/$1/$2/install.rdf > $(DIST_UNI)/$1/$2/install.rdf +endef + +define unify_lightning_repackage +$(call py_action,zip,-C $(DIST_UNI)/$1/$2 ../$(XPI_PKGNAME).xpi '*') +endef + +postflight_all: + $(call unify_lightning,xpi-stage,lightning) + $(call unify_lightning_repackage,xpi-stage,lightning) +ifdef NIGHTLY_BUILD + $(call unify_lightning,$(MOZ_APP_DISPLAYNAME).app/Contents/Resources/extensions,{e2fda1a4-762b-4020-b5ad-a41df1933103}) +else + $(call unify_lightning,$(MOZ_APP_DISPLAYNAME).app/Contents/Resources/distribution/extensions,{e2fda1a4-762b-4020-b5ad-a41df1933103}) +endif diff --git a/calendar/lightning/chrome.manifest b/calendar/lightning/chrome.manifest new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/calendar/lightning/chrome.manifest diff --git a/calendar/lightning/components/calItipProtocolHandler.js b/calendar/lightning/components/calItipProtocolHandler.js new file mode 100644 index 000000000..7615ae9c7 --- /dev/null +++ b/calendar/lightning/components/calItipProtocolHandler.js @@ -0,0 +1,137 @@ +/* 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/. */ + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource://calendar/modules/calUtils.jsm"); + +var Ci = Components.interfaces; + +var ITIP_HANDLER_MIMETYPE = "application/x-itip-internal"; +var ITIP_HANDLER_PROTOCOL = "moz-cal-handle-itip"; +var NS_ERROR_WONT_HANDLE_CONTENT = 0x805d0001; + + +function NYI() { + throw Components.results.NS_ERROR_NOT_IMPLEMENTED; +} + +function ItipChannel(URI) { + this.URI = this.originalURI = URI; +} +var ItipChannelClassID = Components.ID("{643e0328-36f6-411d-a107-16238dff9cd7}"); +var ItipChannelInterfaces = [ + Components.interfaces.nsIChannel, + Components.interfaces.nsIRequest +]; +ItipChannel.prototype = { + classID: ItipChannelClassID, + QueryInterface: XPCOMUtils.generateQI(ItipChannelInterfaces), + classInfo: XPCOMUtils.generateCI({ + classID: ItipChannelClassID, + contractID: "@mozilla.org/calendar/itip-channel;1", + classDescription: "Calendar Itip Channel", + interfaces: ItipChannelInterfaces, + }), + + contentType: ITIP_HANDLER_MIMETYPE, + loadAttributes: null, + contentLength: 0, + owner: null, + loadGroup: null, + notificationCallbacks: null, + securityInfo: null, + + open: NYI, + asyncOpen: function(observer, ctxt) { + observer.onStartRequest(this, ctxt); + }, + asyncRead: function(listener, ctxt) { + return listener.onStartRequest(this, ctxt); + }, + + isPending: function() { return true; }, + status: Components.results.NS_OK, + cancel: function(status) { this.status = status; }, + suspend: NYI, + resume: NYI, +}; + +function ItipProtocolHandler() { + this.wrappedJSObject = this; +} +var ItipProtocolHandlerClassID = Components.ID("{6e957006-b4ce-11d9-b053-001124736B74}"); +var ItipProtocolHandlerInterfaces = [Components.interfaces.nsIProtocolHandler]; +ItipProtocolHandler.prototype = { + classID: ItipProtocolHandlerClassID, + QueryInterface: XPCOMUtils.generateQI(ItipProtocolHandlerInterfaces), + classInfo: XPCOMUtils.generateCI({ + classID: ItipProtocolHandlerClassID, + contractID: "@mozilla.org/network/protocol;1?name=" + ITIP_HANDLER_PROTOCOL, + classDescription: "iTIP Protocol Handler", + interfaces: ItipProtocolHandlerInterfaces + }), + + protocolFlags: Ci.nsIProtocolHandler.URI_NORELATIVE | Ci.nsIProtocolHandler.URI_DANGEROUS_TO_LOAD, + allowPort: () => false, + isSecure: false, + newURI: function(spec, charSet, baseURI) { + let cls = Components.classes["@mozilla.org/network/standard-url;1"]; + let url = cls.createInstance(Ci.nsIStandardURL); + url.init(Ci.nsIStandardURL.URLTYPE_STANDARD, 0, spec, charSet, baseURI); + dump("Creating new URI for " + spec + "\n"); + return url.QueryInterface(Ci.nsIURI); + }, + + newChannel: function(URI) { + return this.newChannel2(URI, null); + }, + + newChannel2: function(URI, aLoadInfo) { + dump("Creating new ItipChannel for " + URI + "\n"); + return new ItipChannel(URI); + }, +}; + +function ItipContentHandler() { + this.wrappedJSObject = this; +} +var ItipContentHandlerClassID = Components.ID("{47c31f2b-b4de-11d9-bfe6-001124736B74}"); +var ItipContentHandlerInterfaces = [Components.interfaces.nsIContentHandler]; +ItipContentHandler.prototype = { + classID: ItipContentHandlerClassID, + QueryInterface: XPCOMUtils.generateQI(ItipContentHandlerInterfaces), + classInfo: XPCOMUtils.generateCI({ + classID: ItipContentHandlerClassID, + contractID: "@mozilla.org/uriloader/content-handler;1?type=" + ITIP_HANDLER_MIMETYPE, + classDescription: "Lightning text/calendar content handler", + interfaces: ItipContentHandlerInterfaces + }), + + handleContent: function(contentType, windowTarget, request) { + let channel = request.QueryInterface(Ci.nsIChannel); + let uri = channel.URI.spec; + if (!uri.startsWith(ITIP_HANDLER_PROTOCOL + ":")) { + cal.ERROR("Unexpected iTIP uri: " + uri + "\n"); + throw NS_ERROR_WONT_HANDLE_CONTENT; + } + // moz-cal-handle-itip:///? + let paramString = uri.substring(ITIP_HANDLER_PROTOCOL.length + 4); + let paramArray = paramString.split("&"); + let paramBlock = { }; + paramArray.forEach((value) => { + let parts = value.split("="); + paramBlock[parts[0]] = unescape(unescape(parts[1])); + }); + // dump("content-handler: have params " + paramBlock.toSource() + "\n"); + let event = cal.createEvent(paramBlock.data); + dump("Processing iTIP event '" + event.title + "' from " + + event.organizer.id + " (" + event.id + ")\n"); + let calMgr = cal.getCalendarManager(); + let cals = calMgr.getCalendars({}); + cals[0].addItem(event, null); + } +}; + +var components = [ItipChannel, ItipProtocolHandler, ItipContentHandler]; +this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components); diff --git a/calendar/lightning/components/calItipProtocolHandler.manifest b/calendar/lightning/components/calItipProtocolHandler.manifest new file mode 100644 index 000000000..561872da7 --- /dev/null +++ b/calendar/lightning/components/calItipProtocolHandler.manifest @@ -0,0 +1,8 @@ +component {643e0328-36f6-411d-a107-16238dff9cd7} calItipProtocolHandler.js +contract @mozilla.org/calendar/itip-channel;1 {643e0328-36f6-411d-a107-16238dff9cd7} + +component {6e957006-b4ce-11d9-b053-001124736B74} calItipProtocolHandler.js +contract @mozilla.org/network/protocol;1?name=moz-cal-handle-itip {6e957006-b4ce-11d9-b053-001124736B74} + +component {47c31f2b-b4de-11d9-bfe6-001124736B74} calItipProtocolHandler.js +contract @mozilla.org/uriloader/content-handler;1?type=application/x-itip-internal {47c31f2b-b4de-11d9-bfe6-001124736B74} diff --git a/calendar/lightning/components/lightningTextCalendarConverter.js b/calendar/lightning/components/lightningTextCalendarConverter.js new file mode 100644 index 000000000..cf33ea6da --- /dev/null +++ b/calendar/lightning/components/lightningTextCalendarConverter.js @@ -0,0 +1,92 @@ +/* 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/. */ + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/Services.jsm"); +Components.utils.import("resource://calendar/modules/calUtils.jsm"); +Components.utils.import("resource://calendar/modules/calXMLUtils.jsm"); +Components.utils.import("resource://calendar/modules/calRecurrenceUtils.jsm"); +Components.utils.import("resource://calendar/modules/ltnInvitationUtils.jsm"); + +function ltnMimeConverter() { + this.wrappedJSObject = this; +} + +var ltnMimeConverterClassID = Components.ID("{c70acb08-464e-4e55-899d-b2c84c5409fa}"); +var ltnMimeConverterInterfaces = [Components.interfaces.nsISimpleMimeConverter]; +ltnMimeConverter.prototype = { + classID: ltnMimeConverterClassID, + QueryInterface: XPCOMUtils.generateQI(ltnMimeConverterInterfaces), + + classInfo: XPCOMUtils.generateCI({ + classID: ltnMimeConverterClassID, + contractID: "@mozilla.org/lightning/mime-converter;1", + classDescription: "Lightning text/calendar handler", + interfaces: ltnMimeConverterInterfaces + }), + + uri: null, + + convertToHTML: function(contentType, data) { + let parser = Components.classes["@mozilla.org/calendar/ics-parser;1"] + .createInstance(Components.interfaces.calIIcsParser); + parser.parseString(data); + let event = null; + for (let item of parser.getItems({})) { + if (cal.isEvent(item)) { + if (item.hasProperty("X-MOZ-FAKED-MASTER")) { + // if it's a faked master, take any overridden item to get a real occurrence: + let exc = item.recurrenceInfo.getExceptionFor(item.startDate); + cal.ASSERT(exc, "unexpected!"); + if (exc) { + item = exc; + } + } + event = item; + break; + } + } + if (!event) { + return ""; + } + + let itipItem = null; + let msgOverlay = ""; + let msgWindow = null; + + itipItem = Components.classes["@mozilla.org/calendar/itip-item;1"] + .createInstance(Components.interfaces.calIItipItem); + itipItem.init(data); + + // this.uri is the message URL that we are processing. + // We use it to get the nsMsgHeaderSink to store the calItipItem. + if (this.uri) { + try { + let msgUrl = this.uri.QueryInterface(Components.interfaces.nsIMsgMailNewsUrl); + msgWindow = msgUrl.msgWindow; + itipItem.sender = msgUrl.mimeHeaders.extractHeader("From", false); + } catch (exc) { + // msgWindow is optional in some scenarios + // (e.g. gloda in action, throws NS_ERROR_INVALID_POINTER then) + } + } + + // msgOverlay needs to be defined irrespectively of the existance of msgWindow to not break + // printing of invitation emails + let dom = ltn.invitation.createInvitationOverlay(event, itipItem); + msgOverlay = cal.xml.serializeDOM(dom); + + if (msgWindow) { + let sinkProps = msgWindow.msgHeaderSink.properties; + sinkProps.setPropertyAsInterface("itipItem", itipItem); + sinkProps.setPropertyAsAUTF8String("msgOverlay", msgOverlay); + + // Notify the observer that the itipItem is available + Services.obs.notifyObservers(null, "onItipItemCreation", 0); + } + return msgOverlay; + } +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ltnMimeConverter]); diff --git a/calendar/lightning/components/lightningTextCalendarConverter.manifest b/calendar/lightning/components/lightningTextCalendarConverter.manifest new file mode 100644 index 000000000..76057cfd7 --- /dev/null +++ b/calendar/lightning/components/lightningTextCalendarConverter.manifest @@ -0,0 +1,3 @@ +component {c70acb08-464e-4e55-899d-b2c84c5409fa} lightningTextCalendarConverter.js +contract @mozilla.org/lightning/mime-converter;1 {c70acb08-464e-4e55-899d-b2c84c5409fa} +category simple-mime-converters text/calendar @mozilla.org/lightning/mime-converter;1 diff --git a/calendar/lightning/components/moz.build b/calendar/lightning/components/moz.build new file mode 100644 index 000000000..4d53c7aca --- /dev/null +++ b/calendar/lightning/components/moz.build @@ -0,0 +1,12 @@ +# vim: set filetype=python: +# 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/. + +EXTRA_COMPONENTS += [ + 'calItipProtocolHandler.js', + 'calItipProtocolHandler.manifest', + 'lightningTextCalendarConverter.js', + 'lightningTextCalendarConverter.manifest', +] + diff --git a/calendar/lightning/content/html-item-editing/lightning-item-iframe.html b/calendar/lightning/content/html-item-editing/lightning-item-iframe.html new file mode 100644 index 000000000..4cc17774c --- /dev/null +++ b/calendar/lightning/content/html-item-editing/lightning-item-iframe.html @@ -0,0 +1,28 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset='utf-8'> + <title>Event in a Tab</title> + <!-- + "chrome://global/skin/global.css" cannot be included here due to + namespaces, see https://bugzilla.mozilla.org/show_bug.cgi?id=1297392#c4 + --> + <link rel="stylesheet" type="text/css" href="chrome://lightning-common/skin/html-item-editing.css"> + </head> + <body> + <!-- This div's id must be "container" for React.js to use it --> + <div id='container'> + <p> + If you can see this, React is still loading or is not working right. + </p> + </div> + <script type="application/javascript" src="resource://devtools/client/shared/vendor/react.js"></script> + <script type="application/javascript" src="resource://devtools/client/shared/vendor/react-dom.js"></script> + <script src='chrome://calendar/content/calendar-dialog-utils.js'></script> + <script src='chrome://calendar/content/calendar-ui-utils.js'></script> + <script src='chrome://calendar/content/calUtils.js'></script> + <script src='chrome://global/content/globalOverlay.js'></script> + <script src='chrome://lightning/content/lightning-item-iframe.js'></script> + <script src='chrome://lightning/content/html-item-editing/react-code.js'></script> + </body> +</html> diff --git a/calendar/lightning/content/html-item-editing/react-code.js b/calendar/lightning/content/html-item-editing/react-code.js new file mode 100644 index 000000000..0b0d07b5c --- /dev/null +++ b/calendar/lightning/content/html-item-editing/react-code.js @@ -0,0 +1,572 @@ +/* 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/. */ + +// This file contains code that uses react.js + +/* exported gTopComponent, DatePicker, TopComponent */ + +var gTopComponent = null; + +var Tabstrip = React.createClass({ + handleChange: function(index) { + // The click handler will update the state with + // the index of the focused menu entry + this.props.onInput(this.props.keyprop, index); + }, + render: function() { + return React.DOM.ul( + { id: "tabstrip" }, + this.props.tabs.map((tab, index) => { + let style = (this.props.activeTab == index) ? "activeTab" : ""; + // The bind() method makes the index + // available to the handleChange function: + return React.DOM.li({ + className: style + " tab", + key: "tabkey" + index, + onClick: this.handleChange.bind(this, index) + }, tab); + }) + ); + } +}); + +var TextField = React.createClass({ + handleChange: function(event) { + this.props.onInput(this.props.keyprop, event.target.value); + }, + render: function() { + return React.DOM.input({ + type: "text", + // placeholder: "New Event" + value: this.props.value, + onChange: this.handleChange + }); + } +}); + +var TextArea = React.createClass({ + handleChange: function(event) { + this.props.onInput(this.props.keyprop, event.target.value); + }, + render: function() { + return React.DOM.textarea({ + type: "text", + // placeholder: "New Event" + value: this.props.value, + onChange: this.handleChange, + placeholder: "Description", + rows: 10, + id: "descriptionTextArea" + }); + } +}); + +var Checkbox = React.createClass({ + handleChange: function(event) { + this.props.onInput(this.props.keyprop, event.target.checked); + }, + render: function() { + return React.DOM.input({ + type: "checkbox", + checked: this.props.checked, + onChange: this.handleChange + }); + } +}); + +var Dropdown = React.createClass({ + handleChange: function(event) { + this.props.onInput(this.props.keyprop, event.target.value); + }, + render: function() { + return React.DOM.select({ + value: this.props.value, + onChange: this.handleChange + }, this.props.options.map((option, index) => { + return React.DOM.option({ + // could use option[0] here instead of index... + key: this.props.keyprop + "Option" + index, + onChange: this.handleChange, + value: option[0], + disabled: this.props.disabled || false + }, option[1]); + })); + } +}); + +var Link = React.createClass({ + handleClick: function() { + this.props.onInput(this.props.value); + }, + render: function() { + return React.DOM.a({ + // href: "", + onClick: this.handleClick + }, + this.props.value + ); + } +}); + +var DatePicker = React.createClass({ + render: function() { + return React.DOM.input({ type: "date" }); + } +}); + +var Capsule = React.createClass({ + handleDelete: function() { + this.props.onDelete(this.props.keyprop, this.props.value); + }, + render: function() { + return React.DOM.span( + { + className: "capsule", + style: { background: "ButtonHighlight" }, + }, + this.props.value, + React.DOM.span({ + className: "deleteCapsule", + onClick: this.handleDelete + }, + "x" + ) + ); + } +}); + +var TopComponent = React.createClass({ + getDefaultProps: function() { + return { + // these "initial" props are passed in as props but + // immediately become state (state can change, props do not) + initialTitle: "New Event", + initialLocation: "", + initialAllDay: false, + initialRepeat: "none", + initialRepeatUntilDate: "forever", + initialReminders: 0, + initialDescription: "", + initialShowTimeAs: "OPAQUE", + initialCalendarId: 0, + initialPrivacy: 0, + initialStatus: 0, + initialPriority: 0, + initialUrl: "", + initialShowUrl: false, + initialCategories: [], + initialCategoriesList: [], + initialAttachments: {}, + + tabs: ["Description", "More", "Reminders", "Attachments", "Attendees"], + calendarList: [ + [0, "Home"], + [1, "Work"] + ], + privacyList: [ + ["NONE", "Not Specified"], + ["PUBLIC", "Public Event"], + ["CONFIDENTIAL", "Show Time and Date Only"], + ["PRIVATE", "Private Event"] + ], + statusList: [ + ["NONE", "Not Specified"], + ["TENTATIVE", "Tentative"], + ["CONFIRMED", "Confirmed"], + ["CANCELLED", "Canceled"] + ], + priorityList: [ + // XXX what about other numbers? + [0, "Not Specified"], + [9, "Low"], + [5, "Normal"], + [1, "High"] + ], + showTimeAsList: [ + ["OPAQUE", true], + ["TRANSPARENT", false] + ], + repeatList: [ + ["none", "Does Not Repeat"], + ["daily", "Daily"], + ["weekly", "Weekly"], + ["every.weekday", "Every Weekday"], + ["bi.weekly", "Bi-weekly"], + ["monthly", "Monthly"], + ["yearly", "Yearly"], + ["custom", "Custom..."] + ], + remindersList: [ + [0, "No Reminder"], + [1, "0 Minutes Before"], + [2, "5 Minutes Before"], + [3, "15 Minutes Before"], + [4, "30 Minutes Before"], + [5, "1 Hour Before"], + [6, "2 Hours Before"], + [7, "12 Hours Before"], + [8, "1 Day Before"], + [9, "2 Days Before"], + [10, "1 Week Before"], + [11, "Custom..."] + ], + supportsPriority: false + }; + }, + getInitialState: function() { + // all the passed-in props that begin with 'initial' become state + return { + title: this.props.initialTitle, + location: this.props.initialLocation, + startTimezone: this.props.initialStartTimezone, + endTimezone: this.props.initialEndTimezone, + startDate: this.props.initialStartDate, + startTime: this.props.initialStartTime, + endDate: this.props.initialEndDate, + endTime: this.props.initialEndTime, + allDay: this.props.initialAllDay, + repeat: this.props.initialRepeat, + repeatUntilDate: this.props.initialRepeatUntilDate, + reminders: this.props.initialReminders, + description: this.props.initialDescription, + showTimeAs: this.props.initialShowTimeAs, + calendarId: this.props.initialCalendarId, + privacy: this.props.initialPrivacy, + status: this.props.initialStatus, + priority: this.props.initialPriority, + url: this.props.initialUrl, + showUrl: this.props.initialShowUrl, + categories: this.props.initialCategories, + categoriesList: this.props.initialCategoriesList, + attachments: this.props.initialAttachments, + + isWideview: (window.innerWidth > 750), + activeTab: 0 + }; + }, + updateWideview: function() { + let wideview = (window.innerWidth > 750); + if (wideview != this.state.isWideview) { + this.setState({ isWideview: wideview }); + } + }, + componentWillMount: function() { + this.updateWideview(); + }, + componentDidMount: function() { + window.addEventListener("resize", this.updateWideview); + }, + componentWillUnmount: function() { + window.removeEventListener("resize", this.updateWideview); + }, + exportState: function() { + // Use this to access this component's state from above/outside + // the react component hierarchy, for example, when saving changes. + return this.state; + }, + importState: function(aStateObj) { + // Use this to impose state changes from above/outside of the + // react component hierarchy. + this.setState(aStateObj); + }, + handleSimpleChange: function(aKey, aValue) { + let obj = {}; + obj[aKey] = aValue; + this.setState(obj); + }, + handleShowTimeAsChange: function(aKey, aValue) { + // convert from true/false to OPAQUE/TRANSPARENT + let list = this.props.showTimeAsList; + let index = list.findIndex(i => (i[1] == aValue)); + let newValue = list[index][0]; + this.handleSimpleChange(aKey, newValue); + }, + linkClicked: function(aValue) { + + }, + onDeleteCapsule: function(aKey, aValue) { + let a = this.state[aKey]; + let index = a.indexOf(aValue); + a.splice(index, 1); + this.setState({ aKey: a }); + }, + render: function() { + // 'key' doesn't seem to work as a prop name (presumably because + // already used by react?), so using 'keyprop' instead for now. + let titleDiv = React.DOM.div( + { id: "titleDiv", className: "box" }, + "Title ", + React.createElement(TextField, { + keyprop: "title", + value: this.state.title, + onInput: this.handleSimpleChange + }) + ); + let locationDiv = React.DOM.div( + { id: "locationDiv", className: "box" }, + "Location ", + React.createElement(TextField, { + keyprop: "location", + value: this.state.location, + onInput: this.handleSimpleChange + }) + ); + let startDiv = React.DOM.div( + { id: "startDiv", className: "box" }, + "Start ", + // React.createElement(DatePicker, { }), + React.createElement(TextField, { + keyprop: "startDate", + value: this.state.startDate, + onInput: this.handleSimpleChange + }), + React.createElement(TextField, { + keyprop: "startTime", + value: this.state.startTime, + onInput: this.handleSimpleChange + }) + ); + let endDiv = React.DOM.div( + { id: "endDiv", className: "box" }, + "End ", + React.createElement(TextField, { + keyprop: "endDate", + value: this.state.endDate, + onInput: this.handleSimpleChange + }), + React.createElement(TextField, { + keyprop: "endTime", + value: this.state.endTime, + onInput: this.handleSimpleChange + }) + // React.createElement(DatePicker, { }), + ); + let allDayDiv = React.DOM.div( + { id: "allDayDiv", className: "box" }, + React.createElement(Checkbox, { + keyprop: "allDay", + checked: this.state.allDay, + onInput: this.handleSimpleChange + }), + "All day event" + ); + let repeatDiv = React.DOM.div( + { id: "repeatDiv", className: "box" }, + "Repeat ", + React.createElement(Dropdown, { + keyprop: "repeat", + value: this.state.repeat, + options: this.props.repeatList, + onInput: this.handleSimpleChange + }), + (this.state.repeat == "none" ? null : " Until "), + (this.state.repeat == "none" + ? null + : React.createElement(TextField, { + keyprop: "repeatUntilDate", + value: this.state.repeatUntilDate, + onInput: this.handleSimpleChange + })) + ); + let calendarDiv = React.DOM.div( + { id: "calendarDiv", className: "box" }, + "Calendar ", + React.createElement(Dropdown, { + keyprop: "calendarId", + value: this.state.calendarId, + options: this.props.calendarList, + onInput: this.handleSimpleChange + }) + ); + let categoriesCapsules; + if (this.state.categories) { + categoriesCapsules = + this.state.categories.map((cat, index) => { + return React.createElement(Capsule, { + // color: this.props.categoryList.color, + value: cat, + key: cat + "key", + keyprop: "categories", + onDelete: this.onDeleteCapsule + }); + }); + } else { + categoriesCapsules = null; + } + let categoriesDiv = React.DOM.div( + { id: "categoriesDiv", className: "box" }, + "Categories ", + categoriesCapsules, + React.createElement(Link, { + value: "Add Categories", + onInput: this.linkClicked + }) + ); + let attendeesDiv = React.DOM.div( + { id: "attendeesDiv", className: "box" }, + "Attendees ", + React.createElement(Link, { + value: "Add Attendees", + onInput: this.linkClicked + }) + ); + let remindersDiv = React.DOM.div( + { id: "remindersDiv", className: "box" }, + "Reminders ", + React.createElement(Dropdown, { + keyprop: "reminders", + value: this.state.reminders, + options: this.props.remindersList, + onInput: this.handleSimpleChange + }) + ); + let attachmentsDiv = React.DOM.div( + { id: "attachmentsDiv", className: "box" }, + "Attachments ", + React.createElement(Link, { + value: "Add Attachments", + onInput: this.linkClicked + }) + ); + let urlDiv = (this.state.showUrl ? + React.DOM.div( + { id: "urlDiv", className: "box" }, + "Event link ", + React.createElement(Link, { + value: this.state.url, + onInput: this.linkClicked + })) + : null + ); + + let privacyDiv = React.DOM.div( + { id: "privacyDiv", className: "box" }, + "Privacy ", + React.createElement(Dropdown, { + keyprop: "privacy", + value: this.state.privacy, + options: this.props.privacyList, + onInput: this.handleSimpleChange + }) + ); + let statusDiv = React.DOM.div( + { id: "statusDiv", className: "box" }, + "Status ", + React.createElement(Dropdown, { + keyprop: "status", + value: this.state.status, + options: this.props.statusList, + onInput: this.handleSimpleChange + }) + ); + let priorityDiv = React.DOM.div( + { id: "priorityDiv", className: "box" }, + "Priority ", + React.createElement(Dropdown, { + keyprop: "priority", + value: this.state.priority, + options: this.props.priorityList, + onInput: this.handleSimpleChange, + disabled: !this.props.supportsPriority + }) + ); + + let tIndex = this.props.showTimeAsList.findIndex(i => (i[0] == this.state.showTimeAs)); + let showTimeAsDiv = React.DOM.div( + { id: "showTimeAsDiv", className: "box" }, + React.createElement(Checkbox, { + keyprop: "showTimeAs", + checked: (tIndex == -1 ? false : this.props.showTimeAsList[tIndex][1]), + onInput: this.handleShowTimeAsChange, + options: this.props.showTimeAsList + }), + "Show Time As Busy" + ); + let descriptionDiv = React.DOM.div( + { id: "description", value: "Description" }, + React.createElement(TextArea, { + keyprop: "description", + value: this.state.description, + onInput: this.handleSimpleChange + }) + ); + + if (this.state.isWideview) { + // wideview + return React.DOM.div( + { id: "topwrapper" }, + React.DOM.div( + { className: "wrapper" }, + titleDiv, + startDiv, + endDiv, + allDayDiv, + repeatDiv, + attendeesDiv, + locationDiv, + calendarDiv, + categoriesDiv, + remindersDiv, + attachmentsDiv, + urlDiv + ), + descriptionDiv, + React.DOM.div( + { className: "wrapper", id: "wrapper2" }, + privacyDiv, + statusDiv, + priorityDiv, + showTimeAsDiv + ) + ); + } else { + // narrowview + let tabpanelChildren = [ + descriptionDiv, + React.DOM.div({ + className: "wrapper", + style: { flexDirection: "column" }, + }, statusDiv, priorityDiv, showTimeAsDiv), + remindersDiv, + attachmentsDiv, + attendeesDiv + ]; + let tabpanels = this.props.tabs.map((elem, index) => { + return React.DOM.div({ + className: "box tabpanel " + (this.state.activeTab == index ? "" : "hidden"), + key: "tabpanelkey " + index + }, tabpanelChildren[index]); + }); + return React.DOM.div({ id: "topwrapper" }, + React.DOM.div({ className: "wrapper" }, + titleDiv, + locationDiv, + startDiv, + endDiv, + allDayDiv, + repeatDiv, + React.DOM.div( + { style: { flexDirection: "row", display: "flex" } }, + calendarDiv, + privacyDiv + ), + categoriesDiv, + React.createElement( + Tabstrip, { + tabs: this.props.tabs, + keyprop: "activeTab", + activeTab: this.state.activeTab, + onInput: this.handleSimpleChange + }), + tabpanels, + urlDiv + ) + ); + } + } +}); + +window.onload = function() { + onLoad(); +}; diff --git a/calendar/lightning/content/imip-bar-overlay.xul b/calendar/lightning/content/imip-bar-overlay.xul new file mode 100644 index 000000000..171bff802 --- /dev/null +++ b/calendar/lightning/content/imip-bar-overlay.xul @@ -0,0 +1,266 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- 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/. --> + +<!DOCTYPE overlay [ + <!ENTITY % lightningDTD SYSTEM "chrome://lightning/locale/lightning.dtd"> + %lightningDTD; +]> + +<?xml-stylesheet href="chrome://lightning/content/lightning-widgets.css" type="text/css"?> + +<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://lightning/content/lightning-utils.js"/> + <script type="application/javascript" + src="chrome://calendar/content/calUtils.js"/> + <script type="application/javascript" + src="chrome://lightning/content/imip-bar.js"/> + <script type="application/javascript" + src="chrome://calendar/content/calendar-management.js"/> + <script type="application/javascript" + src="chrome://calendar/content/calendar-ui-utils.js"/> + + <vbox id="messagepanebox"> + <vbox id="singlemessage" insertbefore="msgHeaderView"> + <lightning-notification-bar id="imip-bar" + collapsed="true" + insertbefore="msgHeaderView" + label="&lightning.imipbar.description;"> + + <!-- Some Toolbox implementation notes: + - + - css style: + - classes within toolbox are making use of existing TB css definitions - as used in + - /comm-central/source/mail/base/content/msgHdrViewOverlay.xul, only icon defining + - classes like imipAcceptButton are noted separately and OS specific within + - skin/lightning.css (resp. the OS-specific theme folders) + - + - The toolbarbuttons will be adjusted dynamically in imip-bar.js based on their + - content of menuitems. To avoid breaking this, the following should be considered + - if adding/changing toolbarbutton definitions. + - general: + - * the toolbarbuttons will appear in order of definition + - within the toolbar if visible + - * must be hidden by default + - * menuitem inside must not be hidden by default + - simple button: + - * must not have a type attribute + - * may have menupopup/menuitem within (not displayed though) + - dropdown only: + - * must have type=menu + - * should have a menupopup with at least one menuitem + - smart-dropdown: + - * must have type=menu-button + - * should have a menupopup with at least one menuitem + - * bubbling up of events from menuitems to toolbarbutton must be prevented + - by adding a trailing "if (event.target.id == this.id) " to the respective + - ltnImipBar.executeAction(...) + - * toolbarbutton's oncommand should end with the first menuitem's oncommand + - to not break automated conforming + - e.g. "if (event.target.id == this.id) ltnImipBar.executeAction('ACCEPTED');" + - and "ltnImipBar.executeAction('ACCEPTED');" + //--> + <toolbox id="imip-view-toolbox" + class="inline-toolbox" + defaulticonsize="small" + minwidth="50px" + defaultlabelalign="end" + labelalign="end" + defaultmode="full" + inlinetoolbox="true"> + <toolbar id="imip-view-toolbar" class="inline-toolbar" align="start" + customizable="false" mode="full" + defaulticonsize="small" defaultmode="full"> + + <!-- show event/invitation details --> + <toolbarbutton id="imipDetailsButton" + label="&lightning.imipbar.btnDetails.label;" + tooltiptext="&lightning.imipbar.btnDetails.tooltiptext;" + class="toolbarbutton-1 msgHeaderView-button imipDetailsButton" + oncommand="ltnImipBar.executeAction('X-SHOWDETAILS')" + hidden="true"/> + + <!-- decline counter --> + <toolbarbutton id="imipDeclineCounterButton" + label="&lightning.imipbar.btnDeclineCounter.label;" + tooltiptext="&lightning.imipbar.btnDeclineCounter.tooltiptext;" + class="toolbarbutton-1 msgHeaderView-button imipDeclineCounterButton" + oncommand="ltnImipBar.executeAction('X-DECLINECOUNTER')" + hidden="true"/> + + <!-- reschedule --> + <toolbarbutton id="imipRescheduleButton" + label="&lightning.imipbar.btnReschedule.label;" + tooltiptext="&lightning.imipbar.btnReschedule.tooltiptext;" + class="toolbarbutton-1 msgHeaderView-button imipRescheduleButton" + oncommand="ltnImipBar.executeAction('X-RESCHEDULE')" + hidden="true"/> + + <!-- add published events --> + <toolbarbutton id="imipAddButton" + label="&lightning.imipbar.btnAdd.label;" + tooltiptext="&lightning.imipbar.btnAdd.tooltiptext;" + class="toolbarbutton-1 msgHeaderView-button imipAddButton" + oncommand="ltnImipBar.executeAction()" + hidden="true"/> + + <!-- update published events and invitations --> + <toolbarbutton id="imipUpdateButton" + label="&lightning.imipbar.btnUpdate.label;" + tooltiptext="&lightning.imipbar.btnUpdate.tooltiptext;" + class="toolbarbutton-1 msgHeaderView-button imipUpdateButton" + oncommand="ltnImipBar.executeAction()" + hidden="true"/> + + <!-- delete cancelled events from calendar --> + <toolbarbutton id="imipDeleteButton" + label="&lightning.imipbar.btnDelete.label;" + tooltiptext="&lightning.imipbar.btnDelete.tooltiptext;" + class="toolbarbutton-1 msgHeaderView-button imipDeleteButton" + oncommand="ltnImipBar.executeAction()" + hidden="true"/> + + <!-- re-confirm partstat --> + <toolbarbutton id="imipReconfirmButton" + label="&lightning.imipbar.btnReconfirm2.label;" + tooltiptext="&lightning.imipbar.btnReconfirm.tooltiptext;" + class="toolbarbutton-1 msgHeaderView-button imipReconfirmButton" + oncommand="ltnImipBar.executeAction()" + hidden="true"/> + + <!-- accept --> + <toolbarbutton id="imipAcceptButton" + tooltiptext="&lightning.imipbar.btnAccept2.tooltiptext;" + label="&lightning.imipbar.btnAccept.label;" + oncommand="if (event.target.id == this.id) ltnImipBar.executeAction('ACCEPTED');" + type="menu-button" + class="imip-button toolbarbutton-1 msgHeaderView-button imipAcceptButton" + hidden="true"> + <menupopup id="imipAcceptDropdown"> + <menuitem id="imipAcceptButton_Accept" + tooltiptext="&lightning.imipbar.btnAccept2.tooltiptext;" + label="&lightning.imipbar.btnAccept.label;" + oncommand="ltnImipBar.executeAction('ACCEPTED');"/> + <menuitem id="imipAcceptButton_Tentative" + tooltiptext="&lightning.imipbar.btnTentative2.tooltiptext;" + label="&lightning.imipbar.btnTentative.label;" + oncommand="ltnImipBar.executeAction('TENTATIVE');"/> + <!-- add here more menuitem as needed --> + </menupopup> + </toolbarbutton> + + <!-- accept recurrences --> + <toolbarbutton id="imipAcceptRecurrencesButton" + tooltiptext="&lightning.imipbar.btnAcceptRecurrences2.tooltiptext;" + label="&lightning.imipbar.btnAcceptRecurrences.label;" + oncommand="if (event.target.id == this.id) ltnImipBar.executeAction('ACCEPTED');" + type="menu-button" + class="imip-button toolbarbutton-1 msgHeaderView-button imipAcceptRecurrencesButton" + hidden="true"> + <menupopup id="imipAcceptRecurrencesDropdown"> + <menuitem id="imipAcceptRecurrencesButton_Accept" + tooltiptext="&lightning.imipbar.btnAcceptRecurrences2.tooltiptext;" + label="&lightning.imipbar.btnAcceptRecurrences.label;" + oncommand="ltnImipBar.executeAction('ACCEPTED');"/> + <menuitem id="imipAcceptRecurrencesButton_Tentative" + tooltiptext="&lightning.imipbar.btnTentativeRecurrences2.tooltiptext;" + label="&lightning.imipbar.btnTentativeRecurrences.label;" + oncommand="ltnImipBar.executeAction('TENTATIVE');"/> + <!-- add here more menuitem as needed --> + </menupopup> + </toolbarbutton> + + <!-- tentative; should only be used, if no imipMoreButton is used and + - imipDeclineButton/imipAcceptButton have no visible menuitems //--> + <toolbarbutton id="imipTentativeButton" + label="&lightning.imipbar.btnTentative.label;" + tooltiptext="&lightning.imipbar.btnTentative2.tooltiptext;" + class="toolbarbutton-1 msgHeaderView-button imipTentativeButton" + oncommand="if (event.target.id == this.id) ltnImipBar.executeAction('TENTATIVE');" + type="menu-button" + hidden="true"> + <menupopup id="imipTentativeDropdown"> + <menuitem id="imipTentativeButton_Tentative" + tooltiptext="&lightning.imipbar.btnTentative2.tooltiptext;" + label="&lightning.imipbar.btnTentative.label;" + oncommand="ltnImipBar.executeAction('TENTATIVE');"/> + <!-- add here more menuitem as needed --> + </menupopup> + </toolbarbutton> + + <!-- tentative recurrences; should only be used, if no imipMoreButton is used and + - imipDeclineRecurrencesButton/imipAcceptRecurrencesButton have no visible menuitems //--> + <toolbarbutton id="imipTentativeRecurrencesButton" + label="&lightning.imipbar.btnTentativeRecurrences.label;" + tooltiptext="&lightning.imipbar.btnTentativeRecurrences2.tooltiptext;" + class="toolbarbutton-1 msgHeaderView-button imipTentativeRecurrencesButton" + oncommand="if (event.target.id == this.id) ltnImipBar.executeAction('TENTATIVE');" + type="menu-button" + hidden="true"> + <menupopup id="imipTentativeRecurrencesDropdown"> + <menuitem id="imipTentativeRecurrencesButton_Tentative" + tooltiptext="&lightning.imipbar.btnTentativeRecurrences2.tooltiptext;" + label="&lightning.imipbar.btnTentativeRecurrences.label;" + oncommand="ltnImipBar.executeAction('TENTATIVE');"/> + <!-- add here more menuitem as needed --> + </menupopup> + </toolbarbutton> + + <!-- decline --> + <toolbarbutton id="imipDeclineButton" + tooltiptext="&lightning.imipbar.btnDecline2.tooltiptext;" + label="&lightning.imipbar.btnDecline.label;" + oncommand="if (event.target.id == this.id) ltnImipBar.executeAction('DECLINED');" + type="menu-button" + class="toolbarbutton-1 msgHeaderView-button imipDeclineButton" + hidden="true"> + <menupopup id="imipDeclineDropdown"> + <menuitem id="imipDeclineButton_Decline" + tooltiptext="&lightning.imipbar.btnDecline2.tooltiptext;" + label="&lightning.imipbar.btnDecline.label;" + oncommand="ltnImipBar.executeAction('DECLINED');"/> + <!-- add here more menuitem as needed --> + </menupopup> + </toolbarbutton> + + <!-- decline recurrences --> + <toolbarbutton id="imipDeclineRecurrencesButton" + tooltiptext="&lightning.imipbar.btnDeclineRecurrences2.tooltiptext;" + label="&lightning.imipbar.btnDeclineRecurrences.label;" + oncommand="if (event.target.id == this.id) ltnImipBar.executeAction('DECLINED');" + type="menu-button" + class="toolbarbutton-1 msgHeaderView-button imipDeclineRecurrencesButton" + hidden="true"> + <menupopup id="imipDeclineRecurrencesDropdown"> + <menuitem id="imipDeclineRecurrencesButton_DeclineAll" + tooltiptext="&lightning.imipbar.btnDeclineRecurrences2.tooltiptext;" + label="&lightning.imipbar.btnDeclineRecurrences.label;" + oncommand="ltnImipBar.executeAction('DECLINED');"/> + <!-- add here more menuitem as needed --> + </menupopup> + </toolbarbutton> + + <!-- more options --> + <toolbarbutton id="imipMoreButton" + type="menu" + tooltiptext="&lightning.imipbar.btnMore.tooltiptext;" + label="&lightning.imipbar.btnMore.label;" + class="toolbarbutton-1 msgHeaderView-button imipMoreButton" + hidden="true"> + <menupopup id="imipMoreDropdown"> + <menuitem id="imipMoreButton_SaveCopy" + tooltiptext="&lightning.imipbar.btnSaveCopy.tooltiptext;" + label="&lightning.imipbar.btnSaveCopy.label;" + oncommand="ltnImipBar.executeAction('X-SAVECOPY');"/> + <!-- add here a menuitem as needed --> + </menupopup> + </toolbarbutton> + </toolbar> + </toolbox> + </lightning-notification-bar> + </vbox> + </vbox> +</overlay> diff --git a/calendar/lightning/content/imip-bar.js b/calendar/lightning/content/imip-bar.js new file mode 100644 index 000000000..039e0c36e --- /dev/null +++ b/calendar/lightning/content/imip-bar.js @@ -0,0 +1,470 @@ +/* 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/. */ + +Components.utils.import("resource://calendar/modules/calUtils.jsm"); +Components.utils.import("resource://calendar/modules/calItipUtils.jsm"); +Components.utils.import("resource://calendar/modules/calXMLUtils.jsm"); +Components.utils.import("resource://calendar/modules/ltnInvitationUtils.jsm"); +Components.utils.import("resource://gre/modules/Preferences.jsm"); +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); + +/** + * This bar lives inside the message window. + * Its lifetime is the lifetime of the main thunderbird message window. + */ +var ltnImipBar = { + + actionFunc: null, + itipItem: null, + foundItems: null, + msgOverlay: null, + + /** + * Thunderbird Message listener interface, hide the bar before we begin + */ + onStartHeaders: function() { + ltnImipBar.resetBar(); + }, + + /** + * Thunderbird Message listener interface + */ + onEndHeaders: function() { + + }, + + /** + * Load Handler called to initialize the imip bar + * NOTE: This function is called without a valid this-context! + */ + load: function() { + // Add a listener to gMessageListeners defined in msgHdrViewOverlay.js + gMessageListeners.push(ltnImipBar); + + // We need to extend the HideMessageHeaderPane function to also hide the + // message header pane. Otherwise, the imip bar will still be shown when + // changing folders. + ltnImipBar.tbHideMessageHeaderPane = HideMessageHeaderPane; + HideMessageHeaderPane = function() { + ltnImipBar.resetBar(); + ltnImipBar.tbHideMessageHeaderPane.apply(null, arguments); + }; + + // Set up our observers + Services.obs.addObserver(ltnImipBar, "onItipItemCreation", false); + }, + + /** + * Unload handler to clean up after the imip bar + * NOTE: This function is called without a valid this-context! + */ + unload: function() { + removeEventListener("messagepane-loaded", ltnImipBar.load, true); + removeEventListener("messagepane-unloaded", ltnImipBar.unload, true); + + ltnImipBar.resetBar(); + Services.obs.removeObserver(ltnImipBar, "onItipItemCreation"); + }, + + observe: function(subject, topic, state) { + if (topic == "onItipItemCreation") { + let itipItem = null; + let msgOverlay = null; + try { + if (!subject) { + let sinkProps = msgWindow.msgHeaderSink.properties; + // This property was set by lightningTextCalendarConverter.js + itipItem = sinkProps.getPropertyAsInterface("itipItem", + Components.interfaces.calIItipItem); + msgOverlay = sinkProps.getPropertyAsAUTF8String("msgOverlay"); + } + } catch (e) { + // This will throw on every message viewed that doesn't have the + // itipItem property set on it. So we eat the errors and move on. + + // XXX TODO: Only swallow the errors we need to. Throw all others. + } + if (!itipItem || !msgOverlay || !gMessageDisplay.displayedMessage) { + return; + } + + let imipMethod = gMessageDisplay.displayedMessage.getStringProperty("imip_method"); + cal.itip.initItemFromMsgData(itipItem, imipMethod, gMessageDisplay.displayedMessage); + + let imipBar = document.getElementById("imip-bar"); + imipBar.setAttribute("collapsed", "false"); + imipBar.setAttribute("label", cal.itip.getMethodText(itipItem.receivedMethod)); + + ltnImipBar.msgOverlay = msgOverlay; + + cal.itip.processItipItem(itipItem, ltnImipBar.setupOptions); + } + }, + + /** + * Hide the imip bar and reset the itip item. + */ + resetBar: function() { + document.getElementById("imip-bar").collapsed = true; + ltnImipBar.resetButtons(); + + // Clear our iMIP/iTIP stuff so it doesn't contain stale information. + cal.itip.cleanupItipItem(ltnImipBar.itipItem); + ltnImipBar.itipItem = null; + }, + + /** + * Resets all buttons and its menuitems, all buttons are hidden thereafter + */ + resetButtons: function() { + let buttons = ltnImipBar.getButtons(); + buttons.forEach(hideElement); + buttons.forEach(aButton => ltnImipBar.getMenuItems(aButton).forEach(showElement)); + }, + + /** + * Provides a list of all available buttons + */ + getButtons: function() { + let toolbarbuttons = document.getElementById("imip-view-toolbar") + .getElementsByTagName("toolbarbutton"); + return Array.from(toolbarbuttons); + }, + + /** + * Provides a list of available menuitems of a button + * + * @param aButton button node + */ + getMenuItems: function(aButton) { + let items = []; + let mitems = aButton.getElementsByTagName("menuitem"); + if (mitems != null && mitems.length > 0) { + for (let mitem of mitems) { + items.push(mitem); + } + } + return items; + }, + + /** + * Checks and converts button types based on available menuitems of the buttons + * to avoid dropdowns which are empty or only replicating the default button action + * Should be called once the buttons are set up + */ + conformButtonType: function() { + // check only needed on visible and not simple buttons + let buttons = ltnImipBar.getButtons() + .filter(aElement => aElement.hasAttribute("type") && !aElement.hidden); + // change button if appropriate + for (let button of buttons) { + let items = ltnImipBar.getMenuItems(button).filter(aItem => !aItem.hidden); + if (button.type == "menu" && items.length == 0) { + // hide non functional buttons + button.hidden = true; + } else if (button.type == "menu-button") { + if (items.length == 0 || + (items.length == 1 && + button.hasAttribute("oncommand") && + items[0].hasAttribute("oncommand") && + button.getAttribute("oncommand") + .endsWith(items[0].getAttribute("oncommand")))) { + // convert to simple button + button.removeAttribute("type"); + } + } + } + }, + + /** + * This is our callback function that is called each time the itip bar UI needs updating. + * NOTE: This function is called without a valid this-context! + * + * @param itipItem The iTIP item to set up for + * @param rc The status code from processing + * @param actionFunc The action function called for execution + * @param foundItems An array of items found while searching for the item + * in subscribed calendars + */ + setupOptions: function(itipItem, rc, actionFunc, foundItems) { + let imipBar = document.getElementById("imip-bar"); + let data = cal.itip.getOptionsText(itipItem, rc, actionFunc, foundItems); + + if (Components.isSuccessCode(rc)) { + ltnImipBar.itipItem = itipItem; + ltnImipBar.actionFunc = actionFunc; + ltnImipBar.foundItems = foundItems; + } + + // We need this to determine whether this is an outgoing or incoming message because + // Thunderbird doesn't provide a distinct flag on message level to do so. Relying on + // folder flags only may lead to false positives. + let isOutgoing = function(aMsgHdr) { + if (!aMsgHdr) { + return false; + } + let author = aMsgHdr.mime2DecodedAuthor; + let isSentFolder = aMsgHdr.folder && aMsgHdr.folder.flags & + Components.interfaces.nsMsgFolderFlags.SentMail; + if (author && isSentFolder) { + let accounts = MailServices.accounts; + for (let identity in fixIterator(accounts.allIdentities, + Components.interfaces.nsIMsgIdentity)) { + if (author.includes(identity.email) && !identity.fccReplyFollowsParent) { + return true; + } + } + } + return false; + }; + + // We override the bar label for sent out invitations and in case the event does not exist + // anymore, we also clear the buttons if any to avoid e.g. accept/decline buttons + if (isOutgoing(gMessageDisplay.displayedMessage)) { + if (ltnImipBar.foundItems && ltnImipBar.foundItems[0]) { + data.label = ltn.getString("lightning", "imipBarSentText"); + } else { + data = { + label: ltn.getString("lightning", "imipBarSentButRemovedText"), + buttons: [], + hideMenuItems: [] + }; + } + } + + imipBar.setAttribute("label", data.label); + // let's reset all buttons first + ltnImipBar.resetButtons(); + // menu items are visible by default, let's hide what's not available + data.hideMenuItems.forEach(aElementId => hideElement(document.getElementById(aElementId))); + // buttons are hidden by default, let's make required buttons visible + data.buttons.forEach(aElementId => showElement(document.getElementById(aElementId))); + // adjust button style if necessary + ltnImipBar.conformButtonType(); + ltnImipBar.displayModifications(); + }, + + /** + * Displays changes in case of invitation updates in invitation overlay + */ + displayModifications: function() { + if (!ltnImipBar.msgOverlay || !msgWindow || !ltnImipBar.foundItems || + !ltnImipBar.foundItems[0] || !ltnImipBar.itipItem) { + return; + } + + let msgOverlay = ltnImipBar.msgOverlay; + let diff = cal.itip.compare(ltnImipBar.itipItem.getItemList({})[0], ltnImipBar.foundItems[0]); + // displaying chnages is only needed if that is enabled, an item already exists and there are + // differences + if (diff != 0 && Preferences.get("calendar.itip.displayInvitationChanges", false)) { + let foundOverlay = ltn.invitation.createInvitationOverlay(ltnImipBar.foundItems[0], + ltnImipBar.itipItem); + let serializedOverlay = cal.xml.serializeDOM(foundOverlay); + let organizerId = ltnImipBar.itipItem.targetCalendar.getProperty("organizerId"); + if (diff == 1) { + // this is an update to previously accepted invitation + msgOverlay = ltn.invitation.compareInvitationOverlay(serializedOverlay, msgOverlay, + organizerId); + } else { + // this is a copy of a previously sent out invitation or a previous revision of a + // meanwhile accepted invitation, so we flip comparison order + msgOverlay = ltn.invitation.compareInvitationOverlay(msgOverlay, serializedOverlay, + organizerId); + } + } + msgWindow.displayHTMLInMessagePane("", msgOverlay, false); + }, + + executeAction: function(partStat, extendResponse) { + function _execAction(aActionFunc, aItipItem, aWindow, aPartStat) { + if (cal.itip.promptCalendar(aActionFunc.method, aItipItem, aWindow)) { + let isDeclineCounter = aPartStat == "X-DECLINECOUNTER"; + // filter out fake partstats + if (aPartStat.startsWith("X-")) { + partStat = ""; + } + // hide the buttons now, to disable pressing them twice... + if (aPartStat == partStat) { + ltnImipBar.resetButtons(); + } + + let opListener = { + QueryInterface: XPCOMUtils.generateQI([Components.interfaces.calIOperationListener]), + onOperationComplete: function(aCalendar, aStatus, aOperationType, aId, aDetail) { + if (Components.isSuccessCode(aStatus) && isDeclineCounter) { + // TODO: move the DECLINECOUNTER stuff to actionFunc + aItipItem.getItemList({}).forEach(aItem => { + // we can rely on the received itipItem to reply at this stage + // already, the checks have been done in cal.itip.processFoundItems + // when setting up the respective aActionFunc + let attendees = cal.getAttendeesBySender( + aItem.getAttendees({}), + aItipItem.sender + ); + let status = true; + if (attendees.length == 1 && ltnImipBar.foundItems && + ltnImipBar.foundItems.length) { + // we must return a message with the same sequence number as the + // counterproposal - to make it easy, we simply use the received + // item and just remove a comment, if any + try { + let item = aItem.clone(); + item.calendar = ltnImipBar.foundItems[0].calendar; + item.deleteProperty("COMMENT"); + // once we have full support to deal with for multiple items + // in a received invitation message, we should send this + // from outside outside of the forEach context + status = cal.itip.sendDeclineCounterMessage( + item, + "DECLINECOUNTER", + attendees, + { value: false } + ); + } catch (e) { + cal.ERROR(e); + status = false; + } + } else { + status = false; + } + if (!status) { + cal.ERROR("Failed to send DECLINECOUNTER reply!"); + } + }); + } + // For now, we just state the status for the user something very simple + let label = cal.itip.getCompleteText(aStatus, aOperationType); + imipBar.setAttribute("label", label); + + if (!Components.isSuccessCode(aStatus)) { + showError(label); + } + }, + onGetResult: function(aCalendar, aStatus, aItemType, aDetail, aCount, aItems) { + } + }; + + try { + aActionFunc(opListener, partStat); + } catch (exc) { + Components.utils.reportError(exc); + } + return true; + } + return false; + } + + let imipBar = document.getElementById("imip-bar"); + if (partStat == null) { + partStat = ""; + } + if (partStat == "X-SHOWDETAILS" || partStat == "X-RESCHEDULE") { + let counterProposal; + let items = ltnImipBar.foundItems; + if (items && items.length) { + let item = items[0].isMutable ? items[0] : items[0].clone(); + + if (partStat == "X-RESCHEDULE") { + // TODO most of the following should be moved to the actionFunc defined in + // calItipUtils + let proposedItem = ltnImipBar.itipItem.getItemList({})[0]; + let proposedRID = proposedItem.getProperty("RECURRENCE-ID"); + if (proposedRID) { + // if this is a counterproposal for a specific occurrence, we use + // that to compare with + item = item.recurrenceInfo.getOccurrenceFor(proposedRID).clone(); + } + let parsedProposal = ltn.invitation.parseCounter(proposedItem, item); + let potentialProposers = cal.getAttendeesBySender( + proposedItem.getAttendees({}), + ltnImipBar.itipItem.sender + ); + let proposingAttendee = potentialProposers.length == 1 ? + potentialProposers[0] : null; + if (proposingAttendee && + ["OK", "OUTDATED", "NOTLATESTUPDATE"].includes(parsedProposal.result.type)) { + counterProposal = { + attendee: proposingAttendee, + proposal: parsedProposal.differences, + oldVersion: parsedProposal.result == "OLDVERSION" || + parsedProposal.result == "NOTLATESTUPDATE", + onReschedule: () => { + imipBar.setAttribute( + "label", + ltn.getString("lightning", "imipBarCounterPreviousVersionText") + ); + // TODO: should we hide the buttons in this case, too? + } + }; + } else { + imipBar.setAttribute( + "label", + ltn.getString("lightning", "imipBarCounterErrorText") + ); + ltnImipBar.resetButtons(); + if (proposingAttendee) { + cal.LOG(parsedProposal.result.descr); + } else { + cal.LOG("Failed to identify the sending attendee of the counterproposal."); + } + + return false; + } + } + // if this a rescheduling operation, we suppress the occurrence prompt here + modifyEventWithDialog(item, null, partStat != "X-RESCHEDULE", null, counterProposal); + } + } else { + if (extendResponse) { + // Open an extended response dialog to enable the user to add a comment, make a + // counterproposal, delegate the event or interact in another way. + // Instead of a dialog, this might be implemented as a separate container inside the + // imip-overlay as proposed in bug 458578 + // + // If implemented as a dialog, the OL compatibility decision should be incorporated + // therein too and the itipItems's autoResponse set to auto subsequently + // to prevent a second popup during imip transport processing. + } + let delmgr = Components.classes["@mozilla.org/calendar/deleted-items-manager;1"] + .getService(Components.interfaces.calIDeletedItems); + let items = ltnImipBar.itipItem.getItemList({}); + if (items && items.length) { + let delTime = delmgr.getDeletedDate(items[0].id); + let dialogText = ltnGetString("lightning", "confirmProcessInvitation"); + let dialogTitle = ltnGetString("lightning", "confirmProcessInvitationTitle"); + if (delTime && !Services.prompt.confirm(window, dialogTitle, dialogText)) { + return false; + } + } + + if (partStat == "X-SAVECOPY") { + // we create and adopt copies of the respective events + let saveitems = ltnImipBar.itipItem.getItemList({}).map(cal.getPublishLikeItemCopy.bind(cal)); + if (saveitems.length > 0) { + let methods = { receivedMethod: "PUBLISH", responseMethod: "PUBLISH" }; + let newItipItem = cal.itip.getModifiedItipItem(ltnImipBar.itipItem, + saveitems, methods); + // control to avoid processing _execAction on later user changes on the item + let isFirstProcessing = true; + // setup callback and trigger re-processing + let storeCopy = function(aItipItem, aRc, aActionFunc, aFoundItems) { + if (isFirstProcessing && aActionFunc && Components.isSuccessCode(aRc)) { + _execAction(aActionFunc, aItipItem, window, partStat); + } + }; + cal.itip.processItipItem(newItipItem, storeCopy); + isFirstProcessing = false; + } + // we stop here to not process the original item + return false; + } + return _execAction(ltnImipBar.actionFunc, ltnImipBar.itipItem, window, partStat); + } + return false; + } +}; + +addEventListener("messagepane-loaded", ltnImipBar.load, true); +addEventListener("messagepane-unloaded", ltnImipBar.unload, true); diff --git a/calendar/lightning/content/lightning-calendar-creation.js b/calendar/lightning/content/lightning-calendar-creation.js new file mode 100644 index 000000000..e132bf581 --- /dev/null +++ b/calendar/lightning/content/lightning-calendar-creation.js @@ -0,0 +1,17 @@ +/* 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/. */ + +var common_initCustomizePage = initCustomizePage; +var common_doCreateCalendar = doCreateCalendar; + +initCustomizePage = function() { + common_initCustomizePage(); + ltnInitMailIdentitiesRow(); +}; + +doCreateCalendar = function() { + common_doCreateCalendar(); + ltnSaveMailIdentitySelection(); + return true; +}; diff --git a/calendar/lightning/content/lightning-calendar-creation.xul b/calendar/lightning/content/lightning-calendar-creation.xul new file mode 100644 index 000000000..e03dd17c7 --- /dev/null +++ b/calendar/lightning/content/lightning-calendar-creation.xul @@ -0,0 +1,30 @@ +<?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/. +--> + +<!DOCTYPE overlay SYSTEM "chrome://lightning/locale/lightning.dtd"> + +<overlay id="ltnCalendarCreationOverlay" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://calendar/content/calendar-ui-utils.js"/> + <script type="application/javascript" + src="chrome://lightning/content/lightning-utils.js"/> + <script type="application/javascript" + src="chrome://lightning/content/lightning-calendar-creation.js"/> + + <rows id="customize-rows"> + <row id="calendar-email-identity-row" + align="center" + insertafter="customize-suppressAlarms-row"> + <label value="&lightning.calendarproperties.email.label;" + control="email-identity-menulist"/> + <menulist id="email-identity-menulist"> + <menupopup id="email-identity-menupopup"/> + </menulist> + </row> + </rows> +</overlay> diff --git a/calendar/lightning/content/lightning-calendar-properties.js b/calendar/lightning/content/lightning-calendar-properties.js new file mode 100644 index 000000000..99a35367f --- /dev/null +++ b/calendar/lightning/content/lightning-calendar-properties.js @@ -0,0 +1,17 @@ +/* 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/. */ + +var common_onLoad = onLoad; +var common_onAcceptDialog = onAcceptDialog; + +onLoad = function() { + gCalendar = window.arguments[0].calendar; + ltnInitMailIdentitiesRow(); + common_onLoad(); +}; + +onAcceptDialog = function() { + ltnSaveMailIdentitySelection(); + return common_onAcceptDialog(); +}; diff --git a/calendar/lightning/content/lightning-calendar-properties.xul b/calendar/lightning/content/lightning-calendar-properties.xul new file mode 100644 index 000000000..0fdb06b4f --- /dev/null +++ b/calendar/lightning/content/lightning-calendar-properties.xul @@ -0,0 +1,32 @@ +<?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/. +--> + +<!DOCTYPE overlay SYSTEM "chrome://lightning/locale/lightning.dtd"> + +<overlay id="ltnCalendarPropertiesOverlay" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://calendar/content/calendar-ui-utils.js"/> + <script type="application/javascript" + src="chrome://lightning/content/lightning-utils.js"/> + <script type="application/javascript" + src="chrome://lightning/content/lightning-calendar-properties.js"/> + + <rows id="calendar-properties-rows"> + <row id="calendar-email-identity-row" + align="center" + insertafter="calendar-uri-row"> + <label value="&lightning.calendarproperties.email.label;" + control="email-identity-menulist" + disable-with-calendar="true"/> + <menulist id="email-identity-menulist" + disable-with-calendar="true"> + <menupopup id="email-identity-menupopup"/> + </menulist> + </row> + </rows> +</overlay> diff --git a/calendar/lightning/content/lightning-invitation.xhtml b/calendar/lightning/content/lightning-invitation.xhtml new file mode 100644 index 000000000..7d8ccf4f7 --- /dev/null +++ b/calendar/lightning/content/lightning-invitation.xhtml @@ -0,0 +1,74 @@ +<!-- 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/. --> + +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" + "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml"> + <head> + <meta http-equiv='Content-Type' content='text/html; charset=utf-8'/> + <link rel='stylesheet' type='text/css' href='chrome://messagebody/skin/imip.css'/> + </head> + <body> + <table id="invitation-table"> + <tr id="imipHtml-header-row"> + <th colspan="2" class="header"> + <p id="imipHtml-header-descr" class="header"/> + </th> + </tr> + <tr id="imipHtml-summary-row" hidden="true"> + <td class="description"><p id="imipHtml-summary-descr"/></td> + <td class="content"><p id="imipHtml-summary-content"/></td> + </tr> + <tr id="imipHtml-location-row" hidden="true"> + <td class="description"><p id="imipHtml-location-descr"/></td> + <td class="content"><p id="imipHtml-location-content"/></td> + </tr> + <tr id="imipHtml-when-row" hidden="true"> + <td class="description"><p id="imipHtml-when-descr"/></td> + <td class="content"><p id="imipHtml-when-content"/></td> + </tr> + <tr id="imipHtml-canceledOccurrences-row" hidden="true"> + <td class="description"><p id="imipHtml-canceledOccurrences-descr"/></td> + <td class="content"><p id="imipHtml-canceledOccurrences-content"/></td> + </tr> + <tr id="imipHtml-modifiedOccurrences-row" hidden="true"> + <td class="description"><p id="imipHtml-modifiedOccurrences-descr"/></td> + <td class="content"><p id="imipHtml-modifiedOccurrences-content"/></td> + </tr> + <tr id="imipHtml-organizer-row" hidden="true"> + <td class="description"><p id="imipHtml-organizer-descr"/></td> + <td class="content"> + <table id="organizer-table"/> + </td> + </tr> + <tr id="imipHtml-description-row" hidden="true"> + <td class="description"><p id="imipHtml-description-descr"/></td> + <td class="content"><p id="imipHtml-description-content"/></td> + </tr> + <tr id="imipHtml-attachments-row" hidden="true"> + <td class="description"><p id="imipHtml-attachments-descr"/></td> + <td class="content"><p id="imipHtml-attachments-content"/></td> + </tr> + <tr id="imipHtml-comment-row" hidden="true"> + <td class="description"><p id="imipHtml-comment-descr"/></td> + <td class="content"><p id="imipHtml-comment-content"/></td> + </tr> + <tr id="imipHtml-attendees-row" hidden="true"> + <td class="description"><p id="imipHtml-attendees-descr"/></td> + <td class="content"> + <table id="attendee-table"> + <tr id="attendee-template" hidden="true"> + <td><p class="itip-icon"/></td> + <td class="attendee-name"/> + </tr> + </table> + </td> + </tr> + <tr id="imipHtml-url-row" hidden="true"> + <td class="description"><p id="imipHtml-url-descr"/></td> + <td class="content"><p id="imipHtml-url-content"/></td> + </tr> + </table> + </body> +</html> diff --git a/calendar/lightning/content/lightning-item-iframe.js b/calendar/lightning/content/lightning-item-iframe.js new file mode 100644 index 000000000..800309dd7 --- /dev/null +++ b/calendar/lightning/content/lightning-item-iframe.js @@ -0,0 +1,4061 @@ +/* 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/. */ + +/* exported onEventDialogUnload, changeUndiscloseCheckboxStatus, + * toggleKeepDuration, dateTimeControls2State, onUpdateAllDay, + * openNewEvent, openNewTask, openNewMessage, openNewCardDialog, + * deleteAllAttachments, copyAttachment, attachmentLinkKeyPress, + * attachmentDblClick, attachmentClick, notifyUser, + * removeNotification, chooseRecentTimezone, showTimezonePopup, + * attendeeDblClick, attendeeClick, removeAttendee, + * removeAllAttendees, sendMailToUndecidedAttendees, checkUntilDate + */ + +Components.utils.import("resource://calendar/modules/calUtils.jsm"); +Components.utils.import("resource://gre/modules/Services.jsm"); +Components.utils.import("resource://calendar/modules/calRecurrenceUtils.jsm"); +Components.utils.import("resource:///modules/mailServices.js"); +Components.utils.import("resource://gre/modules/PluralForm.jsm"); +Components.utils.import("resource://gre/modules/Preferences.jsm"); +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); + +try { + Components.utils.import("resource:///modules/cloudFileAccounts.js"); +} catch (e) { + // This will fail on Seamonkey, but thats ok since the pref for cloudfiles + // is false, which means the UI will not be shown +} + +// Flag for using new item UI code (HTML/React.js). +const gNewItemUI = Preferences.get("calendar.item.useNewItemUI", false); + +// the following variables are constructed if the jsContext this file +// belongs to gets constructed. all those variables are meant to be accessed +// from within this file only. +var gStartTime = null; +var gEndTime = null; +var gItemDuration = null; +var gStartTimezone = null; +var gEndTimezone = null; +var gUntilDate = null; +var gIsReadOnly = false; +var gAttachMap = {}; +var gConfirmCancel = true; +var gLastRepeatSelection = 0; +var gIgnoreUpdate = false; +var gWarning = false; +var gPreviousCalendarId = null; +var gTabInfoObject; +var gConfig = { + priority: 0, + privacy: null, + status: "NONE", + showTimeAs: null, + percentComplete: 0 +}; +// The following variables are set by the load handler function of the +// parent context, so that they are already set before iframe content load: +// - gTimezoneEnabled +// - gShowLink + +var eventDialogQuitObserver = { + observe: function(aSubject, aTopic, aData) { + // Check whether or not we want to veto the quit request (unless another + // observer already did. + if (aTopic == "quit-application-requested" && + (aSubject instanceof Components.interfaces.nsISupportsPRBool) && + !aSubject.data) { + aSubject.data = !onCancel(); + } + } +}; + +var eventDialogCalendarObserver = { + target: null, + isObserving: false, + + onModifyItem: function(aNewItem, aOldItem) { + if (this.isObserving && "calendarItem" in window && + window.calendarItem && window.calendarItem.id == aOldItem.id) { + let doUpdate = true; + + // The item has been modified outside the dialog. We only need to + // prompt if there have been local changes also. + if (isItemChanged()) { + let promptService = Components.interfaces.nsIPromptService; + let promptTitle = calGetString("calendar", "modifyConflictPromptTitle"); + let promptMessage = calGetString("calendar", "modifyConflictPromptMessage"); + let promptButton1 = calGetString("calendar", "modifyConflictPromptButton1"); + let promptButton2 = calGetString("calendar", "modifyConflictPromptButton2"); + let flags = promptService.BUTTON_TITLE_IS_STRING * + promptService.BUTTON_POS_0 + + promptService.BUTTON_TITLE_IS_STRING * + promptService.BUTTON_POS_1; + + let choice = Services.prompt.confirmEx(window, promptTitle, promptMessage, flags, + promptButton1, promptButton2, null, null, {}); + if (!choice) { + doUpdate = false; + } + } + + let item = aNewItem; + if (window.calendarItem.recurrenceId && aNewItem.recurrenceInfo) { + item = aNewItem.recurrenceInfo + .getOccurrenceFor(window.calendarItem.recurrenceId) || item; + } + window.calendarItem = item; + + if (doUpdate) { + loadDialog(window.calendarItem); + } + } + }, + + onDeleteItem: function(aDeletedItem) { + if (this.isObserving && "calendarItem" in window && + window.calendarItem && window.calendarItem.id == aDeletedItem.id) { + cancelItem(); + } + }, + + onStartBatch: function() {}, + onEndBatch: function() {}, + onLoad: function() {}, + onAddItem: function() {}, + onError: function() {}, + onPropertyChanged: function() {}, + onPropertyDeleting: function() {}, + + observe: function(aCalendar) { + // use the new calendar if one was passed, otherwise use the last one + this.target = aCalendar || this.target; + if (this.target) { + this.cancel(); + this.target.addObserver(this); + this.isObserving = true; + } + }, + + cancel: function() { + if (this.isObserving && this.target) { + this.target.removeObserver(this); + this.isObserving = false; + } + } +}; + +/** + * Checks if the given calendar supports notifying attendees. The item is needed + * since calendars may support notifications for only some types of items. + * + * @param {calICalendar} aCalendar The calendar to check + * @param {calIItemBase} aItem The item to check support for + */ +function canNotifyAttendees(aCalendar, aItem) { + try { + let calendar = aCalendar.QueryInterface(Components.interfaces.calISchedulingSupport); + return (calendar.canNotify("REQUEST", aItem) && calendar.canNotify("CANCEL", aItem)); + } catch (exc) { + return false; + } +} + +/** + * Sends an asynchronous message to the parent context that contains the + * iframe. Additional properties of aMessage are generally arguments + * that will be passed to the function named in aMessage.command. + * + * @param {Object} aMessage The message to pass to the parent context + * @param {string} aMessage.command The name of a function to call + */ +function sendMessage(aMessage) { + parent.postMessage(aMessage, "*"); +} + +/** + * Receives asynchronous messages from the parent context that contains the iframe. + * + * @param {MessageEvent} aEvent Contains the message being received + */ +function receiveMessage(aEvent) { + let validOrigin = gTabmail ? "chrome://messenger" : "chrome://calendar"; + if (aEvent.origin !== validOrigin) { + return; + } + switch (aEvent.data.command) { + case "editAttendees": editAttendees(); break; + case "attachURL": attachURL(); break; + case "onCommandDeleteItem": onCommandDeleteItem(); break; + case "onCommandSave": onCommandSave(aEvent.data.isClosing); break; + case "onAccept": onAccept(); break; + case "onCancel": onCancel(aEvent.data.iframeId); break; + case "openNewEvent": openNewEvent(); break; + case "openNewTask": openNewTask(); break; + case "editConfigState": { + Object.assign(gConfig, aEvent.data.argument); + updateConfigState(aEvent.data.argument); + if (gNewItemUI) { + gTopComponent.importState(aEvent.data.argument); + } + break; + } + case "editToDoStatus": { + let textbox = document.getElementById("percent-complete-textbox"); + setElementValue(textbox, aEvent.data.value); + updateToDoStatus("percent-changed"); + break; + } + case "postponeTask": + postponeTask(aEvent.data.value); + break; + case "toggleTimezoneLinks": + gTimezonesEnabled = aEvent.data.checked; + updateDateTime(); + /* + // Not implemented in react-code.js yet + if (gNewItemUI) { + gTopComponent.importState({ timezonesEnabled: aEvent.data.checked }); + } + */ + break; + case "toggleLink": { + let newUrl = window.calendarItem.getProperty("URL") || ""; + let newShow = showOrHideItemURL(aEvent.data.checked, newUrl); + // Disable command if there is no url + if (!newUrl.length) { + sendMessage({ command: "disableLinkCommand" }); + } + if (gNewItemUI) { + gTopComponent.importState({ + url: newUrl, + showUrl: newShow + }); + } else { + updateItemURL(newShow, newUrl); + } + break; + } + case "closingWindowWithTabs": { + let response = onCancel(aEvent.data.id, true); + sendMessage({ + command: "replyToClosingWindowWithTabs", + response: response + }); + break; + } + case "attachFileByAccountKey": + attachFileByAccountKey(aEvent.data.accountKey); + break; + } +} + +/** + * Sets up the event dialog from the window arguments, also setting up all + * dialog controls from the window's item. + */ +function onLoad() { + window.addEventListener("message", receiveMessage, false); + + // first of all retrieve the array of + // arguments this window has been called with. + let args = window.arguments[0]; + + intializeTabOrWindowVariables(); + + // Needed so we can call switchToTab for the prompt about saving + // unsaved changes, to show the tab that the prompt is for. + if (gInTab) { + gTabInfoObject = gTabmail.currentTabInfo; + } + + // The calling entity provides us with an object that is responsible + // for recording details about the initiated modification. the 'finalize' + // property is our hook in order to receive a notification in case the + // operation needs to be terminated prematurely. This function will be + // called if the calling entity needs to immediately terminate the pending + // modification. In this case we serialize the item and close the window. + if (args.job) { + // keep the iframe id so we can close the right tab... + let iframeId = window.frameElement.id; + + // store the 'finalize'-functor in the provided job-object. + args.job.finalize = () => { + // store any pending modifications... + this.onAccept(); + + let item = window.calendarItem; + + // ...and close the window. + sendMessage({ command: "closeWindowOrTab", iframeId: iframeId }); + + return item; + }; + } + + window.fbWrapper = args.fbWrapper; + + // the most important attribute we expect from the + // arguments is the item we'll edit in the dialog. + let item = args.calendarEvent; + + // set the iframe's top level id for event vs task + if (!cal.isEvent(item)) { + setDialogId(document.documentElement, "calendar-task-dialog-inner"); + } + + // new items should have a non-empty title. + if (item.isMutable && (!item.title || item.title.length <= 0)) { + item.title = cal.calGetString("calendar-event-dialog", + isEvent(item) ? "newEvent" : "newTask"); + } + + window.onAcceptCallback = args.onOk; + window.mode = args.mode; + + // we store the item in the window to be able + // to access this from any location. please note + // that the item is either an occurrence [proxy] + // or the stand-alone item [single occurrence item]. + window.calendarItem = item; + // store the initial date value for datepickers in New Task dialog + window.initialStartDateValue = args.initialStartDateValue; + + // we store the array of attendees in the window. + // clone each existing attendee since we still suffer + // from the 'lost x-properties'-bug. + window.attendees = []; + let attendees = item.getAttendees({}); + if (attendees && attendees.length) { + for (let attendee of attendees) { + window.attendees.push(attendee.clone()); + } + } + + window.organizer = null; + if (item.organizer) { + window.organizer = item.organizer.clone(); + } else if (item.getAttendees({}).length > 0) { + // previous versions of calendar may have filled ORGANIZER correctly on overridden instances: + let orgId = item.calendar.getProperty("organizerId"); + if (orgId) { + let organizer = cal.createAttendee(); + organizer.id = orgId; + organizer.commonName = item.calendar.getProperty("organizerCN"); + organizer.role = "REQ-PARTICIPANT"; + organizer.participationStatus = "ACCEPTED"; + organizer.isOrganizer = true; + window.organizer = organizer; + } + } + + // we store the recurrence info in the window so it + // can be accessed from any location. since the recurrence + // info is a property of the parent item we need to check + // whether or not this item is a proxy or a parent. + let parentItem = item; + if (parentItem.parentItem != parentItem) { + parentItem = parentItem.parentItem; + } + + window.recurrenceInfo = null; + if (parentItem.recurrenceInfo) { + window.recurrenceInfo = parentItem.recurrenceInfo.clone(); + } + + // Set initial values for datepickers in New Tasks dialog + if (isToDo(item)) { + let initialDatesValue = cal.dateTimeToJsDate(args.initialStartDateValue); + if (!gNewItemUI) { + setElementValue("completed-date-picker", initialDatesValue); + setElementValue("todo-entrydate", initialDatesValue); + setElementValue("todo-duedate", initialDatesValue); + } + } + loadDialog(window.calendarItem); + + if (args.counterProposal) { + window.counterProposal = args.counterProposal; + displayCounterProposal(); + } + + gMainWindow.setCursor("auto"); + + if (!gNewItemUI) { + document.getElementById("item-title").focus(); + document.getElementById("item-title").select(); + } + + // This causes the app to ask if the window should be closed when the + // application is closed. + Services.obs.addObserver(eventDialogQuitObserver, + "quit-application-requested", false); + + // Normally, Enter closes a <dialog>. We want this to rather on Ctrl+Enter. + // Stopping event propagation doesn't seem to work, so just overwrite the + // function that does this. + if (!gInTab) { + document.documentElement._hitEnter = function() {}; + } + + // set up our calendar event observer + eventDialogCalendarObserver.observe(item.calendar); + + onLoad.hasLoaded = true; +} +// Set a variable to allow or prevent actions before the dialog is done loading. +onLoad.hasLoaded = false; + +function onEventDialogUnload() { + Services.obs.removeObserver(eventDialogQuitObserver, + "quit-application-requested"); + eventDialogCalendarObserver.cancel(); +} + +/** + * Handler function to be called when the accept button is pressed. + * + * @return Returns true if the window should be closed + */ +function onAccept() { + dispose(); + onCommandSave(true); + if (!gWarning) { + sendMessage({ command: "closeWindowOrTab" }); + } + return !gWarning; +} + +/** + * Asks the user if the item should be saved and does so if requested. If the + * user cancels, the window should stay open. + * + * XXX Could possibly be consolidated into onCancel() + * + * @return Returns true if the window should be closed. + */ +function onCommandCancel() { + if (gNewItemUI) { + // saving is not supported yet for gNewItemUI, return true to + // allow the tab to close + console.log("Saving changes is not yet supported with the HTML " + + "UI for editing events and tasks."); + return true; + } + + // Allow closing if the item has not changed and no warning dialog has to be showed. + if (!isItemChanged() && !gWarning) { + return true; + } + + if (gInTab && gTabInfoObject) { + // Switch to the tab that the prompt refers to. + gTabmail.switchToTab(gTabInfoObject); + } + + let promptService = Components.interfaces.nsIPromptService; + + let promptTitle = cal.calGetString("calendar", + isEvent(window.calendarItem) + ? "askSaveTitleEvent" + : "askSaveTitleTask"); + let promptMessage = cal.calGetString("calendar", + isEvent(window.calendarItem) + ? "askSaveMessageEvent" + : "askSaveMessageTask"); + + let flags = promptService.BUTTON_TITLE_SAVE * + promptService.BUTTON_POS_0 + + promptService.BUTTON_TITLE_CANCEL * + promptService.BUTTON_POS_1 + + promptService.BUTTON_TITLE_DONT_SAVE * + promptService.BUTTON_POS_2; + + let choice = Services.prompt.confirmEx(null, + promptTitle, + promptMessage, + flags, + null, + null, + null, + null, + {}); + switch (choice) { + case 0: // Save + onCommandSave(true); + return true; + case 2: // Don't save + // Don't show any warning dialog when closing without saving. + gWarning = false; + return true; + default: // Cancel + return false; + } +} + +/** + * Handler function to be called when the cancel button is pressed. + * aPreventClose is true when closing the main window but leaving the tab open. + * + * @param {string} aIframeId (optional) iframe id of the tab to be closed + * @param {boolean} aPreventClose (optional) True means don't close, just ask about saving + * @return {boolean} True if the tab or window should be closed + */ +function onCancel(aIframeId, aPreventClose) { + // The datepickers need to remove the focus in order to trigger the + // validation of the values just edited, with the keyboard, but not yet + // confirmed (i.e. not followed by a click, a tab or enter keys pressure). + document.documentElement.focus(); + + if (!gConfirmCancel || (gConfirmCancel && onCommandCancel())) { + dispose(); + // Don't allow closing the dialog when the user inputs a wrong + // date then closes the dialog and answers with "Save" in + // the "Save Event" dialog. Don't allow closing the dialog if + // the main window is being closed but the tabs in it are not. + + if (!gWarning && aPreventClose != true) { + sendMessage({ command: "closeWindowOrTab", iframeId: aIframeId }); + } + return !gWarning; + } + return false; +} + +/** + * Cancels (closes) either the window or the tab, for example when the + * item is being deleted. + */ +function cancelItem() { + gConfirmCancel = false; + if (gInTab) { + onCancel(); + } else { + sendMessage({ command: "cancelDialog" }); + } +} + +/** + * Sets up all dialog controls from the information of the passed item. + * + * @param aItem The item to parse information out of. + */ +function loadDialog(aItem) { + loadDateTime(aItem); + + let itemProps; + if (gNewItemUI) { + // Properties for initializing the React component/UI. + itemProps = { + initialTitle: aItem.title, + initialLocation: aItem.getProperty("LOCATION"), + initialStartTimezone: gStartTimezone, + initialEndTimezone: gEndTimezone, + initialStartTime: gStartTime, + initialEndTime: gEndTime + }; + } else { + setElementValue("item-title", aItem.title); + setElementValue("item-location", aItem.getProperty("LOCATION")); + } + + // add calendars to the calendar menulist + if (gNewItemUI) { + let calendarToUse = aItem.calendar || window.arguments[0].calendar; + let unfilteredList = sortCalendarArray(cal.getCalendarManager().getCalendars({})); + + // filter out calendars that should not be included + let calendarList = unfilteredList.filter((calendar) => + (calendar.id == calendarToUse.id || + (calendar && + isCalendarWritable(calendar) && + (userCanAddItemsToCalendar(calendar) || + (calendar == aItem.calendar && userCanModifyItem(aItem))) && + isItemSupported(aItem, calendar)))); + + itemProps.calendarList = calendarList.map(calendar => [calendar.id, calendar.name]); + + if (calendarToUse && calendarToUse.id) { + let index = itemProps.calendarList.findIndex( + calendar => (calendar[0] == calendarToUse.id)); + if (index != -1) { + itemProps.initialCalendarId = calendarToUse.id; + } + } + } else { + let calendarList = document.getElementById("item-calendar"); + removeChildren(calendarList); + let indexToSelect = appendCalendarItems(aItem, calendarList, aItem.calendar || window.arguments[0].calendar); + if (indexToSelect > -1) { + calendarList.selectedIndex = indexToSelect; + } + } + + // Categories + if (gNewItemUI) { + // XXX more to do here with localization, see loadCategories. + itemProps.initialCategoriesList = cal.sortArrayByLocaleCollator(getPrefCategoriesArray()); + itemProps.initialCategories = aItem.getCategories({}); + + // just to demo capsules component + itemProps.initialCategories = ["Some", "Demo", "Categories"]; + } else { + loadCategories(aItem); + } + + // Attachment + if (!gNewItemUI) { + loadCloudProviders(); + } + let hasAttachments = capSupported("attachments"); + let attachments = aItem.getAttachments({}); + if (gNewItemUI) { + itemProps.initialAttachments = {}; + } + if (hasAttachments && attachments && attachments.length > 0) { + for (let attachment of attachments) { + if (gNewItemUI) { + if (attachment && + attachment.hashId && + !(attachment.hashId in gAttachMap) && + // We currently only support uri attachments. + attachment.uri) { + itemProps.initialAttachments[attachment.hashId] = attachment; + + // XXX eventually we probably need to call addAttachment(attachment) + // here, until this works we just call updateAttachment() + updateAttachment(); + } + } else { + addAttachment(attachment); + } + } + } else { + updateAttachment(); + } + + // URL link + // Currently we always show the link for the tab case (if the link + // exists), since there is no menu item or toolbar item to show/hide it. + let showLink = gInTab ? true : gShowLink; + let itemUrl = window.calendarItem.getProperty("URL") || ""; + showLink = showOrHideItemURL(showLink, itemUrl); + + // Disable link command if there is no url + if (!itemUrl.length) { + sendMessage({ command: "disableLinkCommand" }); + } + if (gNewItemUI) { + itemProps.initialUrl = itemUrl; + itemProps.initialShowUrl = showLink; + } else { + updateItemURL(showLink, itemUrl); + } + + // Description + if (gNewItemUI) { + itemProps.initialDescription = aItem.getProperty("DESCRIPTION"); + } else { + setElementValue("item-description", aItem.getProperty("DESCRIPTION")); + } + + // Task completed date + if (!gNewItemUI) { + if (aItem.completedDate) { + updateToDoStatus(aItem.status, cal.dateTimeToJsDate(aItem.completedDate)); + } else { + updateToDoStatus(aItem.status); + } + } + + // Task percent complete + if (isToDo(aItem)) { + let percentCompleteInteger = 0; + let percentCompleteProperty = aItem.getProperty("PERCENT-COMPLETE"); + if (percentCompleteProperty != null) { + percentCompleteInteger = parseInt(percentCompleteProperty, 10); + } + if (percentCompleteInteger < 0) { + percentCompleteInteger = 0; + } else if (percentCompleteInteger > 100) { + percentCompleteInteger = 100; + } + gConfig.percentComplete = percentCompleteInteger; + if (gNewItemUI) { + itemProps.initialPercentComplete = percentCompleteInteger; + } else { + setElementValue("percent-complete-textbox", percentCompleteInteger); + } + } + + // When in a window, set Item-Menu label to Event or Task + if (!gInTab) { + let isEvent = cal.isEvent(aItem); + + let labelString = isEvent ? "itemMenuLabelEvent" : "itemMenuLabelTask"; + let label = cal.calGetString("calendar-event-dialog", labelString); + + let accessKeyString = isEvent ? "itemMenuAccesskeyEvent2" : "itemMenuAccesskeyTask2"; + let accessKey = cal.calGetString("calendar-event-dialog", accessKeyString); + sendMessage({ + command: "initializeItemMenu", + label: label, + accessKey: accessKey + }); + } + + // Repeat details + let [repeatType, untilDate] = getRepeatTypeAndUntilDate(aItem); + if (gNewItemUI) { + itemProps.initialRepeat = repeatType; + itemProps.initialRepeatUntilDate = untilDate; + // XXX more to do, see loadRepeat + } else { + loadRepeat(repeatType, untilDate, aItem); + } + + if (!gNewItemUI) { + // load reminders details + loadReminders(aItem.getAlarms({})); + + // Synchronize link-top-image with keep-duration-button status + let keepAttribute = document.getElementById("keepduration-button").getAttribute("keep") == "true"; + setBooleanAttribute("link-image-top", "keep", keepAttribute); + + updateDateTime(); + + updateCalendar(); + + // figure out what the title of the dialog should be and set it + // tabs already have their title set + if (!gInTab) { + updateTitle(); + } + + let notifyCheckbox = document.getElementById("notify-attendees-checkbox"); + let undiscloseCheckbox = document.getElementById("undisclose-attendees-checkbox"); + let disallowcounterCheckbox = document.getElementById("disallow-counter-checkbox"); + if (canNotifyAttendees(aItem.calendar, aItem)) { + // visualize that the server will send out mail: + notifyCheckbox.checked = true; + // hide these controls as this a client only feature + undiscloseCheckbox.disabled = true; + } else { + let itemProp = aItem.getProperty("X-MOZ-SEND-INVITATIONS"); + notifyCheckbox.checked = (aItem.calendar.getProperty("imip.identity") && + ((itemProp === null) + ? Preferences.get("calendar.itip.notify", true) + : (itemProp == "TRUE"))); + let undiscloseProp = aItem.getProperty("X-MOZ-SEND-INVITATIONS-UNDISCLOSED"); + undiscloseCheckbox.checked = (undiscloseProp === null) + ? false // default value as most common within organizations + : (undiscloseProp == "TRUE"); + // disable checkbox, if notifyCheckbox is not checked + undiscloseCheckbox.disabled = (notifyCheckbox.checked == false); + } + // this may also be a server exposed calendar property from exchange servers - if so, this + // probably should overrule the client-side config option + let disallowCounterProp = aItem.getProperty("X-MICROSOFT-DISALLOW-COUNTER"); + disallowcounterCheckbox.checked = disallowCounterProp == "TRUE"; + // if we're in reschedule mode, it's pointless to enable the control + disallowcounterCheckbox.disabled = !!window.counterProposal; + + updateAttendees(); + updateRepeat(true); + updateReminder(true); + } + + // Status + if (cal.isEvent(aItem)) { + gConfig.status = aItem.hasProperty("STATUS") ? + aItem.getProperty("STATUS") : "NONE"; + if (gConfig.status == "NONE") { + sendMessage({ command: "showCmdStatusNone" }); + } + updateConfigState({ status: gConfig.status }); + if (gNewItemUI) { + itemProps.initialStatus = gConfig.status; + } + } else { + let itemStatus = aItem.getProperty("STATUS"); + if (gNewItemUI) { + // Not implemented yet in react-code.js + // itemProps.initialTodoStatus = itemStatus; + } else { + let todoStatus = document.getElementById("todo-status"); + setElementValue(todoStatus, itemStatus); + if (!todoStatus.selectedItem) { + // No selected item means there was no <menuitem> that matches the + // value given. Select the "NONE" item by default. + setElementValue(todoStatus, "NONE"); + } + } + } + + // Priority, Privacy, Transparency + gConfig.priority = parseInt(aItem.priority, 10); + gConfig.privacy = aItem.privacy; + gConfig.showTimeAs = aItem.getProperty("TRANSP"); + + // update in outer parent context + updateConfigState(gConfig); + + // update in iframe (gNewItemUI only) + if (gNewItemUI) { + itemProps.initialPriority = gConfig.priority; + itemProps.supportsPriority = capSupported("priority"); + + itemProps.initialPrivacy = gConfig.privacy || "NONE"; + // XXX need to update the privacy options depending on calendar support for them + itemProps.supportsPrivacy = capSupported("privacy"); + + itemProps.initialShowTimeAs = gConfig.showTimeAs; + } + + // render the UI for gNewItemUI + if (gNewItemUI) { + gTopComponent = ReactDOM.render( + React.createElement(TopComponent, itemProps), + document.getElementById("container") + ); + } +} + +/** + * Enables/disables undiscloseCheckbox on (un)checking notifyCheckbox + */ +function changeUndiscloseCheckboxStatus() { + let notifyCheckbox = document.getElementById("notify-attendees-checkbox"); + let undiscloseCheckbox = document.getElementById("undisclose-attendees-checkbox"); + undiscloseCheckbox.disabled = (!notifyCheckbox.checked); +} + +/** + * Loads the item's categories into the category panel + * + * @param aItem The item to load into the category panel + */ +function loadCategories(aItem) { + let categoryPanel = document.getElementById("item-categories-panel"); + categoryPanel.loadItem(aItem); + updateCategoryMenulist(); +} + +/** + * Updates the category menulist to show the correct label, depending on the + * selected categories in the category panel + */ +function updateCategoryMenulist() { + let categoryMenulist = document.getElementById("item-categories"); + let categoryPanel = document.getElementById("item-categories-panel"); + + // Make sure the maximum number of categories is applied to the listbox + let calendar = getCurrentCalendar(); + let maxCount = calendar.getProperty("capabilities.categories.maxCount"); + categoryPanel.maxCount = (maxCount === null ? -1 : maxCount); + + // Hide the categories listbox and label in case categories are not + // supported + setBooleanAttribute("item-categories", "hidden", (maxCount === 0)); + setBooleanAttribute("item-categories-label", "hidden", (maxCount === 0)); + setBooleanAttribute("item-calendar-label", "hidden", (maxCount === 0)); + setBooleanAttribute("item-calendar-aux-label", "hidden", (maxCount !== 0)); + + let label; + let categoryList = categoryPanel.categories; + if (categoryList.length > 1) { + label = cal.calGetString("calendar", "multipleCategories"); + } else if (categoryList.length == 1) { + label = categoryList[0]; + } else { + label = cal.calGetString("calendar", "None"); + } + categoryMenulist.setAttribute("label", label); +} + +/** + * Saves the selected categories into the passed item + * + * @param aItem The item to set the categories on + */ +function saveCategories(aItem) { + let categoryPanel = document.getElementById("item-categories-panel"); + let categoryList = categoryPanel.categories; + aItem.setCategories(categoryList.length, categoryList); +} + +/** + * Sets up all date related controls from the passed item + * + * @param item The item to parse information out of. + */ +function loadDateTime(item) { + let kDefaultTimezone = calendarDefaultTimezone(); + if (isEvent(item)) { + let startTime = item.startDate; + let endTime = item.endDate; + let duration = endTime.subtractDate(startTime); + + // Check if an all-day event has been passed in (to adapt endDate). + if (startTime.isDate) { + startTime = startTime.clone(); + endTime = endTime.clone(); + + endTime.day--; + duration.days--; + } + + // store the start/end-times as calIDateTime-objects + // converted to the default timezone. store the timezones + // separately. + gStartTimezone = startTime.timezone; + gEndTimezone = endTime.timezone; + gStartTime = startTime.getInTimezone(kDefaultTimezone); + gEndTime = endTime.getInTimezone(kDefaultTimezone); + gItemDuration = duration; + } + + if (isToDo(item)) { + let startTime = null; + let endTime = null; + let duration = null; + + let hasEntryDate = (item.entryDate != null); + if (hasEntryDate) { + startTime = item.entryDate; + gStartTimezone = startTime.timezone; + startTime = startTime.getInTimezone(kDefaultTimezone); + } else { + gStartTimezone = kDefaultTimezone; + } + let hasDueDate = (item.dueDate != null); + if (hasDueDate) { + endTime = item.dueDate; + gEndTimezone = endTime.timezone; + endTime = endTime.getInTimezone(kDefaultTimezone); + } else { + gEndTimezone = kDefaultTimezone; + } + if (hasEntryDate && hasDueDate) { + duration = endTime.subtractDate(startTime); + } + if (!gNewItemUI) { + setElementValue("cmd_attendees", true, "disabled"); + setBooleanAttribute("keepduration-button", "disabled", !(hasEntryDate && hasDueDate)); + } + sendMessage({ + command: "updateConfigState", + argument: { attendeesCommand: false } + }); + gStartTime = startTime; + gEndTime = endTime; + gItemDuration = duration; + } else { + sendMessage({ + command: "updateConfigState", + argument: { attendeesCommand: true } + }); + } +} + +/** + * Toggles the "keep" attribute every time the keepduration-button is pressed. + */ +function toggleKeepDuration() { + let kdb = document.getElementById("keepduration-button"); + let keepAttribute = kdb.getAttribute("keep") == "true"; + // To make the "keep" attribute persistent, it mustn't be removed when in + // false state (bug 15232). + kdb.setAttribute("keep", keepAttribute ? "false" : "true"); + setBooleanAttribute("link-image-top", "keep", !keepAttribute); +} + +/** + * Handler function to be used when the Start time or End time of the event have + * changed. + * When changing the Start date, the End date changes automatically so the + * event/task's duration stays the same. Instead the End date is not linked + * to the Start date unless the the keepDurationButton has the "keep" attribute + * set to true. In this case modifying the End date changes the Start date in + * order to keep the same duration. + * + * @param aStartDatepicker If true the Start or Entry datepicker has changed, + * otherwise the End or Due datepicker has changed. + */ +function dateTimeControls2State(aStartDatepicker) { + if (gIgnoreUpdate) { + return; + } + let keepAttribute = document.getElementById("keepduration-button") + .getAttribute("keep") == "true"; + let allDay = getElementValue("event-all-day", "checked"); + let startWidgetId; + let endWidgetId; + if (isEvent(window.calendarItem)) { + startWidgetId = "event-starttime"; + endWidgetId = "event-endtime"; + } else { + if (!getElementValue("todo-has-entrydate", "checked")) { + gItemDuration = null; + } + if (!getElementValue("todo-has-duedate", "checked")) { + gItemDuration = null; + } + startWidgetId = "todo-entrydate"; + endWidgetId = "todo-duedate"; + } + + let saveStartTime = gStartTime; + let saveEndTime = gEndTime; + let kDefaultTimezone = calendarDefaultTimezone(); + + if (gStartTime) { + // jsDate is always in OS timezone, thus we create a calIDateTime + // object from the jsDate representation then we convert the timezone + // in order to keep gStartTime in default timezone. + if (gTimezonesEnabled || allDay) { + gStartTime = cal.jsDateToDateTime(getElementValue(startWidgetId), gStartTimezone); + gStartTime = gStartTime.getInTimezone(kDefaultTimezone); + } else { + gStartTime = cal.jsDateToDateTime(getElementValue(startWidgetId), kDefaultTimezone); + } + gStartTime.isDate = allDay; + } + if (gEndTime) { + if (aStartDatepicker) { + // Change the End date in order to keep the duration. + gEndTime = gStartTime.clone(); + if (gItemDuration) { + gEndTime.addDuration(gItemDuration); + } + } else { + let timezone = gEndTimezone; + if (timezone.isUTC) { + if (gStartTime && !compareObjects(gStartTimezone, gEndTimezone)) { + timezone = gStartTimezone; + } + } + if (gTimezonesEnabled || allDay) { + gEndTime = cal.jsDateToDateTime(getElementValue(endWidgetId), timezone); + gEndTime = gEndTime.getInTimezone(kDefaultTimezone); + } else { + gEndTime = cal.jsDateToDateTime(getElementValue(endWidgetId), kDefaultTimezone); + } + gEndTime.isDate = allDay; + if (keepAttribute && gItemDuration) { + // Keepduration-button links the the Start to the End date. We + // have to change the Start date in order to keep the duration. + let fduration = gItemDuration.clone(); + fduration.isNegative = true; + gStartTime = gEndTime.clone(); + gStartTime.addDuration(fduration); + } + } + } + + if (allDay) { + gStartTime.isDate = true; + gEndTime.isDate = true; + gItemDuration = gEndTime.subtractDate(gStartTime); + } + + // calculate the new duration of start/end-time. + // don't allow for negative durations. + let warning = false; + let stringWarning = ""; + if (!aStartDatepicker && gStartTime && gEndTime) { + if (gEndTime.compare(gStartTime) >= 0) { + gItemDuration = gEndTime.subtractDate(gStartTime); + } else { + gStartTime = saveStartTime; + gEndTime = saveEndTime; + warning = true; + stringWarning = cal.calGetString("calendar", "warningEndBeforeStart"); + } + } + + let startChanged = false; + if (gStartTime && saveStartTime) { + startChanged = gStartTime.compare(saveStartTime) != 0; + } + // Preset the date in the until-datepicker's minimonth to the new start + // date if it has changed. + if (startChanged) { + let startDate = cal.dateTimeToJsDate(gStartTime.getInTimezone(cal.floating())); + document.getElementById("repeat-until-datepicker").extraDate = startDate; + } + + // Sort out and verify the until date if the start date has changed. + if (gUntilDate && startChanged) { + // Make the time part of the until date equal to the time of start date. + updateUntildateRecRule(); + + // Don't allow for until date earlier than the start date. + if (gUntilDate.compare(gStartTime) < 0) { + // We have to restore valid dates. Since the user has intentionally + // changed the start date, it looks reasonable to restore a valid + // until date equal to the start date. + gUntilDate = gStartTime.clone(); + // Update the rule. + let rrules = splitRecurrenceRules(window.recurrenceInfo); + recRule = rrules[0][0]; + recRule.untilDate = gUntilDate.clone(); + // Update the until-date-picker. In case of "custom" rule, the + // recurrence string is going to be changed by updateDateTime() below. + let notCustomRule = document.getElementById("repeat-deck").selectedIndex == 0; + if (notCustomRule) { + setElementValue("repeat-until-datepicker", + cal.dateTimeToJsDate(gUntilDate.getInTimezone(cal.floating()))); + } + + warning = true; + stringWarning = cal.calGetString("calendar", "warningUntilDateBeforeStart"); + } + } + + updateDateTime(); + updateTimezone(); + updateAccept(); + + if (warning) { + // Disable the "Save" and "Save and Close" commands as long as the + // warning dialog is showed. + enableAcceptCommand(false); + gWarning = true; + let callback = function() { + Services.prompt.alert(null, document.title, stringWarning); + gWarning = false; + updateAccept(); + }; + setTimeout(callback, 1); + } +} + +/** + * Updates the entry date checkboxes, used for example when choosing an alarm: + * the entry date needs to be checked in that case. + */ +function updateEntryDate() { + updateDateCheckboxes( + "todo-entrydate", + "todo-has-entrydate", + { + isValid: function() { + return gStartTime != null; + }, + setDateTime: function(date) { + gStartTime = date; + } + }); +} + +/** + * Updates the due date checkboxes. + */ +function updateDueDate() { + updateDateCheckboxes( + "todo-duedate", + "todo-has-duedate", + { + isValid: function() { + return gEndTime != null; + }, + setDateTime: function(date) { + gEndTime = date; + } + }); +} + +/** + * Common function used by updateEntryDate and updateDueDate to set up the + * checkboxes correctly. + * + * @param aDatePickerId The XUL id of the datepicker to update. + * @param aCheckboxId The XUL id of the corresponding checkbox. + * @param aDateTime An object implementing the isValid and setDateTime + * methods. XXX explain. + */ +function updateDateCheckboxes(aDatePickerId, aCheckboxId, aDateTime) { + if (gIgnoreUpdate) { + return; + } + + if (!isToDo(window.calendarItem)) { + return; + } + + // force something to get set if there was nothing there before + setElementValue(aDatePickerId, getElementValue(aDatePickerId)); + + // first of all disable the datetime picker if we don't have a date + let hasDate = getElementValue(aCheckboxId, "checked"); + setElementValue(aDatePickerId, !hasDate, "disabled"); + + // create a new datetime object if date is now checked for the first time + if (hasDate && !aDateTime.isValid()) { + let date = cal.jsDateToDateTime(getElementValue(aDatePickerId), cal.calendarDefaultTimezone()); + aDateTime.setDateTime(date); + } else if (!hasDate && aDateTime.isValid()) { + aDateTime.setDateTime(null); + } + + // calculate the duration if possible + let hasEntryDate = getElementValue("todo-has-entrydate", "checked"); + let hasDueDate = getElementValue("todo-has-duedate", "checked"); + if (hasEntryDate && hasDueDate) { + let start = cal.jsDateToDateTime(getElementValue("todo-entrydate")); + let end = cal.jsDateToDateTime(getElementValue("todo-duedate")); + gItemDuration = end.subtractDate(start); + } else { + gItemDuration = null; + } + setBooleanAttribute("keepduration-button", "disabled", !(hasEntryDate && hasDueDate)); + updateDateTime(); + updateTimezone(); +} + +/** + * Get the item's recurrence information for displaying in dialog controls. + * + * @param {Object} aItem The calendar item + * @return {string[]} An array of two strings: [repeatType, untilDate] + */ +function getRepeatTypeAndUntilDate(aItem) { + let recurrenceInfo = window.recurrenceInfo; + let repeatType = "none"; + let untilDate = "forever"; + + /** + * Updates the until date (locally and globally). + * + * @param aRule The recurrence rule + */ + let updateUntilDate = (aRule) => { + if (!aRule.isByCount) { + if (aRule.isFinite) { + gUntilDate = aRule.untilDate.clone().getInTimezone(cal.calendarDefaultTimezone()); + untilDate = cal.dateTimeToJsDate(gUntilDate.getInTimezone(cal.floating())); + } else { + gUntilDate = null; + } + } + }; + + if (recurrenceInfo) { + repeatType = "custom"; + let ritems = recurrenceInfo.getRecurrenceItems({}); + let rules = []; + let exceptions = []; + for (let ritem of ritems) { + if (ritem.isNegative) { + exceptions.push(ritem); + } else { + rules.push(ritem); + } + } + if (rules.length == 1) { + let rule = cal.wrapInstance(rules[0], Components.interfaces.calIRecurrenceRule); + if (rule) { + switch (rule.type) { + case "DAILY": + if (!checkRecurrenceRule(rule, ["BYSECOND", + "BYMINUTE", + "BYHOUR", + "BYMONTHDAY", + "BYYEARDAY", + "BYWEEKNO", + "BYMONTH", + "BYSETPOS"])) { + let ruleComp = rule.getComponent("BYDAY", {}); + if (rule.interval == 1) { + if (ruleComp.length > 0) { + if (ruleComp.length == 5) { + let found = false; + for (let i = 0; i < 5; i++) { + if (ruleComp[i] != i + 2) { + found = true; + break; + } + } + if (!found && (!rule.isFinite || !rule.isByCount)) { + repeatType = "every.weekday"; + updateUntilDate(rule); + } + } + } else if (!rule.isFinite || !rule.isByCount) { + repeatType = "daily"; + updateUntilDate(rule); + } + } + } + break; + case "WEEKLY": + if (!checkRecurrenceRule(rule, ["BYSECOND", + "BYMINUTE", + "BYDAY", + "BYHOUR", + "BYMONTHDAY", + "BYYEARDAY", + "BYWEEKNO", + "BYMONTH", + "BYSETPOS"])) { + let weekType=["weekly", "bi.weekly"]; + if ((rule.interval == 1 || rule.interval == 2) && + (!rule.isFinite || !rule.isByCount)) { + repeatType = weekType[rule.interval - 1]; + updateUntilDate(rule); + } + } + break; + case "MONTHLY": + if (!checkRecurrenceRule(rule, ["BYSECOND", + "BYMINUTE", + "BYDAY", + "BYHOUR", + "BYMONTHDAY", + "BYYEARDAY", + "BYWEEKNO", + "BYMONTH", + "BYSETPOS"])) { + if (rule.interval == 1 && (!rule.isFinite || !rule.isByCount)) { + repeatType = "monthly"; + updateUntilDate(rule); + } + } + break; + case "YEARLY": + if (!checkRecurrenceRule(rule, ["BYSECOND", + "BYMINUTE", + "BYDAY", + "BYHOUR", + "BYMONTHDAY", + "BYYEARDAY", + "BYWEEKNO", + "BYMONTH", + "BYSETPOS"])) { + if (rule.interval == 1 && (!rule.isFinite || !rule.isByCount)) { + repeatType = "yearly"; + updateUntilDate(rule); + } + } + break; + } + } + } + } + return [repeatType, untilDate]; +} + +/** + * Updates the XUL UI with the repeat type and the until date. + * + * XXX For gNewItemUI we need to handle gLastRepeatSelection and + * disabling the element as we do in this function. + * + * @param {string} aRepeatType The type of repeat + * @param {string} aUntilDate The until date + * @param {Object} aItem The calendar item + */ +function loadRepeat(aRepeatType, aUntilDate, aItem) { + setElementValue("item-repeat", aRepeatType); + let repeatMenu = document.getElementById("item-repeat"); + gLastRepeatSelection = repeatMenu.selectedIndex; + + if (aItem.parentItem != aItem) { + disableElement("item-repeat"); + disableElement("repeat-until-datepicker"); + } + // Show the repeat-until-datepicker and set its date + document.getElementById("repeat-deck").selectedIndex = 0; + setElementValue("repeat-until-datepicker", aUntilDate); +} + +/** + * Update reminder related elements on the dialog. + * + * @param aSuppressDialogs If true, controls are updated without prompting + * for changes with the custom dialog + */ +function updateReminder(aSuppressDialogs) { + commonUpdateReminder(aSuppressDialogs); + updateAccept(); +} + +/** + * Saves all values the user chose on the dialog to the passed item + * + * @param item The item to save to. + */ +function saveDialog(item) { + // Calendar + item.calendar = getCurrentCalendar(); + + setItemProperty(item, "title", getElementValue("item-title")); + setItemProperty(item, "LOCATION", getElementValue("item-location")); + + saveDateTime(item); + + if (isToDo(item)) { + let percentCompleteInteger = 0; + if (getElementValue("percent-complete-textbox") != "") { + percentCompleteInteger = + parseInt(getElementValue("percent-complete-textbox"), 10); + } + if (percentCompleteInteger < 0) { + percentCompleteInteger = 0; + } else if (percentCompleteInteger > 100) { + percentCompleteInteger = 100; + } + setItemProperty(item, "PERCENT-COMPLETE", percentCompleteInteger); + } + + // Categories + saveCategories(item); + + // Attachment + // We want the attachments to be up to date, remove all first. + item.removeAllAttachments(); + + // Now add back the new ones + for (let hashId in gAttachMap) { + let att = gAttachMap[hashId]; + item.addAttachment(att); + } + + // Description + setItemProperty(item, "DESCRIPTION", getElementValue("item-description")); + + // Event Status + if (isEvent(item)) { + if (gConfig.status && gConfig.status != "NONE") { + item.setProperty("STATUS", gConfig.status); + } else { + item.deleteProperty("STATUS"); + } + } else { + let status = getElementValue("todo-status"); + if (status != "COMPLETED") { + item.completedDate = null; + } + setItemProperty(item, "STATUS", status == "NONE" ? null : status); + } + + // set the "PRIORITY" property if a valid priority has been + // specified (any integer value except *null*) OR the item + // already specifies a priority. in any other case we don't + // need this property and can safely delete it. we need this special + // handling since the WCAP provider always includes the priority + // with value *null* and we don't detect changes to this item if + // we delete this property. + if (capSupported("priority") && + (gConfig.priority || item.hasProperty("PRIORITY"))) { + item.setProperty("PRIORITY", gConfig.priority); + } else { + item.deleteProperty("PRIORITY"); + } + + // Transparency + if (gConfig.showTimeAs) { + item.setProperty("TRANSP", gConfig.showTimeAs); + } else { + item.deleteProperty("TRANSP"); + } + + // Privacy + setItemProperty(item, "CLASS", gConfig.privacy, "privacy"); + + if (item.status == "COMPLETED" && isToDo(item)) { + let elementValue = getElementValue("completed-date-picker"); + item.completedDate = cal.jsDateToDateTime(elementValue); + } + + saveReminder(item); +} + +/** + * Save date and time related values from the dialog to the passed item. + * + * @param item The item to save to. + */ +function saveDateTime(item) { + // Changes to the start date don't have to change the until date. + untilDateCompensation(item); + + if (isEvent(item)) { + let startTime = gStartTime.getInTimezone(gStartTimezone); + let endTime = gEndTime.getInTimezone(gEndTimezone); + let isAllDay = getElementValue("event-all-day", "checked"); + if (isAllDay) { + startTime = startTime.clone(); + endTime = endTime.clone(); + startTime.isDate = true; + endTime.isDate = true; + endTime.day += 1; + } else { + startTime = startTime.clone(); + startTime.isDate = false; + endTime = endTime.clone(); + endTime.isDate = false; + } + setItemProperty(item, "startDate", startTime); + setItemProperty(item, "endDate", endTime); + } + if (isToDo(item)) { + let startTime = gStartTime && gStartTime.getInTimezone(gStartTimezone); + let endTime = gEndTime && gEndTime.getInTimezone(gEndTimezone); + setItemProperty(item, "entryDate", startTime); + setItemProperty(item, "dueDate", endTime); + } +} + +/** + * Changes the until date in the rule in order to compensate the automatic + * correction caused by the function onStartDateChange() when saving the + * item. + * It allows to keep the until date set in the dialog irrespective of the + * changes that the user has done to the start date. + */ +function untilDateCompensation(aItem) { + // The current start date in the item is always the date that we get + // when opening the dialog or after the last save. + let startDate = aItem[calGetStartDateProp(aItem)]; + + if (aItem.recurrenceInfo) { + let rrules = splitRecurrenceRules(aItem.recurrenceInfo); + let rule = rrules[0][0]; + if (!rule.isByCount && rule.isFinite && startDate) { + let compensation = startDate.subtractDate(gStartTime); + if (compensation != "PT0S") { + let untilDate = rule.untilDate.clone(); + untilDate.addDuration(compensation); + rule.untilDate = untilDate; + } + } + } +} + +/** + * Updates the dialog title based on item type and if the item is new or to be + * modified. + */ +function updateTitle() { + let strName; + if (cal.isEvent(window.calendarItem)) { + strName = (window.mode == "new" ? "newEventDialog" : "editEventDialog"); + } else if (cal.isToDo(window.calendarItem)) { + strName = (window.mode == "new" ? "newTaskDialog" : "editTaskDialog"); + } else { + throw Components.results.NS_ERROR_NOT_IMPLEMENTED; + } + let newTitle = cal.calGetString("calendar", strName) + ": " + + getElementValue("item-title"); + sendMessage({ command: "updateTitle", argument: newTitle }); +} + +/** + * Update the disabled status of the accept button. The button is enabled if all + * parts of the dialog have options selected that make sense. + * constraining factors like + */ +function updateAccept() { + let enableAccept = true; + let kDefaultTimezone = calendarDefaultTimezone(); + let startDate; + let endDate; + let isEvent = cal.isEvent(window.calendarItem); + + // don't allow for end dates to be before start dates + if (isEvent) { + startDate = cal.jsDateToDateTime(getElementValue("event-starttime")); + endDate = cal.jsDateToDateTime(getElementValue("event-endtime")); + } else { + startDate = getElementValue("todo-has-entrydate", "checked") ? + cal.jsDateToDateTime(getElementValue("todo-entrydate")) : null; + endDate = getElementValue("todo-has-duedate", "checked") ? + cal.jsDateToDateTime(getElementValue("todo-duedate")) : null; + } + + if (startDate && endDate) { + if (gTimezonesEnabled) { + let startTimezone = gStartTimezone; + let endTimezone = gEndTimezone; + if (endTimezone.isUTC) { + if (!compareObjects(gStartTimezone, gEndTimezone)) { + endTimezone = gStartTimezone; + } + } + + startDate = startDate.getInTimezone(kDefaultTimezone); + endDate = endDate.getInTimezone(kDefaultTimezone); + + startDate.timezone = startTimezone; + endDate.timezone = endTimezone; + } + + startDate = startDate.getInTimezone(kDefaultTimezone); + endDate = endDate.getInTimezone(kDefaultTimezone); + + // For all-day events we are not interested in times and compare only + // dates. + if (isEvent && getElementValue("event-all-day", "checked")) { + // jsDateToDateTime returnes the values in UTC. Depending on the + // local timezone and the values selected in datetimepicker the date + // in UTC might be shifted to the previous or next day. + // For example: The user (with local timezone GMT+05) selected + // Feb 10 2006 00:00:00. The corresponding value in UTC is + // Feb 09 2006 19:00:00. If we now set isDate to true we end up with + // a date of Feb 09 2006 instead of Feb 10 2006 resulting in errors + // during the following comparison. + // Calling getInTimezone() ensures that we use the same dates as + // displayed to the user in datetimepicker for comparison. + startDate.isDate = true; + endDate.isDate = true; + } + } + + if (endDate && startDate && endDate.compare(startDate) == -1) { + enableAccept = false; + } + + enableAcceptCommand(enableAccept); + + return enableAccept; +} + +/** + * Enables/disables the commands cmd_accept and cmd_save related to the + * save operation. + * + * @param aEnable true: enables the command + */ +function enableAcceptCommand(aEnable) { + sendMessage({ command: "enableAcceptCommand", argument: aEnable }); +} + +// Global variables used to restore start and end date-time when changing the +// "all day" status in the onUpdateAllday() function. +var gOldStartTime = null; +var gOldEndTime = null; +var gOldStartTimezone = null; +var gOldEndTimezone = null; + +/** + * Handler function to update controls and state in consequence of the "all + * day" checkbox being clicked. + */ +function onUpdateAllDay() { + if (!isEvent(window.calendarItem)) { + return; + } + let allDay = getElementValue("event-all-day", "checked"); + let kDefaultTimezone = calendarDefaultTimezone(); + + if (allDay) { + // Store date-times and related timezones so we can restore + // if the user unchecks the "all day" checkbox. + gOldStartTime = gStartTime.clone(); + gOldEndTime = gEndTime.clone(); + gOldStartTimezone = gStartTimezone; + gOldEndTimezone = gEndTimezone; + // When events that end at 0:00 become all-day events, we need to + // subtract a day from the end date because the real end is midnight. + if (gEndTime.hour == 0 && gEndTime.minute == 0) { + let tempStartTime = gStartTime.clone(); + let tempEndTime = gEndTime.clone(); + tempStartTime.isDate = true; + tempEndTime.isDate = true; + tempStartTime.day++; + if (tempEndTime.compare(tempStartTime) >= 0) { + gEndTime.day--; + } + } + } else { + gStartTime.isDate = false; + gEndTime.isDate = false; + if (!gOldStartTime && !gOldEndTime) { + // The checkbox has been unchecked for the first time, the event + // was an "All day" type, so we have to set default values. + gStartTime.hour = getDefaultStartDate(window.initialStartDateValue).hour; + gEndTime.hour = gStartTime.hour; + gEndTime.minute += Preferences.get("calendar.event.defaultlength", 60); + gOldStartTimezone = kDefaultTimezone; + gOldEndTimezone = kDefaultTimezone; + } else { + // Restore date-times previously stored. + gStartTime.hour = gOldStartTime.hour; + gStartTime.minute = gOldStartTime.minute; + gEndTime.hour = gOldEndTime.hour; + gEndTime.minute = gOldEndTime.minute; + // When we restore 0:00 as end time, we need to add one day to + // the end date in order to include the last day until midnight. + if (gEndTime.hour == 0 && gEndTime.minute == 0) { + gEndTime.day++; + } + } + } + gStartTimezone = (allDay ? cal.floating() : gOldStartTimezone); + gEndTimezone = (allDay ? cal.floating() : gOldEndTimezone); + setShowTimeAs(allDay); + + updateAllDay(); +} + +/** + * This function sets the enabled/disabled state of the following controls: + * - 'event-starttime' + * - 'event-endtime' + * - 'timezone-starttime' + * - 'timezone-endtime' + * the state depends on whether or not the event is configured as 'all-day' or not. + */ +function updateAllDay() { + if (gIgnoreUpdate) { + return; + } + + if (!isEvent(window.calendarItem)) { + return; + } + + let allDay = getElementValue("event-all-day", "checked"); + setElementValue("event-starttime", allDay, "timepickerdisabled"); + setElementValue("event-endtime", allDay, "timepickerdisabled"); + + gStartTime.isDate = allDay; + gEndTime.isDate = allDay; + gItemDuration = gEndTime.subtractDate(gStartTime); + + updateDateTime(); + updateUntildateRecRule(); + updateRepeatDetails(); + updateAccept(); +} + +/** + * Use the window arguments to cause the opener to create a new event on the + * item's calendar + */ +function openNewEvent() { + let item = window.calendarItem; + let args = window.arguments[0]; + args.onNewEvent(item.calendar); +} + +/** + * Use the window arguments to cause the opener to create a new event on the + * item's calendar + */ +function openNewTask() { + let item = window.calendarItem; + let args = window.arguments[0]; + args.onNewTodo(item.calendar); +} + +/** + * Update the transparency status of this dialog, depending on if the event + * is all-day or not. + * + * @param allDay If true, the event is all-day + */ +function setShowTimeAs(allDay) { + gConfig.showTimeAs = cal.getEventDefaultTransparency(allDay); + updateConfigState({ showTimeAs: gConfig.showTimeAs }); +} + +function editAttendees() { + let savedWindow = window; + let calendar = getCurrentCalendar(); + + let callback = function(attendees, organizer, startTime, endTime) { + savedWindow.attendees = attendees; + if (organizer) { + // In case we didn't have an organizer object before we + // added attendees to our event we take the one created + // by the 'invite attendee'-dialog. + if (savedWindow.organizer) { + // The other case is that we already had an organizer object + // before we went throught the 'invite attendee'-dialog. In that + // case make sure we don't carry over attributes that have been + // set to their default values by the dialog but don't actually + // exist in the original organizer object. + if (!savedWindow.organizer.id) { + organizer.id = null; + } + if (!savedWindow.organizer.role) { + organizer.role = null; + } + if (!savedWindow.organizer.participationStatus) { + organizer.participationStatus = null; + } + if (!savedWindow.organizer.commonName) { + organizer.commonName = null; + } + } + savedWindow.organizer = organizer; + } + let duration = endTime.subtractDate(startTime); + startTime = startTime.clone(); + endTime = endTime.clone(); + let kDefaultTimezone = calendarDefaultTimezone(); + gStartTimezone = startTime.timezone; + gEndTimezone = endTime.timezone; + gStartTime = startTime.getInTimezone(kDefaultTimezone); + gEndTime = endTime.getInTimezone(kDefaultTimezone); + gItemDuration = duration; + updateAttendees(); + updateDateTime(); + updateAllDay(); + + if (isAllDay != gStartTime.isDate) { + setShowTimeAs(gStartTime.isDate); + } + }; + + let startTime = gStartTime.getInTimezone(gStartTimezone); + let endTime = gEndTime.getInTimezone(gEndTimezone); + + let isAllDay = getElementValue("event-all-day", "checked"); + if (isAllDay) { + startTime.isDate = true; + endTime.isDate = true; + endTime.day += 1; + } else { + startTime.isDate = false; + endTime.isDate = false; + } + let args = {}; + args.startTime = startTime; + args.endTime = endTime; + args.displayTimezone = gTimezonesEnabled; + args.attendees = window.attendees; + args.organizer = window.organizer && window.organizer.clone(); + args.calendar = calendar; + args.item = window.calendarItem; + args.onOk = callback; + args.fbWrapper = window.fbWrapper; + + // open the dialog modally + openDialog( + "chrome://calendar/content/calendar-event-dialog-attendees.xul", + "_blank", + "chrome,titlebar,modal,resizable", + args); +} + +/** + * Updates the UI outside of the iframe (toolbar, menu, statusbar, etc.) + * for changes in priority, privacy, status, showTimeAs/transparency, + * and/or other properties. This function should be called any time that + * gConfig.privacy, gConfig.priority, etc. are updated. + * + * Privacy and priority updates depend on the selected calendar. If the + * selected calendar does not support them, or only supports certain + * values, these are removed from the UI. + * + * @param {Object} aArg Container + * @param {string} aArg.privacy (optional) The new privacy value + * @param {short} aArg.priority (optional) The new priority value + * @param {string} aArg.status (optional) The new status value + * @param {string} aArg.showTimeAs (optional) The new transparency value + */ +function updateConfigState(aArg) { + // We include additional info for priority and privacy. + if (aArg.hasOwnProperty("priority")) { + aArg.hasPriority = capSupported("priority"); + } + if (aArg.hasOwnProperty("privacy")) { + Object.assign(aArg, { + hasPrivacy: capSupported("privacy"), + calendarType: getCurrentCalendar().type, + privacyValues: capValues("privacy", + ["PUBLIC", "CONFIDENTIAL", "PRIVATE"]) + }); + } + + // For tasks, do not include showTimeAs + if (aArg.hasOwnProperty("showTimeAs") && cal.isToDo(window.calendarItem)) { + delete aArg.showTimeAs; + if (Object.keys(aArg).length == 0) { + return; + } + } + + sendMessage({ command: "updateConfigState", argument: aArg }); +} + +/** + * Add menu items to the UI for attaching files using cloud providers. + */ +function loadCloudProviders() { + let cloudFileEnabled = Preferences.get("mail.cloud_files.enabled", false); + let cmd = document.getElementById("cmd_attach_cloud"); + let message = { + command: "setElementAttribute", + argument: { id: "cmd_attach_cloud", attribute: "hidden", value: null } + }; + + if (!cloudFileEnabled) { + // If cloud file support is disabled, just hide the attach item + cmd.hidden = true; + message.argument.value = true; + sendMessage(message); + return; + } + + let isHidden = cloudFileAccounts.accounts.length == 0; + cmd.hidden = isHidden; + message.argument.value = isHidden; + sendMessage(message); + + let itemObjects = []; + + for (let cloudProvider of cloudFileAccounts.accounts) { + // Create a serializable object to pass in a message outside the iframe + let itemObject = {}; + itemObject.displayName = cloudFileAccounts.getDisplayName(cloudProvider); + itemObject.label = cal.calGetString("calendar-event-dialog", "attachViaFilelink", [itemObject.displayName]); + itemObject.cloudProviderAccountKey = cloudProvider.accountKey; + if (cloudProvider.iconClass) { + itemObject.class = "menuitem-iconic"; + itemObject.image = cloudProvider.iconClass; + } + + itemObjects.push(itemObject); + + // Create a menu item from the serializable object + let item = createXULElement("menuitem"); + item.setAttribute("label", itemObject.label); + item.setAttribute("observes", "cmd_attach_cloud"); + item.setAttribute("oncommand", "attachFile(event.target.cloudProvider); event.stopPropagation();"); + + if (itemObject.class) { + item.setAttribute("class", itemObject.class); + item.setAttribute("image", itemObject.image); + } + + // Add the menu item to places inside the iframe where we advertise cloud providers + let attachmentPopup = document.getElementById("attachment-popup"); + attachmentPopup.appendChild(item).cloudProvider = cloudProvider; + } + + // Add the items to places outside the iframe where we advertise cloud providers + sendMessage({ command: "loadCloudProviders", items: itemObjects }); +} + +/** + * Prompts the user to attach an url to this item. + */ +function attachURL() { + if (Services.prompt) { + // ghost in an example... + let result = { value: "http://" }; + if (Services.prompt.prompt(window, + calGetString("calendar-event-dialog", + "specifyLinkLocation"), + calGetString("calendar-event-dialog", + "enterLinkLocation"), + result, + null, + { value: 0 })) { + try { + // If something bogus was entered, makeURL may fail. + let attachment = createAttachment(); + attachment.uri = makeURL(result.value); + addAttachment(attachment); + } catch (e) { + // TODO We might want to show a warning instead of just not + // adding the file + } + } + } +} + +/** + * Attach a file using a cloud provider, identified by its accountKey. + * + * @param {string} aAccountKey The accountKey for a cloud provider + */ +function attachFileByAccountKey(aAccountKey) { + for (let cloudProvider of cloudFileAccounts.accounts) { + if (aAccountKey == cloudProvider.accountKey) { + attachFile(cloudProvider); + return; + } + } +} + +/** + * Attach a file to the item. Not passing a cloud provider is currently unsupported. + * + * @param cloudProvider If set, the cloud provider will be used for attaching + */ +function attachFile(cloudProvider) { + if (!cloudProvider) { + cal.ERROR("[calendar-event-dialog] Could not attach file without cloud provider" + cal.STACK(10)); + } + + let files; + try { + const nsIFilePicker = Components.interfaces.nsIFilePicker; + let filePicker = Components.classes["@mozilla.org/filepicker;1"] + .createInstance(nsIFilePicker); + filePicker.init(window, + calGetString("calendar-event-dialog", "selectAFile"), + nsIFilePicker.modeOpenMultiple); + + // Check for the last directory + let lastDir = lastDirectory(); + if (lastDir) { + filePicker.displayDirectory = lastDir; + } + + // Get the attachment + if (filePicker.show() == nsIFilePicker.returnOK) { + files = filePicker.files; + } + } catch (ex) { + dump("failed to get attachments: " +ex+ "\n"); + } + + // Check if something has to be done + if (!files || !files.hasMoreElements()) { + return; + } + + // Create the attachment + while (files.hasMoreElements()) { + let file = files.getNext().QueryInterface(Components.interfaces.nsILocalFile); + + let fileHandler = Services.io.getProtocolHandler("file") + .QueryInterface(Components.interfaces.nsIFileProtocolHandler); + let uriSpec = fileHandler.getURLSpecFromFile(file); + + if (!(uriSpec in gAttachMap)) { + // If the attachment hasn't been added, then set the last display + // directory. + lastDirectory(uriSpec); + + // ... and add the attachment. + let attachment = cal.createAttachment(); + if (cloudProvider) { + attachment.uri = makeURL(uriSpec); + } else { + // TODO read file into attachment + } + addAttachment(attachment, cloudProvider); + } + } +} + +/** + * Helper function to remember the last directory chosen when attaching files. + * + * @param aFileUri (optional) If passed, the last directory will be set and + * returned. If null, the last chosen directory + * will be returned. + * @return The last directory that was set with this function. + */ +function lastDirectory(aFileUri) { + if (aFileUri) { + // Act similar to a setter, save the passed uri. + let uri = makeURL(aFileUri); + let file = uri.QueryInterface(Components.interfaces.nsIFileURL).file; + lastDirectory.mValue = file.parent.QueryInterface(Components.interfaces.nsILocalFile); + } + + // In any case, return the value + return (lastDirectory.mValue === undefined ? null : lastDirectory.mValue); +} + +/** + * Turns an url into a string that can be used in UI. + * - For a file:// url, shows the filename. + * - For a http:// url, removes protocol and trailing slash + * + * @param aUri The uri to parse. + * @return A string that can be used in UI. + */ +function makePrettyName(aUri) { + let name = aUri.spec; + + if (aUri.schemeIs("file")) { + name = aUri.spec.split("/").pop(); + } else if (aUri.schemeIs("http")) { + name = aUri.spec.replace(/\/$/, "").replace(/^http:\/\//, ""); + } + return name; +} + +/** + * Asynchronously uploads the given attachment to the cloud provider, updating + * the passed listItem as things progress. + * + * @param attachment A calIAttachment to upload + * @param cloudProvider The clould provider to upload to + * @param listItem The listitem in attachment-link listbox to update. + */ +function uploadCloudAttachment(attachment, cloudProvider, listItem) { + let file = attachment.uri.QueryInterface(Components.interfaces.nsIFileURL).file; + listItem.attachLocalFile = file; + listItem.attachCloudProvider = cloudProvider; + cloudProvider.uploadFile(file, { + onStartRequest: function() { + listItem.setAttribute("image", "chrome://global/skin/icons/loading.png"); + }, + + onStopRequest: function(aRequest, aContext, aStatusCode) { + if (Components.isSuccessCode(aStatusCode)) { + delete gAttachMap[attachment.hashId]; + attachment.uri = makeURL(cloudProvider.urlForFile(file)); + attachment.setParameter("FILENAME", file.leafName); + attachment.setParameter("PROVIDER", cloudProvider.type); + listItem.setAttribute("label", file.leafName); + gAttachMap[attachment.hashId] = attachment; + listItem.setAttribute("image", cloudProvider.iconClass); + updateAttachment(); + } else { + cal.ERROR("[calendar-event-dialog] Uploading cloud attachment " + + "failed. Status code: " + aStatusCode); + + // Uploading failed. First of all, show an error icon. Also, + // delete it from the attach map now, this will make sure it is + // not serialized if the user saves. + listItem.setAttribute("image", "chrome://messenger/skin/icons/error.png"); + delete gAttachMap[attachment.hashId]; + + // Keep the item for a while so the user can see something failed. + // When we have a nice notification bar, we can show more info + // about the failure. + setTimeout(() => { + listItem.remove(); + updateAttachment(); + }, 5000); + } + } + }); +} + +/** + * Adds the given attachment to dialog controls. + * + * @param attachment The calIAttachment object to add + * @param cloudProvider (optional) If set, the given cloud provider will be used. + */ +function addAttachment(attachment, cloudProvider) { + if (!attachment || + !attachment.hashId || + attachment.hashId in gAttachMap) { + return; + } + + // We currently only support uri attachments + if (attachment.uri) { + let documentLink = document.getElementById("attachment-link"); + let listItem = createXULElement("listitem"); + + // Set listitem attributes + listItem.setAttribute("label", makePrettyName(attachment.uri)); + listItem.setAttribute("crop", "end"); + listItem.setAttribute("class", "listitem-iconic"); + listItem.setAttribute("tooltiptext", attachment.uri.spec); + if (cloudProvider) { + if (attachment.uri.schemeIs("file")) { + // Its still a local url, needs to be uploaded + listItem.setAttribute("image", "chrome://messenger/skin/icons/connecting.png"); + uploadCloudAttachment(attachment, cloudProvider, listItem); + } else { + let leafName = attachment.getParameter("FILENAME"); + listItem.setAttribute("image", cloudProvider.iconClass); + if (leafName) { + listItem.setAttribute("label", leafName); + } + } + } else if (attachment.uri.schemeIs("file")) { + listItem.setAttribute("image", "moz-icon://" + attachment.uri); + } else { + let leafName = attachment.getParameter("FILENAME"); + let providerType = attachment.getParameter("PROVIDER"); + let cloudFileEnabled = Preferences.get("mail.cloud_files.enabled", false); + + if (leafName) { + // TODO security issues? + listItem.setAttribute("label", leafName); + } + if (providerType && cloudFileEnabled) { + let provider = cloudFileAccounts.getProviderForType(providerType); + listItem.setAttribute("image", provider.iconClass); + } else { + listItem.setAttribute("image", "moz-icon://dummy.html"); + } + } + + // Now that everything is set up, add it to the attachment box. + documentLink.appendChild(listItem); + + // full attachment object is stored here + listItem.attachment = attachment; + + // Update the number of rows and save our attachment globally + documentLink.rows = documentLink.getRowCount(); + } + + gAttachMap[attachment.hashId] = attachment; + updateAttachment(); +} + +/** + * Removes the currently selected attachment from the dialog controls. + * + * XXX This could use a dialog maybe? + */ +function deleteAttachment() { + let documentLink = document.getElementById("attachment-link"); + let item = documentLink.selectedItem; + delete gAttachMap[item.attachment.hashId]; + documentLink.removeItemAt(documentLink.selectedIndex); + + if (item.attachLocalFile && item.attachCloudProvider) { + try { + item.attachCloudProvider.deleteFile(item.attachLocalFile, { + onStartRequest: function() {}, + onStopRequest: function(aRequest, aContext, aStatusCode) { + if (!Components.isSuccessCode(aStatusCode)) { + // TODO With a notification bar, we could actually show this error. + cal.ERROR("[calendar-event-dialog] Deleting cloud attachment " + + "failed, file will remain on server. " + + " Status code: " + aStatusCode); + } + } + }); + } catch (e) { + cal.ERROR("[calendar-event-dialog] Deleting cloud attachment " + + "failed, file will remain on server. " + + "Exception: " + e); + } + } + + updateAttachment(); +} + +/** + * Removes all attachments from the dialog controls. + */ +function deleteAllAttachments() { + let documentLink = document.getElementById("attachment-link"); + let itemCount = documentLink.getRowCount(); + let canRemove = (itemCount < 2); + + if (itemCount > 1) { + let removeText = PluralForm.get(itemCount, cal.calGetString("calendar-event-dialog", "removeAttachmentsText")); + let removeTitle = cal.calGetString("calendar-event-dialog", "removeCalendarsTitle"); + canRemove = Services.prompt.confirm(window, removeTitle, removeText.replace("#1", itemCount), {}); + } + + if (canRemove) { + let child; + while (documentLink.hasChildNodes()) { + child = documentLink.lastChild; + child.attachment = null; + child.remove(); + } + gAttachMap = {}; + } + updateAttachment(); +} + +/** + * Opens the selected attachment using the external protocol service. + * @see nsIExternalProtocolService + */ +function openAttachment() { + // Only one file has to be selected and we don't handle base64 files at all + let documentLink = document.getElementById("attachment-link"); + if (documentLink.selectedItems.length == 1) { + let attURI = documentLink.getSelectedItem(0).attachment.uri; + let externalLoader = Components.classes["@mozilla.org/uriloader/external-protocol-service;1"] + .getService(Components.interfaces.nsIExternalProtocolService); + // TODO There should be a nicer dialog + externalLoader.loadUrl(attURI); + } +} + +/** + * Copies the link location of the first selected attachment to the clipboard + */ +function copyAttachment() { + let documentLink = document.getElementById("attachment-link"); + let attURI = documentLink.getSelectedItem(0).attachment.uri.spec; + let clipboard = Components.classes["@mozilla.org/widget/clipboardhelper;1"] + .getService(Components.interfaces.nsIClipboardHelper); + clipboard.copyString(attURI); +} + +/** + * Handler function to handle pressing keys in the attachment listbox. + * + * @param aEvent The DOM event caused by the key press. + */ +function attachmentLinkKeyPress(aEvent) { + const kKE = Components.interfaces.nsIDOMKeyEvent; + switch (aEvent.keyCode) { + case kKE.DOM_VK_BACK_SPACE: + case kKE.DOM_VK_DELETE: + deleteAttachment(); + break; + case kKE.DOM_VK_RETURN: + openAttachment(); + break; + } +} + +/** + * Handler function to take care of double clicking on an attachment + * + * @param aEvent The DOM event caused by the clicking. + */ +function attachmentDblClick(aEvent) { + // left double click on a list item + if (aEvent.originalTarget.localName == "listitem" && aEvent.button == 0) { + openAttachment(); + } +} + +/** + * Handler function to take care of right clicking on an attachment or the attachment list + * + * @param aEvent The DOM event caused by the clicking. + */ +function attachmentClick(aEvent) { + // we take only care about right clicks + if (aEvent.button != 2) { + return; + } + let attachmentPopup = document.getElementById("attachment-popup"); + for (let node of attachmentPopup.childNodes) { + if (aEvent.originalTarget.localName == "listitem" || + node.id == "attachment-popup-attachPage") { + showElement(node); + } else { + hideElement(node); + } + } +} + +/** + * Helper function to show a notification in the event-dialog's notificationBox + * + * @param aMessage the message text to show + * @param aValue string identifying the notification + * @param aPriority (optional) the priority of the warning (info, critical), default is 'warn' + * @param aImage (optional) URL of image to appear on the notification + * @param aButtonset (optional) array of button descriptions to appear on the notification + * @param aCallback (optional) a function to handle events from the notificationBox + */ +function notifyUser(aMessage, aValue, aPriority, aImage, aButtonset, aCallback) { + let notificationBox = document.getElementById("event-dialog-notifications"); + // only append, if the notification does not already exist + if (notificationBox.getNotificationWithValue(aValue) == null) { + const prioMap = { + info: notificationBox.PRIORITY_INFO_MEDIUM, + critical: notificationBox.PRIORITY_CRITICAL_MEDIUM + }; + let priority = prioMap[aPriority] || notificationBox.PRIORITY_WARNING_MEDIUM; + notificationBox.appendNotification(aMessage, + aValue, + aImage, + priority, + aButtonset, + aCallback); + } +} + +/** + * Remove a notification from the notifiactionBox + * + * @param aValue string identifying the notification to remove + */ +function removeNotification(aValue) { + let notificationBox = document.getElementById("event-dialog-notifications"); + let notification = notificationBox.getNotificationWithValue(aValue); + if (notification != null) { + notificationBox.removeNotification(notification); + } +} + +/** + * Update the dialog controls related to the item's calendar. + */ +function updateCalendar() { + let item = window.calendarItem; + let calendar = getCurrentCalendar(); + + gIsReadOnly = calendar.readOnly; + + if (!gPreviousCalendarId) { + gPreviousCalendarId = item.calendar.id; + } + + // We might have to change the organizer, let's see + let calendarOrgId = calendar.getProperty("organizerId"); + if (window.organizer && calendarOrgId && + calendar.id != gPreviousCalendarId) { + window.organizer.id = calendarOrgId; + window.organizer.commonName = calendar.getProperty("organizerCN"); + gPreviousCalendarId = calendar.id; + } + + if (!canNotifyAttendees(calendar, item) && calendar.getProperty("imip.identity")) { + enableElement("notify-attendees-checkbox"); + enableElement("undisclose-attendees-checkbox"); + } else { + disableElement("notify-attendees-checkbox"); + disableElement("undisclose-attendees-checkbox"); + } + + // update the accept button + updateAccept(); + + // TODO: the code above decided about whether or not the item is readonly. + // below we enable/disable all controls based on this decision. + // unfortunately some controls need to be disabled based on some other + // criteria. this is why we enable all controls in case the item is *not* + // readonly and run through all those updateXXX() functions to disable + // them again based on the specific logic build into those function. is this + // really a good idea? + if (gIsReadOnly) { + let disableElements = document.getElementsByAttribute("disable-on-readonly", "true"); + for (let element of disableElements) { + element.setAttribute("disabled", "true"); + + // we mark link-labels with the hyperlink attribute, since we need + // to remove their class in case they get disabled. TODO: it would + // be better to create a small binding for those link-labels + // instead of adding those special stuff. + if (element.hasAttribute("hyperlink")) { + element.removeAttribute("class"); + element.removeAttribute("onclick"); + } + } + + let collapseElements = document.getElementsByAttribute("collapse-on-readonly", "true"); + for (let element of collapseElements) { + element.setAttribute("collapsed", "true"); + } + } else { + sendMessage({ command: "removeDisableAndCollapseOnReadonly" }); + + let enableElements = document.getElementsByAttribute("disable-on-readonly", "true"); + for (let element of enableElements) { + element.removeAttribute("disabled"); + if (element.hasAttribute("hyperlink")) { + element.setAttribute("class", "text-link"); + } + } + + let collapseElements = document.getElementsByAttribute("collapse-on-readonly", "true"); + for (let element of collapseElements) { + element.removeAttribute("collapsed"); + } + + // Task completed date + if (item.completedDate) { + updateToDoStatus(item.status, cal.dateTimeToJsDate(item.completedDate)); + } else { + updateToDoStatus(item.status); + } + + // disable repeat menupopup if this is an occurrence + item = window.calendarItem; + if (item.parentItem != item) { + disableElement("item-repeat"); + disableElement("repeat-until-datepicker"); + let repeatDetails = document.getElementById("repeat-details"); + let numChilds = repeatDetails.childNodes.length; + for (let i = 0; i < numChilds; i++) { + let node = repeatDetails.childNodes[i]; + node.setAttribute("disabled", "true"); + node.removeAttribute("class"); + node.removeAttribute("onclick"); + } + } + + // If the item is a proxy occurrence/instance, a few things aren't + // valid. + if (item.parentItem != item) { + disableElement("item-calendar"); + + // don't allow to revoke the entrydate of recurring todo's. + disableElementWithLock("todo-has-entrydate", "permanent-lock"); + } + + // update datetime pickers, disable checkboxes if dates are required by + // recurrence or reminders. + updateRepeat(true); + updateReminder(true); + updateAllDay(); + } + + // Make sure capabilties are reflected correctly + updateCapabilities(); +} + +/** + * Opens the recurrence dialog modally to allow the user to edit the recurrence + * rules. + */ +function editRepeat() { + let args = {}; + args.calendarEvent = window.calendarItem; + args.recurrenceInfo = window.recurrenceInfo; + args.startTime = gStartTime; + args.endTime = gEndTime; + + let savedWindow = window; + args.onOk = function(recurrenceInfo) { + savedWindow.recurrenceInfo = recurrenceInfo; + }; + + window.setCursor("wait"); + + // open the dialog modally + openDialog( + "chrome://calendar/content/calendar-event-dialog-recurrence.xul", + "_blank", + "chrome,titlebar,modal,resizable", + args); +} + +/** + * This function is responsible for propagating UI state to controls + * depending on the repeat setting of an item. This functionality is used + * after the dialog has been loaded as well as if the repeat pattern has + * been changed. + * + * @param aSuppressDialogs If true, controls are updated without prompting + * for changes with the recurrence dialog + * @param aItemRepeatCall True when the function is being called from + * the item-repeat menu list. It allows to detect + * a change from the "custom" option. + */ +function updateRepeat(aSuppressDialogs, aItemRepeatCall) { + function setUpEntrydateForTask(item) { + // if this item is a task, we need to make sure that it has + // an entry-date, otherwise we can't create a recurrence. + if (isToDo(item)) { + // automatically check 'has entrydate' if needed. + if (!getElementValue("todo-has-entrydate", "checked")) { + setElementValue("todo-has-entrydate", "true", "checked"); + + // make sure gStartTime is properly initialized + updateEntryDate(); + } + + // disable the checkbox to indicate that we need + // the entry-date. the 'disabled' state will be + // revoked if the user turns off the repeat pattern. + disableElementWithLock("todo-has-entrydate", "repeat-lock"); + } + } + + let repeatMenu = document.getElementById("item-repeat"); + let repeatValue = repeatMenu.selectedItem.getAttribute("value"); + let repeatDeck = document.getElementById("repeat-deck"); + + if (repeatValue == "none") { + repeatDeck.selectedIndex = -1; + window.recurrenceInfo = null; + let item = window.calendarItem; + if (isToDo(item)) { + enableElementWithLock("todo-has-entrydate", "repeat-lock"); + } + } else if (repeatValue == "custom") { + let lastRepeatDeck = repeatDeck.selectedIndex; + repeatDeck.selectedIndex = 1; + // the user selected custom repeat pattern. we now need to bring + // up the appropriate dialog in order to let the user specify the + // new rule. First of all, retrieve the item we want to specify + // the custom repeat pattern for. + let item = window.calendarItem; + + setUpEntrydateForTask(item); + + // retrieve the current recurrence info, we need this + // to find out whether or not the user really created + // a new repeat pattern. + let recurrenceInfo = window.recurrenceInfo; + + // now bring up the recurrence dialog. + // don't pop up the dialog if aSuppressDialogs was specified or if + // called during initialization of the dialog. + if (!aSuppressDialogs && repeatMenu.hasAttribute("last-value")) { + editRepeat(); + } + + // Assign gUntilDate on the first run or when returning from the + // edit recurrence dialog. + if (window.recurrenceInfo) { + let rrules = splitRecurrenceRules(window.recurrenceInfo); + let rule = rrules[0][0]; + gUntilDate = null; + if (!rule.isByCount && rule.isFinite) { + gUntilDate = rule.untilDate.clone().getInTimezone(calendarDefaultTimezone()); + } + } + + // we need to address two separate cases here. + // 1)- We need to revoke the selection of the repeat + // drop down list in case the user didn't specify + // a new repeat pattern (i.e. canceled the dialog); + // - re-enable the 'has entrydate' option in case + // we didn't end up with a recurrence rule. + // 2) Check whether the new recurrence rule needs the + // recurrence details text or it can be displayed + // only with the repeat-until-datepicker. + if (recurrenceInfo == window.recurrenceInfo) { + repeatMenu.selectedIndex = gLastRepeatSelection; + repeatDeck.selectedIndex = lastRepeatDeck; + if (isToDo(item)) { + if (!window.recurrenceInfo) { + enableElementWithLock("todo-has-entrydate", "repeat-lock"); + } + } + } else { + // From the Edit Recurrence dialog, the rules "every day" and + // "every weekday" don't need the recurrence details text when they + // have only the until date. The getRepeatTypeAndUntilDate() + // function verifies whether this is the case. + let [repeatType, untilDate] = getRepeatTypeAndUntilDate(item); + if (gNewItemUI) { + gTopComponent.importState({ + repeat: repeatType, + repeatUntilDate: untilDate + }); + // XXX more to do, see loadRepeat + } else { + loadRepeat(repeatType, untilDate, window.calendarItem); + } + } + } else { + let item = window.calendarItem; + let recurrenceInfo = window.recurrenceInfo || item.recurrenceInfo; + let proposedUntilDate = (gStartTime || window.initialStartDateValue).clone(); + + if (recurrenceInfo) { + recurrenceInfo = recurrenceInfo.clone(); + let rrules = splitRecurrenceRules(recurrenceInfo); + let rule = rrules[0][0]; + + // If the previous rule was "custom" we have to recover the until + // date, or the last occurrence's date in order to set the + // repeat-until-datepicker with the same date. + if (aItemRepeatCall && repeatDeck.selectedIndex == 1) { + let repeatDate; + if (!rule.isByCount || !rule.isFinite) { + if (rule.isFinite) { + repeatDate = rule.untilDate.getInTimezone(cal.floating()); + repeatDate = cal.dateTimeToJsDate(repeatDate); + } else { + repeatDate = "forever"; + } + } else { + // Try to recover the last occurrence in 10(?) years. + let endDate = gStartTime.clone(); + endDate.year += 10; + let lastOccurrenceDate = null; + let dates = recurrenceInfo.getOccurrenceDates(gStartTime, endDate, 0, {}); + if (dates) { + lastOccurrenceDate = dates[dates.length - 1]; + } + repeatDate = (lastOccurrenceDate || proposedUntilDate).getInTimezone(cal.floating()); + repeatDate = cal.dateTimeToJsDate(repeatDate); + } + setElementValue("repeat-until-datepicker", repeatDate); + } + if (rrules[0].length > 0) { + recurrenceInfo.deleteRecurrenceItem(rule); + } + } else { + // New event proposes "forever" as default until date. + recurrenceInfo = createRecurrenceInfo(item); + setElementValue("repeat-until-datepicker", "forever"); + } + + repeatDeck.selectedIndex = 0; + + let recRule = createRecurrenceRule(); + recRule.interval = 1; + switch (repeatValue) { + case "daily": + recRule.type = "DAILY"; + break; + case "weekly": + recRule.type = "WEEKLY"; + break; + case "every.weekday": + recRule.type = "DAILY"; + recRule.setComponent("BYDAY", 5, [2, 3, 4, 5, 6]); + break; + case "bi.weekly": + recRule.type = "WEEKLY"; + recRule.interval = 2; + break; + case "monthly": + recRule.type = "MONTHLY"; + break; + case "yearly": + recRule.type = "YEARLY"; + break; + } + + setUpEntrydateForTask(item); + updateUntildateRecRule(recRule); + + recurrenceInfo.insertRecurrenceItemAt(recRule, 0); + window.recurrenceInfo = recurrenceInfo; + + if (isToDo(item)) { + if (!getElementValue("todo-has-entrydate", "checked")) { + setElementValue("todo-has-entrydate", "true", "checked"); + } + disableElementWithLock("todo-has-entrydate", "repeat-lock"); + } + + // Preset the until-datepicker's minimonth to the start date. + let startDate = cal.dateTimeToJsDate(gStartTime.getInTimezone(cal.floating())); + document.getElementById("repeat-until-datepicker").extraDate = startDate; + } + + gLastRepeatSelection = repeatMenu.selectedIndex; + repeatMenu.setAttribute("last-value", repeatValue); + + updateRepeatDetails(); + updateEntryDate(); + updateDueDate(); + updateAccept(); +} + +/** + * Update the until date in the recurrence rule in order to set + * the same time of the start date. + * + * @param recRule (optional) The recurrence rule + */ +function updateUntildateRecRule(recRule) { + if (!recRule) { + let recurrenceInfo = window.recurrenceInfo; + if (!recurrenceInfo) { + return; + } + let rrules = splitRecurrenceRules(recurrenceInfo); + recRule = rrules[0][0]; + } + let defaultTimezone = cal.calendarDefaultTimezone(); + let repeatUntilDate = null; + + let itemRepeat = document.getElementById("item-repeat").selectedItem.value; + if (itemRepeat == "none") { + return; + } else if (itemRepeat == "custom") { + repeatUntilDate = gUntilDate; + } else { + let untilDatepickerDate = getElementValue("repeat-until-datepicker"); + if (untilDatepickerDate != "forever") { + repeatUntilDate = cal.jsDateToDateTime(untilDatepickerDate, defaultTimezone); + } + } + + if (repeatUntilDate) { + if (onLoad.hasLoaded) { + repeatUntilDate.isDate = gStartTime.isDate; // Enforce same value type as DTSTART + if (!gStartTime.isDate) { + repeatUntilDate.hour = gStartTime.hour; + repeatUntilDate.minute = gStartTime.minute; + repeatUntilDate.second = gStartTime.second; + } + } + recRule.untilDate = repeatUntilDate.clone(); + gUntilDate = repeatUntilDate.clone().getInTimezone(defaultTimezone); + } else { + // Rule that recurs forever or with a "count" number of recurrences. + gUntilDate = null; + } +} + +/** + * Updates the UI controls related to a task's completion status. + * + * @param {string} aStatus The item's completion status or a string + * that allows to identify a change in the + * percent-complete's textbox. + * @param {Date} aCompletedDate The item's completed date (as a JSDate). + */ +function updateToDoStatus(aStatus, aCompletedDate=null) { + // RFC2445 doesn't support completedDates without the todo's status + // being "COMPLETED", however twiddling the status menulist shouldn't + // destroy that information at this point (in case you change status + // back to COMPLETED). When we go to store this VTODO as .ics the + // date will get lost. + + // remember the original values + let oldPercentComplete = parseInt(getElementValue("percent-complete-textbox"), 10); + let oldCompletedDate = getElementValue("completed-date-picker"); + + // If the percent completed has changed to 100 or from 100 to another + // value, the status must change. + if (aStatus == "percent-changed") { + let selectedIndex = document.getElementById("todo-status").selectedIndex; + let menuItemCompleted = selectedIndex == 3; + let menuItemNotSpecified = selectedIndex == 0; + if (oldPercentComplete == 100) { + aStatus = "COMPLETED"; + } else if (menuItemCompleted || menuItemNotSpecified) { + aStatus = "IN-PROCESS"; + } + } + + switch (aStatus) { + case null: + case "": + case "NONE": + oldPercentComplete = 0; + document.getElementById("todo-status").selectedIndex = 0; + disableElement("percent-complete-textbox"); + disableElement("percent-complete-label"); + break; + case "CANCELLED": + document.getElementById("todo-status").selectedIndex = 4; + disableElement("percent-complete-textbox"); + disableElement("percent-complete-label"); + break; + case "COMPLETED": + document.getElementById("todo-status").selectedIndex = 3; + enableElement("percent-complete-textbox"); + enableElement("percent-complete-label"); + // if there is no aCompletedDate, set it to the previous value + if (!aCompletedDate) { + aCompletedDate = oldCompletedDate; + } + break; + case "IN-PROCESS": + document.getElementById("todo-status").selectedIndex = 2; + disableElement("completed-date-picker"); + enableElement("percent-complete-textbox"); + enableElement("percent-complete-label"); + break; + case "NEEDS-ACTION": + document.getElementById("todo-status").selectedIndex = 1; + enableElement("percent-complete-textbox"); + enableElement("percent-complete-label"); + break; + } + + let newPercentComplete; + if ((aStatus == "IN-PROCESS" || aStatus == "NEEDS-ACTION") && + oldPercentComplete == 100) { + newPercentComplete = 0; + setElementValue("completed-date-picker", oldCompletedDate); + disableElement("completed-date-picker"); + } else if (aStatus == "COMPLETED") { + newPercentComplete = 100; + setElementValue("completed-date-picker", aCompletedDate); + enableElement("completed-date-picker"); + } else { + newPercentComplete = oldPercentComplete; + setElementValue("completed-date-picker", oldCompletedDate); + disableElement("completed-date-picker"); + } + + gConfig.percentComplete = newPercentComplete; + setElementValue("percent-complete-textbox", newPercentComplete); + if (gInTab) { + sendMessage({ + command: "updateConfigState", + argument: { percentComplete: newPercentComplete } + }); + } +} + +/** + * Saves all dialog controls back to the item. + * + * @return a copy of the original item with changes made. + */ +function saveItem() { + // we need to clone the item in order to apply the changes. + // it is important to not apply the changes to the original item + // (even if it happens to be mutable) in order to guarantee + // that providers see a proper oldItem/newItem pair in case + // they rely on this fact (e.g. WCAP does). + let originalItem = window.calendarItem; + let item = originalItem.clone(); + + // override item's recurrenceInfo *before* serializing date/time-objects. + if (!item.recurrenceId) { + item.recurrenceInfo = window.recurrenceInfo; + } + + // serialize the item + saveDialog(item); + + item.organizer = window.organizer; + + item.removeAllAttendees(); + if (window.attendees && (window.attendees.length > 0)) { + for (let attendee of window.attendees) { + item.addAttendee(attendee); + } + + let notifyCheckbox = document.getElementById("notify-attendees-checkbox"); + if (notifyCheckbox.disabled) { + item.deleteProperty("X-MOZ-SEND-INVITATIONS"); + } else { + item.setProperty("X-MOZ-SEND-INVITATIONS", notifyCheckbox.checked ? "TRUE" : "FALSE"); + } + let undiscloseCheckbox = document.getElementById("undisclose-attendees-checkbox"); + if (undiscloseCheckbox.disabled) { + item.deleteProperty("X-MOZ-SEND-INVITATIONS-UNDISCLOSED"); + } else { + item.setProperty("X-MOZ-SEND-INVITATIONS-UNDISCLOSED", + undiscloseCheckbox.checked ? "TRUE" : "FALSE"); + } + let disallowcounterCheckbox = document.getElementById("disallow-counter-checkbox"); + let xProp = window.calendarItem.getProperty("X-MICROSOFT-DISALLOW-COUNTER"); + // we want to leave an existing x-prop in case the checkbox is disabled as we need to + // roundtrip x-props that are not exclusively under our control + if (!disallowcounterCheckbox.disabled) { + // we only set the prop if we need to + if (disallowcounterCheckbox.checked) { + item.setProperty("X-MICROSOFT-DISALLOW-COUNTER", "TRUE"); + } else if (xProp) { + item.setProperty("X-MICROSOFT-DISALLOW-COUNTER", "FALSE"); + } + } + } + + // We check if the organizerID is different from our + // calendar-user-address-set. The organzerID is the owner of the calendar. + // If it's different, that is because someone is acting on behalf of + // the organizer. + if (item.organizer && item.calendar.aclEntry) { + let userAddresses = item.calendar.aclEntry.getUserAddresses({}); + if (userAddresses.length > 0 && + !cal.attendeeMatchesAddresses(item.organizer, userAddresses)) { + let organizer = item.organizer.clone(); + organizer.setProperty("SENT-BY", "mailto:" + userAddresses[0]); + item.organizer = organizer; + } + } + return item; +} + +/** + * Action to take when the user chooses to save. This can happen either by + * saving directly or the user selecting to save after being prompted when + * closing the dialog. + * + * This function also takes care of notifying this dialog's caller that the item + * is saved. + * + * @param aIsClosing If true, the save action originates from the + * save prompt just before the window is closing. + */ +function onCommandSave(aIsClosing) { + // The datepickers need to remove the focus in order to trigger the + // validation of the values just edited, with the keyboard, but not yet + // confirmed (i.e. not followed by a click, a tab or enter keys pressure). + document.documentElement.focus(); + + // Don't save if a warning dialog about a wrong input date must be showed. + if (gWarning) { + return; + } + + eventDialogCalendarObserver.cancel(); + + let originalItem = window.calendarItem; + let item = saveItem(); + let calendar = getCurrentCalendar(); + + item.makeImmutable(); + // Set the item for now, the callback below will set the full item when the + // call succeeded + window.calendarItem = item; + + // When the call is complete, we need to set the new item, so that the + // dialog is up to date. + + // XXX Do we want to disable the dialog or at least the save button until + // the call is complete? This might help when the user tries to save twice + // before the call is complete. In that case, we do need a progress bar and + // the ability to cancel the operation though. + let listener = { + QueryInterface: XPCOMUtils.generateQI([Components.interfaces.calIOperationListener]), + onOperationComplete: function(aCalendar, aStatus, aOpType, aId, aItem) { + // Check if the current window has a calendarItem first, because in case of undo + // window refers to the main window and we would get a 'calendarItem is undefined' warning. + if (!aIsClosing && "calendarItem" in window) { + // If we changed the calendar of the item, onOperationComplete will be called multiple + // times. We need to make sure we're receiving the update on the right calendar. + if ((!window.calendarItem.id ||aId == window.calendarItem.id) && + (aCalendar.id == window.calendarItem.calendar.id) && + Components.isSuccessCode(aStatus)) { + if (window.calendarItem.recurrenceId) { + // TODO This workaround needs to be removed in bug 396182 + // We are editing an occurrence. Make sure that the returned + // item is the same occurrence, not its parent item. + let occ = aItem.recurrenceInfo + .getOccurrenceFor(window.calendarItem.recurrenceId); + window.calendarItem = occ; + } else { + // We are editing the parent item, no workarounds needed + window.calendarItem = aItem; + } + + // We now have an item, so we must change to an edit. + window.mode = "modify"; + updateTitle(); + eventDialogCalendarObserver.observe(window.calendarItem.calendar); + } + } + // this triggers the update of the imipbar in case this is a rescheduling case + if (window.counterProposal && window.counterProposal.onReschedule) { + window.counterProposal.onReschedule(); + } + }, + onGetResult: function() {} + }; + window.onAcceptCallback(item, calendar, originalItem, listener); +} + +/** + * This function is called when the user chooses to delete an Item + * from the Event/Task dialog + * + */ +function onCommandDeleteItem() { + // only ask for confirmation, if the User changed anything on a new item or we modify an existing item + if (isItemChanged() || window.mode != "new") { + let promptTitle = ""; + let promptMessage = ""; + + if (cal.isEvent(window.calendarItem)) { + promptTitle = calGetString("calendar", "deleteEventLabel"); + promptMessage = calGetString("calendar", "deleteEventMessage"); + } else if (cal.isToDo(window.calendarItem)) { + promptTitle = calGetString("calendar", "deleteTaskLabel"); + promptMessage = calGetString("calendar", "deleteTaskMessage"); + } + + let answerDelete = Services.prompt.confirm( + null, + promptTitle, + promptMessage); + if (!answerDelete) { + return; + } + } + + if (window.mode == "new") { + cancelItem(); + } else { + let deleteListener = { + // when deletion of item is complete, close the dialog + onOperationComplete: function(aCalendar, aStatus, aOperationType, aId, aDetail) { + // Check if the current window has a calendarItem first, because in case of undo + // window refers to the main window and we would get a 'calendarItem is undefined' warning. + if ("calendarItem" in window) { + if (aId == window.calendarItem.id && Components.isSuccessCode(aStatus)) { + cancelItem(); + } else { + eventDialogCalendarObserver.observe(window.calendarItem.calendar); + } + } + } + }; + + eventDialogCalendarObserver.cancel(); + if (window.calendarItem.parentItem.recurrenceInfo && window.calendarItem.recurrenceId) { + // if this is a single occurrence of a recurring item + let newItem = window.calendarItem.parentItem.clone(); + newItem.recurrenceInfo.removeOccurrenceAt(window.calendarItem.recurrenceId); + + gMainWindow.doTransaction("modify", newItem, newItem.calendar, + window.calendarItem.parentItem, deleteListener); + } else { + gMainWindow.doTransaction("delete", window.calendarItem, window.calendarItem.calendar, + null, deleteListener); + } + } +} + +/** + * Postpone the task's start date/time and due date/time. ISO 8601 + * format: "PT1H", "P1D", and "P1W" are 1 hour, 1 day, and 1 week. (We + * use this format intentionally instead of a calIDuration object because + * those objects cannot be serialized for message passing with iframes.) + * + * @param {string} aDuration A duration in ISO 8601 format + */ +function postponeTask(aDuration) { + let duration = cal.createDuration(aDuration); + if (gStartTime != null) { + gStartTime.addDuration(duration); + } + if (gEndTime != null) { + gEndTime.addDuration(duration); + } + updateDateTime(); +} + +/** + * Prompts the user to change the start timezone. + */ +function editStartTimezone() { + editTimezone("timezone-starttime", + gStartTime.getInTimezone(gStartTimezone), + editStartTimezone.complete); +} +editStartTimezone.complete = function(datetime) { + let equalTimezones = false; + if (gStartTimezone && gEndTimezone) { + if (gStartTimezone == gEndTimezone) { + equalTimezones = true; + } + } + gStartTimezone = datetime.timezone; + if (equalTimezones) { + gEndTimezone = datetime.timezone; + } + updateDateTime(); +}; + +/** + * Prompts the user to change the end timezone. + */ +function editEndTimezone() { + editTimezone("timezone-endtime", + gEndTime.getInTimezone(gEndTimezone), + editEndTimezone.complete); +} +editEndTimezone.complete = function(datetime) { + gEndTimezone = datetime.timezone; + updateDateTime(); +}; + +/** + * Called to choose a recent timezone from the timezone popup. + * + * @param event The event with a target that holds the timezone id value. + */ +function chooseRecentTimezone(event) { + let tzid = event.target.value; + let timezonePopup = document.getElementById("timezone-popup"); + let tzProvider = getCurrentCalendar().getProperty("timezones.provider") || + cal.getTimezoneService(); + + if (tzid != "custom") { + let zone = tzProvider.getTimezone(tzid); + let datetime = timezonePopup.dateTime.getInTimezone(zone); + timezonePopup.editTimezone.complete(datetime); + } +} + +/** + * Opens the timezone popup on the node the event target points at. + * + * @param event The event causing the popup to open + * @param dateTime The datetime for which the timezone should be modified + * @param editFunc The function to be called when the custom menuitem is clicked. + */ +function showTimezonePopup(event, dateTime, editFunc) { + // Don't do anything for right/middle-clicks. Also, don't show the popup if + // the opening node is disabled. + if (event.button != 0 || event.target.disabled) { + return; + } + + let timezonePopup = document.getElementById("timezone-popup"); + let timezoneDefaultItem = document.getElementById("timezone-popup-defaulttz"); + let timezoneSeparator = document.getElementById("timezone-popup-menuseparator"); + let defaultTimezone = cal.calendarDefaultTimezone(); + let recentTimezones = cal.getRecentTimezones(true); + + // Set up the right editTimezone function, so the custom item can use it. + timezonePopup.editTimezone = editFunc; + timezonePopup.dateTime = dateTime; + + // Set up the default timezone item + timezoneDefaultItem.value = defaultTimezone.tzid; + timezoneDefaultItem.label = defaultTimezone.displayName; + + // Clear out any old recent timezones + while (timezoneDefaultItem.nextSibling != timezoneSeparator) { + timezoneDefaultItem.nextSibling.remove(); + } + + // Fill in the new recent timezones + for (let timezone of recentTimezones) { + let menuItem = createXULElement("menuitem"); + menuItem.setAttribute("value", timezone.tzid); + menuItem.setAttribute("label", timezone.displayName); + timezonePopup.insertBefore(menuItem, timezoneDefaultItem.nextSibling); + } + + // Show the popup + timezonePopup.openPopup(event.target, "after_start", 0, 0, true); +} + +/** + * Common function of edit(Start|End)Timezone() to prompt the user for a + * timezone change. + * + * @param aElementId The XUL element id of the timezone label. + * @param aDateTime The Date/Time of the time to change zone on. + * @param aCallback What to do when the user has chosen a zone. + */ +function editTimezone(aElementId, aDateTime, aCallback) { + if (document.getElementById(aElementId) + .hasAttribute("disabled")) { + return; + } + + // prepare the arguments that will be passed to the dialog + let args = {}; + args.time = aDateTime; + args.calendar = getCurrentCalendar(); + args.onOk = function(datetime) { + cal.saveRecentTimezone(datetime.timezone.tzid); + return aCallback(datetime); + }; + + // open the dialog modally + openDialog( + "chrome://calendar/content/calendar-event-dialog-timezone.xul", + "_blank", + "chrome,titlebar,modal,resizable", + args); +} + +/** + * This function initializes the following controls: + * - 'event-starttime' + * - 'event-endtime' + * - 'event-all-day' + * - 'todo-has-entrydate' + * - 'todo-entrydate' + * - 'todo-has-duedate' + * - 'todo-duedate' + * The date/time-objects are either displayed in their respective + * timezone or in the default timezone. This decision is based + * on whether or not 'cmd_timezone' is checked. + * the necessary information is taken from the following variables: + * - 'gStartTime' + * - 'gEndTime' + * - 'window.calendarItem' (used to decide about event/task) + */ +function updateDateTime() { + gIgnoreUpdate = true; + + let item = window.calendarItem; + // Convert to default timezone if the timezone option + // is *not* checked, otherwise keep the specific timezone + // and display the labels in order to modify the timezone. + if (gTimezonesEnabled) { + if (isEvent(item)) { + let startTime = gStartTime.getInTimezone(gStartTimezone); + let endTime = gEndTime.getInTimezone(gEndTimezone); + + setElementValue("event-all-day", startTime.isDate, "checked"); + + // In the case where the timezones are different but + // the timezone of the endtime is "UTC", we convert + // the endtime into the timezone of the starttime. + if (startTime && endTime) { + if (!compareObjects(startTime.timezone, endTime.timezone)) { + if (endTime.timezone.isUTC) { + endTime = endTime.getInTimezone(startTime.timezone); + } + } + } + + // before feeding the date/time value into the control we need + // to set the timezone to 'floating' in order to avoid the + // automatic conversion back into the OS timezone. + startTime.timezone = floating(); + endTime.timezone = floating(); + + setElementValue("event-starttime", cal.dateTimeToJsDate(startTime)); + setElementValue("event-endtime", cal.dateTimeToJsDate(endTime)); + } + + if (isToDo(item)) { + let startTime = gStartTime && gStartTime.getInTimezone(gStartTimezone); + let endTime = gEndTime && gEndTime.getInTimezone(gEndTimezone); + let hasEntryDate = (startTime != null); + let hasDueDate = (endTime != null); + + if (hasEntryDate && hasDueDate) { + setElementValue("todo-has-entrydate", hasEntryDate, "checked"); + startTime.timezone = floating(); + setElementValue("todo-entrydate", cal.dateTimeToJsDate(startTime)); + + setElementValue("todo-has-duedate", hasDueDate, "checked"); + endTime.timezone = floating(); + setElementValue("todo-duedate", cal.dateTimeToJsDate(endTime)); + } else if (hasEntryDate) { + setElementValue("todo-has-entrydate", hasEntryDate, "checked"); + startTime.timezone = floating(); + setElementValue("todo-entrydate", cal.dateTimeToJsDate(startTime)); + + startTime.timezone = floating(); + setElementValue("todo-duedate", cal.dateTimeToJsDate(startTime)); + } else if (hasDueDate) { + endTime.timezone = floating(); + setElementValue("todo-entrydate", cal.dateTimeToJsDate(endTime)); + + setElementValue("todo-has-duedate", hasDueDate, "checked"); + endTime.timezone = floating(); + setElementValue("todo-duedate", cal.dateTimeToJsDate(endTime)); + } else { + startTime = window.initialStartDateValue; + startTime.timezone = floating(); + endTime = startTime.clone(); + + setElementValue("todo-entrydate", cal.dateTimeToJsDate(startTime)); + setElementValue("todo-duedate", cal.dateTimeToJsDate(endTime)); + } + } + } else { + let kDefaultTimezone = calendarDefaultTimezone(); + + if (isEvent(item)) { + let startTime = gStartTime.getInTimezone(kDefaultTimezone); + let endTime = gEndTime.getInTimezone(kDefaultTimezone); + setElementValue("event-all-day", startTime.isDate, "checked"); + + // before feeding the date/time value into the control we need + // to set the timezone to 'floating' in order to avoid the + // automatic conversion back into the OS timezone. + startTime.timezone = floating(); + endTime.timezone = floating(); + setElementValue("event-starttime", cal.dateTimeToJsDate(startTime)); + setElementValue("event-endtime", cal.dateTimeToJsDate(endTime)); + } + + if (isToDo(item)) { + let startTime = gStartTime && + gStartTime.getInTimezone(kDefaultTimezone); + let endTime = gEndTime && gEndTime.getInTimezone(kDefaultTimezone); + let hasEntryDate = (startTime != null); + let hasDueDate = (endTime != null); + + if (hasEntryDate && hasDueDate) { + setElementValue("todo-has-entrydate", hasEntryDate, "checked"); + startTime.timezone = floating(); + setElementValue("todo-entrydate", cal.dateTimeToJsDate(startTime)); + + setElementValue("todo-has-duedate", hasDueDate, "checked"); + endTime.timezone = floating(); + setElementValue("todo-duedate", cal.dateTimeToJsDate(endTime)); + } else if (hasEntryDate) { + setElementValue("todo-has-entrydate", hasEntryDate, "checked"); + startTime.timezone = floating(); + setElementValue("todo-entrydate", cal.dateTimeToJsDate(startTime)); + + startTime.timezone = floating(); + setElementValue("todo-duedate", cal.dateTimeToJsDate(startTime)); + } else if (hasDueDate) { + endTime.timezone = floating(); + setElementValue("todo-entrydate", cal.dateTimeToJsDate(endTime)); + + setElementValue("todo-has-duedate", hasDueDate, "checked"); + endTime.timezone = floating(); + setElementValue("todo-duedate", cal.dateTimeToJsDate(endTime)); + } else { + startTime = window.initialStartDateValue; + startTime.timezone = floating(); + endTime = startTime.clone(); + + setElementValue("todo-entrydate", cal.dateTimeToJsDate(startTime)); + setElementValue("todo-duedate", cal.dateTimeToJsDate(endTime)); + } + } + } + + updateTimezone(); + updateAllDay(); + updateRepeatDetails(); + + gIgnoreUpdate = false; +} + +/** + * This function initializes the following controls: + * - 'timezone-starttime' + * - 'timezone-endtime' + * the timezone-links show the corrosponding names of the + * start/end times. If 'cmd_timezone' is not checked + * the links will be collapsed. + */ +function updateTimezone() { + function updateTimezoneElement(aTimezone, aId, aDateTime) { + let element = document.getElementById(aId); + if (!element) { + return; + } + + if (aTimezone) { + element.removeAttribute("collapsed"); + element.value = aTimezone.displayName || aTimezone.tzid; + if (!aDateTime || !aDateTime.isValid || gIsReadOnly || aDateTime.isDate) { + if (element.hasAttribute("class")) { + element.setAttribute("class-on-enabled", + element.getAttribute("class")); + element.removeAttribute("class"); + } + if (element.hasAttribute("onclick")) { + element.setAttribute("onclick-on-enabled", + element.getAttribute("onclick")); + element.removeAttribute("onclick"); + } + element.setAttribute("disabled", "true"); + } else { + if (element.hasAttribute("class-on-enabled")) { + element.setAttribute("class", + element.getAttribute("class-on-enabled")); + element.removeAttribute("class-on-enabled"); + } + if (element.hasAttribute("onclick-on-enabled")) { + element.setAttribute("onclick", + element.getAttribute("onclick-on-enabled")); + element.removeAttribute("onclick-on-enabled"); + } + element.removeAttribute("disabled"); + } + } else { + element.setAttribute("collapsed", "true"); + } + } + + // convert to default timezone if the timezone option + // is *not* checked, otherwise keep the specific timezone + // and display the labels in order to modify the timezone. + if (gTimezonesEnabled) { + updateTimezoneElement(gStartTimezone, + "timezone-starttime", + gStartTime); + updateTimezoneElement(gEndTimezone, + "timezone-endtime", + gEndTime); + } else { + document.getElementById("timezone-starttime") + .setAttribute("collapsed", "true"); + document.getElementById("timezone-endtime") + .setAttribute("collapsed", "true"); + } +} + +/** + * Updates dialog controls related to item attachments + */ +function updateAttachment() { + let hasAttachments = capSupported("attachments"); + if (!gNewItemUI) { + setElementValue("cmd_attach_url", !hasAttachments && "true", "disabled"); + } + sendMessage({ + command: "updateConfigState", + argument: { attachUrlCommand: hasAttachments } + }); +} + +/** + * Returns whether to show or hide the related link on the dialog + * (rfc2445 URL property). The aShow argument passed in may be overridden + * for various reasons. + * + * @param {boolean} aShow Show the link (true) or not (false) + * @param {string} aUrl The url in question + * @return {boolean} Returns true for show and false for hide + */ +function showOrHideItemURL(aShow, aUrl) { + if (aShow && aUrl.length) { + let handler; + let uri; + try { + uri = makeURL(aUrl); + handler = Services.io.getProtocolHandler(uri.scheme); + } catch (e) { + // No protocol handler for the given protocol, or invalid uri + // hideOrShow(false); + return false; + } + // Only show if its either an internal protcol handler, or its external + // and there is an external app for the scheme + handler = cal.wrapInstance(handler, Components.interfaces.nsIExternalProtocolHandler); + return !handler || handler.externalAppExistsForScheme(uri.scheme); + } else { + // Hide if there is no url, or the menuitem was chosen so that the url + // should be hidden. + return false; + } +} + +/** + * Updates the related link on the dialog (rfc2445 URL property). + * + * @param {boolean} aShow Show the link (true) or not (false) + * @param {string} aUrl The url + */ +function updateItemURL(aShow, aUrl) { + // Hide or show the link + setElementValue("event-grid-link-row", !aShow && "true", "hidden"); + // The separator is not there in the summary dialog + let separator = document.getElementById("event-grid-link-separator"); + if (separator) { + setElementValue("event-grid-link-separator", !aShow && "true", "hidden"); + } + + // Set the url for the link + if (aShow && aUrl.length) { + setTimeout(() => { + // HACK the url-link doesn't crop when setting the value in onLoad + setElementValue("url-link", aUrl); + setElementValue("url-link", aUrl, "href"); + }, 0); + } +} + + +/** + * This function updates dialog controls related to attendees. + */ +function updateAttendees() { + // sending email invitations currently only supported for events + let attendeeTab = document.getElementById("event-grid-tab-attendees"); + let attendeePanel = document.getElementById("event-grid-tabpanel-attendees"); + if (isEvent(window.calendarItem)) { + attendeeTab.removeAttribute("collapsed"); + attendeePanel.removeAttribute("collapsed"); + + if (window.organizer && window.organizer.id) { + document.getElementById("item-organizer-row").removeAttribute("collapsed"); + let cell = document.querySelector(".item-organizer-cell"); + let icon = cell.querySelector("img:nth-of-type(1)"); + let text = cell.querySelector("label:nth-of-type(1)"); + + let role = organizer.role || "REQ-PARTICIPANT"; + let userType = organizer.userType || "INDIVIDUAL"; + let partStat = organizer.participationStatus || "NEEDS-ACTION"; + + let orgName = (organizer.commonName && organizer.commonName.length) + ? organizer.commonName : organizer.toString(); + let userTypeString = cal.calGetString("calendar", "dialog.tooltip.attendeeUserType2." + userType, + [organizer.toString()]); + let roleString = cal.calGetString("calendar", "dialog.tooltip.attendeeRole2." + role, + [userTypeString]); + let partStatString = cal.calGetString("calendar", "dialog.tooltip.attendeePartStat2." + partStat, + [orgName]); + let tooltip = cal.calGetString("calendar", "dialog.tooltip.attendee.combined", + [roleString, partStatString]); + + text.setAttribute("value", orgName); + cell.setAttribute("tooltiptext", tooltip); + icon.setAttribute("partstat", partStat); + icon.setAttribute("usertype", userType); + icon.setAttribute("role", role); + } else { + setBooleanAttribute("item-organizer-row", "collapsed", true); + } + setupAttendees(); + } else { + attendeeTab.setAttribute("collapsed", "true"); + attendeePanel.setAttribute("collapsed", "true"); + } +} + +/** + * This function updates dialog controls related to recurrence, in this case the + * text describing the recurrence rule. + */ +function updateRepeatDetails() { + // Don't try to show the details text for + // anything but a custom recurrence rule. + let recurrenceInfo = window.recurrenceInfo; + let itemRepeat = document.getElementById("item-repeat"); + if (itemRepeat.value == "custom" && recurrenceInfo) { + let item = window.calendarItem; + document.getElementById("repeat-deck").selectedIndex = 1; + // First of all collapse the details text. If we fail to + // create a details string, we simply don't show anything. + // this could happen if the repeat rule is something exotic + // we don't have any strings prepared for. + let repeatDetails = document.getElementById("repeat-details"); + repeatDetails.setAttribute("collapsed", "true"); + + // Try to create a descriptive string from the rule(s). + let kDefaultTimezone = calendarDefaultTimezone(); + let event = cal.isEvent(item); + + let startDate = getElementValue(event ? "event-starttime" : "todo-entrydate"); + let endDate = getElementValue(event ? "event-endtime" : "todo-duedate"); + startDate = cal.jsDateToDateTime(startDate, kDefaultTimezone); + endDate = cal.jsDateToDateTime(endDate, kDefaultTimezone); + + let allDay = getElementValue("event-all-day", "checked"); + let detailsString = recurrenceRule2String(recurrenceInfo, startDate, + endDate, allDay); + + if (!detailsString) { + detailsString = cal.calGetString("calendar-event-dialog", "ruleTooComplex"); + } + + // Now display the string... + let lines = detailsString.split("\n"); + repeatDetails.removeAttribute("collapsed"); + while (repeatDetails.childNodes.length > lines.length) { + repeatDetails.lastChild.remove(); + } + let numChilds = repeatDetails.childNodes.length; + for (let i = 0; i < lines.length; i++) { + if (i >= numChilds) { + let newNode = repeatDetails.childNodes[0] + .cloneNode(true); + repeatDetails.appendChild(newNode); + } + repeatDetails.childNodes[i].value = lines[i]; + repeatDetails.childNodes[i].setAttribute("tooltiptext", + detailsString); + } + } else { + let repeatDetails = document.getElementById("repeat-details"); + repeatDetails.setAttribute("collapsed", "true"); + } +} + +/** + * This function does not strictly check if the given attendee has the status + * TENTATIVE, but also if he hasn't responded. + * + * @param aAttendee The attendee to check. + * @return True, if the attendee hasn't responded. + */ +function isAttendeeUndecided(aAttendee) { + return aAttendee.participationStatus != "ACCEPTED" && + aAttendee.participationStatus != "DECLINED" && + aAttendee.participationStatus != "DELEGATED"; +} + +/** + * Event handler for dblclick on attendee items. + * + * @param aEvent The popupshowing event + */ +function attendeeDblClick(aEvent) { + // left mouse button + if (aEvent.button == 0) { + editAttendees(); + } + return; +} + +/** + * Event handler to set up the attendee-popup. This builds the popup menuitems. + * + * @param aEvent The popupshowing event + */ +function attendeeClick(aEvent) { + // we need to handle right clicks only to display the context menu + if (aEvent.button != 2) { + return; + } + + if (window.attendees.length == 0) { + // we just need the option to open the attendee dialog in this case + let popup = document.getElementById("attendee-popup"); + let invite = document.getElementById("attendee-popup-invite-menuitem"); + for (let node of popup.childNodes) { + if (node == invite) { + showElement(node); + } else { + hideElement(node); + } + } + } else { + if (window.attendees.length > 1) { + let removeall = document.getElementById("attendee-popup-removeallattendees-menuitem"); + showElement(removeall); + } + let sendEmail = document.getElementById("attendee-popup-sendemail-menuitem"); + let sendTentativeEmail = document.getElementById("attendee-popup-sendtentativeemail-menuitem"); + let firstSeparator = document.getElementById("attendee-popup-first-separator"); + [sendEmail, sendTentativeEmail, firstSeparator].forEach(showElement); + + // setup attendee specific menu items if appropriate otherwise hide respective menu items + let mailto = document.getElementById("attendee-popup-emailattendee-menuitem"); + let remove = document.getElementById("attendee-popup-removeattendee-menuitem"); + let secondSeparator = document.getElementById("attendee-popup-second-separator"); + let attId = aEvent.target.parentNode.getAttribute("attendeeid"); + let attendee = window.attendees.find(aAtt => aAtt.id == attId); + if (attendee) { + [mailto, remove, secondSeparator].forEach(showElement); + mailto.setAttribute("label", attendee.toString()); + mailto.attendee = attendee; + remove.attendee = attendee; + } else { + [mailto, remove, secondSeparator].forEach(hideElement); + } + + if (window.attendees.some(isAttendeeUndecided)) { + document.getElementById("cmd_email_undecided") + .removeAttribute("disabled"); + } else { + document.getElementById("cmd_email_undecided") + .setAttribute("disabled", "true"); + } + } +} + +/** + * Removes the selected attendee from the window + * @param aAttendee + */ +function removeAttendee(aAttendee) { + if (aAttendee) { + window.attendees = window.attendees.filter(aAtt => aAtt != aAttendee); + updateAttendees(); + } +} + +/** + * Removes all attendees from the window + */ +function removeAllAttendees() { + window.attendees = []; + window.organizer = null; + updateAttendees(); +} + +/** + * Send Email to all attendees that haven't responded or are tentative. + * + * @param aAttendees The attendees to check. + */ +function sendMailToUndecidedAttendees(aAttendees) { + let targetAttendees = attendees.filter(isAttendeeUndecided); + sendMailToAttendees(targetAttendees); +} + +/** + * Send Email to all given attendees. + * + * @param aAttendees The attendees to send mail to. + */ +function sendMailToAttendees(aAttendees) { + let toList = cal.getRecipientList(aAttendees); + let item = saveItem(); + let emailSubject = cal.calGetString("calendar-event-dialog", "emailSubjectReply", [item.title]); + let identity = window.calendarItem.calendar.getProperty("imip.identity"); + sendMailTo(toList, emailSubject, null, identity); +} + +/** + * Make sure all fields that may have calendar specific capabilities are updated + */ +function updateCapabilities() { + updateAttachment(); + updateConfigState({ + priority: gConfig.priority, + privacy: gConfig.privacy + }); + updateReminderDetails(); + updateCategoryMenulist(); +} + +/** + * find out if the User already changed values in the Dialog + * + * @return: true if the values in the Dialog have changed. False otherwise. + */ +function isItemChanged() { + let newItem = saveItem(); + let oldItem = window.calendarItem.clone(); + + // we need to guide the description text through the text-field since + // newlines are getting converted which would indicate changes to the + // text. + setElementValue("item-description", oldItem.getProperty("DESCRIPTION")); + setItemProperty(oldItem, + "DESCRIPTION", + getElementValue("item-description")); + setElementValue("item-description", newItem.getProperty("DESCRIPTION")); + + if ((newItem.calendar.id == oldItem.calendar.id) && + compareItemContent(newItem, oldItem)) { + return false; + } + return true; +} + +/** + * Test if a specific capability is supported + * + * @param aCap The capability from "capabilities.<aCap>.supported" + */ +function capSupported(aCap) { + let calendar = getCurrentCalendar(); + return calendar.getProperty("capabilities." + aCap + ".supported") !== false; +} + +/** + * Return the values for a certain capability. + * + * @param aCap The capability from "capabilities.<aCap>.values" + * @return The values for this capability + */ +function capValues(aCap, aDefault) { + let calendar = getCurrentCalendar(); + let vals = calendar.getProperty("capabilities." + aCap + ".values"); + return (vals === null ? aDefault : vals); +} + +/** + * Checks the until date just entered in the datepicker in order to avoid + * setting a date earlier than the start date. + * Restores the previous correct date; sets the warning flag to prevent closing + * the dialog when the user enters a wrong until date. + */ +function checkUntilDate() { + let repeatUntilDate = getElementValue("repeat-until-datepicker"); + if (repeatUntilDate == "forever") { + updateRepeat(); + // "forever" is never earlier than another date. + return; + } + + // Check whether the date is valid. Set the correct time just in this case. + let untilDate = cal.jsDateToDateTime(repeatUntilDate, gStartTime.timezone); + let startDate = gStartTime.clone(); + startDate.isDate = true; + if (untilDate.compare(startDate) < 0) { + // Invalid date: restore the previous date. Since we are checking an + // until date, a null value for gUntilDate means repeat "forever". + setElementValue("repeat-until-datepicker", + gUntilDate ? cal.dateTimeToJsDate(gUntilDate.getInTimezone(cal.floating())) + : "forever"); + gWarning = true; + let callback = function() { + // Disable the "Save" and "Save and Close" commands as long as the + // warning dialog is showed. + enableAcceptCommand(false); + + Services.prompt.alert( + null, + document.title, + cal.calGetString("calendar", "warningUntilDateBeforeStart")); + enableAcceptCommand(true); + gWarning = false; + }; + setTimeout(callback, 1); + } else { + // Valid date: set the time equal to start date time. + gUntilDate = untilDate; + updateUntildateRecRule(); + } +} + +/** + * Displays a counterproposal if any + */ +function displayCounterProposal() { + if (!window.counterProposal || !window.counterProposal.attendee || + !window.counterProposal.proposal) { + return; + } + + let propLabels = document.getElementById("counter-proposal-property-labels"); + let propValues = document.getElementById("counter-proposal-property-values"); + let idCounter = 0; + let comment; + + for (let proposal of window.counterProposal.proposal) { + if (proposal.property == "COMMENT") { + if (proposal.proposed && !proposal.original) { + comment = proposal.proposed; + } + } else { + let label = lookupCounterLabel(proposal); + let value = formatCounterValue(proposal); + if (label && value) { + // setup label node + let propLabel = propLabels.firstChild.cloneNode(false); + propLabel.id = propLabel.id + "-" + idCounter; + propLabel.control = propLabel.control + "-" + idCounter; + propLabel.removeAttribute("collapsed"); + propLabel.value = label; + // setup value node + let propValue = propValues.firstChild.cloneNode(false); + propValue.id = propLabel.control; + propValue.removeAttribute("collapsed"); + propValue.value = value; + // append nodes + propLabels.appendChild(propLabel); + propValues.appendChild(propValue); + idCounter++; + } + } + } + + let attendeeId = window.counterProposal.attendee.CN || + cal.removeMailTo(window.counterProposal.attendee.id || ""); + let partStat = window.counterProposal.attendee.participationStatus; + if (partStat == "DECLINED") { + partStat = "counterSummaryDeclined"; + } else if (partStat == "TENTATIVE") { + partStat = "counterSummaryTentative"; + } else if (partStat == "ACCEPTED") { + partStat = "counterSummaryAccepted"; + } else if (partStat == "DELEGATED") { + partStat = "counterSummaryDelegated"; + } else if (partStat == "NEEDS-ACTION") { + partStat = "counterSummaryNeedsAction"; + } else { + cal.LOG("Unexpected partstat " + partStat + " detected."); + // we simply reset partStat not display the summary text of the counter box + // to avoid the window of death + partStat = null; + } + + if (idCounter > 0) { + if (partStat && attendeeId.length) { + document.getElementById("counter-proposal-summary").value = cal.calGetString( + "calendar-event-dialog", + partStat, + [attendeeId] + ); + document.getElementById("counter-proposal-summary").removeAttribute("collapsed"); + } + if (comment) { + document.getElementById("counter-proposal-comment").value = comment; + document.getElementById("counter-proposal-box").removeAttribute("collapsed"); + } + document.getElementById("counter-proposal-box").removeAttribute("collapsed"); + + if (window.counterProposal.oldVersion) { + // this is a counterproposal to a previous version of the event - we should notify the + // user accordingly + notifyUser( + "counterProposalOnPreviousVersion", + cal.calGetString("calendar-event-dialog", "counterOnPreviousVersionNotification"), + "warn" + ); + } + if (window.calendarItem.getProperty("X-MICROSOFT-DISALLOW-COUNTER") == "TRUE") { + // this is a counterproposal although the user disallowed countering when sending the + // invitation, so we notify the user accordingly + notifyUser( + "counterProposalOnCounteringDisallowed", + cal.calGetString("calendar-event-dialog", "counterOnCounterDisallowedNotification"), + "warn" + ); + } + } +} + +/** + * Get the property label to display for a counterproposal based on the respective label used in + * the dialog + * + * @param {JSObject} aProperty The property to check for a label + * @returns {String|null} The label to display or null if no such label + */ +function lookupCounterLabel(aProperty) { + let nodeIds = getPropertyMap(); + let labels = nodeIds.has(aProperty.property) && + document.getElementsByAttribute("control", nodeIds.get(aProperty.property)); + let labelValue; + if (labels && labels.length) { + // as label control assignment should be unique, we can just take the first result + labelValue = labels[0].value; + } else { + cal.LOG("Unsupported property " + aProperty.property + " detected when setting up counter " + + "box labels."); + } + return labelValue; +} + +/** + * Get the property value to display for a counterproposal as currently supported + * + * @param {JSObject} aProperty The property to check for a label + * @returns {String|null} The value to display or null if the property is not supported + */ +function formatCounterValue(aProperty) { + const dateProps = ["DTSTART", "DTEND"]; + const stringProps = ["SUMMARY", "LOCATION"]; + + let val; + if (dateProps.includes(aProperty.property)) { + let localTime = aProperty.proposed.getInTimezone(cal.calendarDefaultTimezone()); + let formatter = getDateFormatter(); + val = formatter.formatDateTime(localTime); + if (gTimezonesEnabled) { + let tzone = localTime.timezone.displayName || localTime.timezone.tzid; + val += " " + tzone; + } + } else if (stringProps.includes(aProperty.property)) { + val = aProperty.proposed; + } else { + cal.LOG("Unsupported property " + aProperty.property + + " detected when setting up counter box values."); + } + return val; +} + +/** + * Get a map of porperty names and labels of currently supported properties + * + * @returns {Map} + */ +function getPropertyMap() { + let map = new Map(); + map.set("SUMMARY", "item-title"); + map.set("LOCATION", "item-location"); + map.set("DTSTART", "event-starttime"); + map.set("DTEND", "event-endtime"); + return map; +} + +/** + * Applies the proposal or original data to the respective dialog fields + * + * @param {String} aType Either 'proposed' or 'original' + */ +function applyValues(aType) { + if (!window.counterProposal || (aType != "proposed" && aType != "original")) { + return; + } + let originalBtn = document.getElementById("counter-original-btn"); + if (originalBtn.disabled) { + // The button is disbled when opening the dialog/tab, which makes it more obvious to the + // user that he/she needs to apply the proposal values prior to saving & sending. + // Once that happened, we leave both options to the user without toogling the button states + // to avoid needing to listen to manual changes to do that correctly + originalBtn.removeAttribute("disabled"); + } + let nodeIds = getPropertyMap(); + window.counterProposal.proposal.forEach(aProperty => { + if (aProperty.property != "COMMENT") { + let valueNode = nodeIds.has(aProperty.property) && + document.getElementById(nodeIds.get(aProperty.property)); + if (valueNode) { + if (["DTSTART", "DTEND"].includes(aProperty.property)) { + valueNode.value = cal.dateTimeToJsDate(aProperty[aType]); + } else { + valueNode.value = aProperty[aType]; + } + } + } + }); +} diff --git a/calendar/lightning/content/lightning-item-iframe.xul b/calendar/lightning/content/lightning-item-iframe.xul new file mode 100644 index 000000000..b18fc425a --- /dev/null +++ b/calendar/lightning/content/lightning-item-iframe.xul @@ -0,0 +1,740 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- 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/. --> + +<!-- XXX some of these css files may not be needed here. + widget-bindings.css definitely is needed here --> +<?xml-stylesheet type="text/css" href="chrome://global/skin/global.css"?> +<?xml-stylesheet type="text/css" href="chrome://calendar-common/skin/calendar-alarms.css"?> +<?xml-stylesheet type="text/css" href="chrome://calendar-common/skin/calendar-attendees.css"?> +<?xml-stylesheet type="text/css" href="chrome://calendar/content/widgets/calendar-widget-bindings.css"?> +<?xml-stylesheet type="text/css" href="chrome://calendar/skin/calendar-event-dialog.css"?> +<?xml-stylesheet type="text/css" href="chrome://calendar/content/calendar-event-dialog.css"?> +<?xml-stylesheet type="text/css" href="chrome://calendar/content/datetimepickers/datetimepickers.css"?> +<?xml-stylesheet type="text/css" href="chrome://messenger/skin/primaryToolbar.css"?> +<?xml-stylesheet type="text/css" href="chrome://messenger/skin/messenger.css"?> + + +<!DOCTYPE window [ + <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> + <!ENTITY % globalDTD SYSTEM "chrome://calendar/locale/global.dtd"> + <!ENTITY % calendarDTD SYSTEM "chrome://calendar/locale/calendar.dtd"> + <!ENTITY % eventDialogDTD SYSTEM "chrome://calendar/locale/calendar-event-dialog.dtd"> + %brandDTD; + %globalDTD; + %calendarDTD; + %eventDialogDTD; +]> + +<!-- Vbox id is changed during excution to allow different treatment. + document.loadOverlay() will not work on this one. --> +<window id="calendar-event-dialog-inner" + onload="onLoad();" + onunload="onEventDialogUnload();" + onresize="rearrangeAttendees();" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <!-- Javascript includes --> + <script type="application/javascript" + src="chrome://lightning/content/lightning-item-iframe.js"/> + <script type="application/javascript" + src="chrome://calendar/content/calendar-dialog-utils.js"/> + <script type="application/javascript" + src="chrome://calendar/content/calendar-ui-utils.js"/> + <script type="application/javascript" + src="chrome://calendar/content/calUtils.js"/> + <script type="application/javascript" + src="chrome://calendar/content/calApplicationUtils.js"/> + <script type="application/javascript" + src="chrome://global/content/globalOverlay.js"/> + <script type="application/javascript" + src="chrome://global/content/printUtils.js"/> + <script type="application/javascript" + src="chrome://calendar/content/calendar-statusbar.js"/> + + <commandset id=""> + <command id="cmd_recurrence" + oncommand="editRepeat();"/> + <command id="cmd_attendees" + oncommand="editAttendees();"/> + <command id="cmd_email" + oncommand="sendMailToAttendees(window.attendees);"/> + <command id="cmd_email_undecided" + oncommand="sendMailToUndecidedAttendees(window.attendees);"/> + <command id="cmd_attach_url" + disable-on-readonly="true" + oncommand="attachURL()"/> + <command id="cmd_attach_cloud" + disable-on-readonly="true"/> + <command id="cmd_openAttachment" + oncommand="openAttachment()"/> + <command id="cmd_copyAttachment" + oncommand="copyAttachment()"/> + <command id="cmd_deleteAttachment" + disable-on-readonly="true" + oncommand="deleteAttachment()"/> + <command id="cmd_deleteAllAttachments" + disable-on-readonly="true" + oncommand="deleteAllAttachments()"/> + <command id="cmd_applyProposal" + disable-on-readonly="true" + oncommand="applyValues('proposed')"/> + <command id="cmd_applyOriginal" + disable-on-readonly="true" + oncommand="applyValues('original')"/> + </commandset> + + <!-- Counter information section --> + <hbox id="counter-proposal-box" + collapsed="true"> + <vbox> + <description id="counter-proposal-summary" + collapsed="true" + crop="end" /> + <hbox id="counter-proposal"> + <vbox id="counter-proposal-property-labels"> + <label id="counter-proposal-property-label" + control="counter-proposal-property-value" + collapsed="true" + value="" /> + </vbox> + <vbox id="counter-proposal-property-values"> + <description id="counter-proposal-property-value" + crop="end" + collapsed="true" + value="" /> + </vbox> + </hbox> + <description id="counter-proposal-comment" + collapsed="true" + crop="end" /> + </vbox> + <spacer flex="1" /> + <vbox id ="counter-buttons"> + <button id="counter-proposal-btn" + label="&counter.button.proposal.label;" + crop="end" + command="cmd_applyProposal" + orient="horizontal" + class="counter-buttons" + accesskey="&counter.button.proposal.accesskey;" + tooltip="&counter.button.proposal.tooltip2;" /> + <button id="counter-original-btn" + label="&counter.button.original.label;" + crop="end" + command="cmd_applyOriginal" + orient="horizontal" + disabled="true" + class="counter-buttons" + accesskey="&counter.button.original.accesskey;" + tooltip="&counter.button.original.tooltip2;" /> + </vbox> + </hbox> + + <notificationbox id="event-dialog-notifications" notificationside="top"/> + + <grid id="event-grid" + flex="1" + style="padding-top: 8px; padding-bottom: 10px; padding-inline-start: 8px; padding-inline-end: 10px;"> + <columns id="event-grid-columns"> + <column id="event-description-column"/> + <column id="event-controls-column" flex="1"/> + </columns> + + <rows id="event-grid-rows"> + <!-- Title --> + <row id="event-grid-title-row" + align="center"> + <label value="&event.title.textbox.label;" + accesskey="&event.title.textbox.accesskey;" + control="item-title" + disable-on-readonly="true"/> + <textbox id="item-title" + disable-on-readonly="true" + flex="1" + oninput="updateTitle()"/> + </row> + + <!-- Location --> + <row id="event-grid-location-row" + align="center"> + <label value="&event.location.label;" + accesskey="&event.location.accesskey;" + control="item-location" + disable-on-readonly="true"/> + <textbox id="item-location" + disable-on-readonly="true"/> + </row> + + <!-- Category & Calendar --> + <row id="event-grid-category-color-row" + align="center"> + <hbox id="event-grid-category-labels-box"> + <label value="&event.categories.label;" + accesskey="&event.categories.accesskey;" + control="item-categories" + id="item-categories-label" + disable-on-readonly="true"/> + <label value="&event.calendar.label;" + accesskey="&event.calendar.accesskey;" + id="item-calendar-aux-label" + control="item-calendar" + disable-on-readonly="true"/> + </hbox> + <hbox id="event-grid-category-box" align="center"> + <menulist id="item-categories" + type="panel-menulist" + disable-on-readonly="true" + flex="1"> + <panel id="item-categories-panel" + type="category-panel" + onpopuphiding="updateCategoryMenulist()"/> + </menulist> + <label value="&event.calendar.label;" + accesskey="&event.calendar.accesskey;" + control="item-calendar" + id="item-calendar-label" + disable-on-readonly="true"/> + <menulist id="item-calendar" + disable-on-readonly="true" + flex="1" + oncommand="updateCalendar();"/> + </hbox> + </row> + + <separator class="groove" id="event-grid-basic-separator"/> + + <!-- All-Day --> + <row id="event-grid-allday-row" + align="center"> + <spacer/> + <checkbox id="event-all-day" + class="event-only" + disable-on-readonly="true" + label="&event.alldayevent.label;" + accesskey="&event.alldayevent.accesskey;" + oncommand="onUpdateAllDay();"/> + </row> + + <!-- StartDate --> + <row id="event-grid-startdate-row"> + <hbox id="event-grid-startdate-label-box" + align="center"> + <label value="&event.from.label;" + accesskey="&event.from.accesskey;" + control="event-starttime" + class="event-only" + disable-on-readonly="true"/> + <label value="&task.from.label;" + accesskey="&task.from.accesskey;" + control="todo-has-entrydate" + class="todo-only" + disable-on-readonly="true"/> + </hbox> + <hbox id="event-grid-startdate-picker-box"> + <datetimepicker id="event-starttime" + class="event-only" + disable-on-readonly="true" + onchange="dateTimeControls2State(true);"/> + <checkbox id="todo-has-entrydate" + class="todo-only checkbox-no-label" + disable-on-readonly="true" + oncommand="updateEntryDate();"/> + <datetimepicker id="todo-entrydate" + class="todo-only" + disable-on-readonly="true" + onchange="dateTimeControls2State(true);"/> + <vbox> + <hbox> + <image id="link-image-top" class="keepduration-link-image" keep="true"/> + </hbox> + <spacer flex="1"/> + <toolbarbutton id="keepduration-button" + accesskey="&event.dialog.keepDurationButton.accesskey;" + oncommand="toggleKeepDuration();" + persist="keep" + keep="false" + tooltiptext="&event.dialog.keepDurationButton.tooltip;"/> + </vbox> + <hbox align="center"> + <label id="timezone-starttime" + class="text-link" + collapsed="true" + crop="end" + disable-on-readonly="true" + flex="1" + hyperlink="true" + onclick="showTimezonePopup(event, gStartTime.getInTimezone(gStartTimezone), editStartTimezone)"/> + </hbox> + </hbox> + </row> + + <!-- EndDate --> + <row id="event-grid-enddate-row"> + <hbox id="event-grid-enddate-label-box" + align="center"> + <label value="&event.to.label;" + accesskey="&event.to.accesskey;" + control="event-endtime" + class="event-only" + disable-on-readonly="true"/> + <label value="&task.to.label;" + accesskey="&task.to.accesskey;" + control="todo-has-duedate" + class="todo-only" + disable-on-readonly="true"/> + </hbox> + <vbox> + <hbox id="event-grid-enddate-picker-box"> + <datetimepicker id="event-endtime" + class="event-only" + disable-on-readonly="true" + onchange="dateTimeControls2State(false);"/> + <checkbox id="todo-has-duedate" + class="todo-only checkbox-no-label" + disable-on-readonly="true" + oncommand="updateDueDate();"/> + <datetimepicker id="todo-duedate" + class="todo-only" + disable-on-readonly="true" + onchange="dateTimeControls2State(false);"/> + <vbox pack="end"> + <image id="link-image-bottom" class="keepduration-link-image"/> + </vbox> + <hbox align="center"> + <label id="timezone-endtime" + class="text-link" + collapsed="true" + crop="end" + disable-on-readonly="true" + flex="1" + hyperlink="true" + onclick="showTimezonePopup(event, gEndTime.getInTimezone(gEndTimezone), editEndTimezone)"/> + </hbox> + </hbox> + </vbox> + </row> + + <row id="event-grid-todo-status-row" + class="todo-only" + align="center"> + <label id="todo-status-label" + value="&task.status.label;" + accesskey="&task.status.accesskey;" + control="todo-status" + disable-on-readonly="true"/> + <hbox id="event-grid-todo-status-picker-box" + align="center"> + <menulist id="todo-status" + class="todo-only" + disable-on-readonly="true" + oncommand="updateToDoStatus(this.value);"> + <menupopup id="todo-status-menupopup"> + <menuitem id="todo-status-none-menuitem" + label="&newevent.todoStatus.none.label;" + value="NONE"/> + <menuitem id="todo-status-needsaction-menuitem" + label="&newevent.status.needsaction.label;" + value="NEEDS-ACTION"/> + <menuitem id="todo-status-inprogress-menuitem" + label="&newevent.status.inprogress.label;" + value="IN-PROCESS"/> + <menuitem id="todo-status-completed-menuitem" + label="&newevent.status.completed.label;" + value="COMPLETED"/> + <menuitem id="todo-status-canceled-menuitem" + label="&newevent.todoStatus.cancelled.label;" + value="CANCELLED"/> + </menupopup> + </menulist> + <datepicker id="completed-date-picker" + class="todo-only" + disable-on-readonly="true" + disabled="true" + value=""/> + <textbox id="percent-complete-textbox" + type="number" + min="0" + max="100" + disable-on-readonly="true" + size="3" + oninput="updateToDoStatus('percent-changed')" + onselect="updateToDoStatus('percent-changed')"/> + <label id="percent-complete-label" + class="todo-only" + disable-on-readonly="true" + value="&newtodo.percentcomplete.label;"/> + </hbox> + </row> + + <separator id="event-grid-recurrence-separator" class="groove"/> + + <!-- Recurrence --> + <row id="event-grid-recurrence-row" + align="center"> + <label value="&event.repeat.label;" + accesskey="&event.repeat.accesskey;" + control="item-repeat" + disable-on-readonly="true"/> + <hbox id="event-grid-recurrence-picker-box" + align="center" + flex="1"> + <menulist id="item-repeat" + disable-on-readonly="true" + oncommand="updateRepeat(null, true)"> + <menupopup id="item-repeat-menupopup"> + <menuitem id="repeat-none-menuitem" + label="&event.repeat.does.not.repeat.label;" + selected="true" + value="none"/> + <menuitem id="repeat-daily-menuitem" + label="&event.repeat.daily.label;" + value="daily"/> + <menuitem id="repeat-weekly-menuitem" + label="&event.repeat.weekly.label;" + value="weekly"/> + <menuitem id="repeat-weekday-menuitem" + label="&event.repeat.every.weekday.label;" + value="every.weekday"/> + <menuitem id="repeat-biweekly-menuitem" + label="&event.repeat.bi.weekly.label;" + value="bi.weekly"/> + <menuitem id="repeat-monthly-menuitem" + label="&event.repeat.monthly.label;" + value="monthly"/> + <menuitem id="repeat-yearly-menuitem" + label="&event.repeat.yearly.label;" + value="yearly"/> + <menuseparator id="item-repeat-separator"/> + <menuitem id="repeat-custom-menuitem" + label="&event.repeat.custom.label;" + value="custom"/> + </menupopup> + </menulist> + <deck id="repeat-deck" selectedIndex="-1"> + <hbox id="repeat-untilDate" align="center"> + <label value="&event.until.label;" + accesskey="&event.until.accesskey;" + control="repeat-until-datepicker" + disable-on-readonly="true"/> + <datepicker-forever id="repeat-until-datepicker" flex="1" + disable-on-readonly="true" + onchange="checkUntilDate();" + oncommand="checkUntilDate();" + value=""/> + </hbox> + <vbox id="repeat-details" flex="1"> + <label class="text-link" + crop="right" + disable-on-readonly="true" + hyperlink="true" + flex="1" + onclick="updateRepeat()"/> + </vbox> + </deck> + </hbox> + </row> + + <separator id="event-grid-alarm-separator" + class="groove"/> + + <!-- Reminder (Alarm) --> + <row id="event-grid-alarm-row" + align="center"> + <label value="&event.reminder.label;" + accesskey="&event.reminder.accesskey;" + control="item-alarm" + disable-on-readonly="true"/> + <hbox id="event-grid-alarm-picker-box" + align="center"> + <menulist id="item-alarm" + disable-on-readonly="true" + oncommand="updateReminder()"> + <menupopup id="item-alarm-menupopup"> + <menuitem id="reminder-none-menuitem" + label="&event.reminder.none.label;" + selected="true" + value="none"/> + <menuseparator id="reminder-none-separator"/> + <menuitem id="reminder-0minutes-menuitem" + label="&event.reminder.0minutes.before.label;" + length="0" + origin="before" + relation="START" + unit="minutes"/> + <menuitem id="reminder-5minutes-menuitem" + label="&event.reminder.5minutes.before.label;" + length="5" + origin="before" + relation="START" + unit="minutes"/> + <menuitem id="reminder-15minutes-menuitem" + label="&event.reminder.15minutes.before.label;" + length="15" + origin="before" + relation="START" + unit="minutes"/> + <menuitem id="reminder-30minutes-menuitem" + label="&event.reminder.30minutes.before.label;" + length="30" + origin="before" + relation="START" + unit="minutes"/> + <menuseparator id="reminder-minutes-separator"/> + <menuitem id="reminder-1hour-menuitem" + label="&event.reminder.1hour.before.label;" + length="1" + origin="before" + relation="START" + unit="hours"/> + <menuitem id="reminder-2hours-menuitem" + label="&event.reminder.2hours.before.label;" + length="2" + origin="before" + relation="START" + unit="hours"/> + <menuitem id="reminder-12hours-menuitem" + label="&event.reminder.12hours.before.label;" + length="12" + origin="before" + relation="START" + unit="hours"/> + <menuseparator id="reminder-hours-separator"/> + <menuitem id="reminder-1day-menuitem" + label="&event.reminder.1day.before.label;" + length="1" + origin="before" + relation="START" + unit="days"/> + <menuitem id="reminder-2days-menuitem" + label="&event.reminder.2days.before.label;" + length="2" + origin="before" + relation="START" + unit="days"/> + <menuitem id="reminder-1week-menuitem" + label="&event.reminder.1week.before.label;" + length="7" + origin="before" + relation="START" + unit="days"/> + <menuseparator id="reminder-custom-separator"/> + <menuitem id="reminder-custom-menuitem" + label="&event.reminder.custom.label;" + value="custom"/> + </menupopup> + </menulist> + <hbox id="reminder-details"> + <hbox id="reminder-icon-box" + class="alarm-icons-box" + align="center"/> + <!-- TODO oncommand? onkeypress? --> + <label id="reminder-multiple-alarms-label" + hidden="true" + value="&event.reminder.multiple.label;" + class="text-link" + disable-on-readonly="true" + flex="1" + hyperlink="true" + onclick="updateReminder()"/> + <label id="reminder-single-alarms-label" + hidden="true" + class="text-link" + disable-on-readonly="true" + flex="1" + hyperlink="true" + onclick="updateReminder()"/> + </hbox> + </hbox> + </row> + + <separator id="event-grid-tabbox-separator" + class="groove"/> + + <!-- Multi purpose tab box --> + <tabbox id="event-grid-tabbox" + selectedIndex="0" + flex="1"> + <tabs id="event-grid-tabs"> + <tab id="event-grid-tab-description" + label="&event.description.label;" + accesskey="&event.description.accesskey;"/> + <tab id="event-grid-tab-attachments" + label="&event.attachments.label;" + accesskey="&event.attachments.accesskey;"/> + <tab id="event-grid-tab-attendees" + label="&event.attendees.label;" + accesskey="&event.attendees.accesskey;" + collapsed="true"/> + </tabs> + <tabpanels id="event-grid-tabpanels" + flex="1"> + <tabpanel id="event-grid-tabpanel-description"> + <textbox id="item-description" + disable-on-readonly="true" + flex="1" + multiline="true" + rows="12"/> + </tabpanel> + <tabpanel id="event-grid-tabpanel-attachements"> + <vbox flex="1"> + <listbox id="attachment-link" + context="attachment-popup" + rows="3" + flex="1" + disable-on-readonly="true" + onkeypress="attachmentLinkKeyPress(event)" + onclick="attachmentClick(event);" + ondblclick="attachmentDblClick(event);"/> + </vbox> + </tabpanel> + <tabpanel id="event-grid-tabpanel-attendees" + collapsed="true"> + <vbox flex="1"> + <hbox id="item-organizer-row" + collapsed="true" + align="top" + class="item-attendees-row"> + <label value="&read.only.organizer.label;"/> + <hbox class="item-organizer-cell"> + <img class="itip-icon"/> + <label id="item-organizer" + class="item-attendees-cell-label" + crop="right"/> + </hbox> + </hbox> + <hbox flex="1"> + <vbox id="item-attendees-box" + dialog-type="event" + flex="1" + context="attendee-popup" + onclick="attendeeClick(event)" + disable-on-readonly="true"/> + </hbox> + <hbox id="notify-options" align="center"> + <checkbox id="notify-attendees-checkbox" + label="&event.attendees.notify.label;" + accesskey="&event.attendees.notify.accesskey;" + oncommand="changeUndiscloseCheckboxStatus();" + pack="start"/> + <checkbox id="undisclose-attendees-checkbox" + label="&event.attendees.notifyundisclosed.label;" + accesskey="&event.attendees.notifyundisclosed.accesskey;" + tooltiptext="&event.attendees.notifyundisclosed.tooltip;" + pack="start"/> + <checkbox id="disallow-counter-checkbox" + label="&event.attendees.disallowcounter.label;" + accesskey="&event.attendees.disallowcounter.accesskey;" + tooltiptext="&event.attendees.disallowcounter.tooltip;" + pack="start"/> + </hbox> + </vbox> + </tabpanel> + </tabpanels> + </tabbox> + + <separator id="event-grid-link-separator" + class="groove" + hidden="true"/> + <row id="event-grid-link-row" + align="center" + hidden="true"> + <label value="&event.url.label;" + control="url-link"/> + <label id="url-link" + onclick="launchBrowser(this.getAttribute('href'), event)" + oncommand="launchBrowser(this.getAttribute('href'), event)" + class="text-link" + crop="end"/> + </row> + </rows> + </grid> + + + <popupset id="event-dialog-popupset"> + <menupopup id="attendee-popup"> + <menuitem id="attendee-popup-invite-menuitem" + label="&event.invite.attendees.label;" + accesskey="&event.invite.attendees.accesskey;" + command="cmd_attendees" + disable-on-readonly="true"/> + <menuitem id="attendee-popup-removeallattendees-menuitem" + label="&event.remove.attendees.label2;" + accesskey="&event.remove.attendees.accesskey;" + oncommand="removeAllAttendees()" + disable-on-readonly="true" + crop="end"/> + <menuitem id="attendee-popup-removeattendee-menuitem" + label="&event.remove.attendee.label;" + accesskey="&event.remove.attendee.accesskey;" + oncommand="removeAttendee(event.target.attendee)" + crop="end"/> + <menuseparator id="attendee-popup-first-separator"/> + <menuitem id="attendee-popup-sendemail-menuitem" + label="&event.email.attendees.label;" + accesskey="&event.email.attendees.accesskey;" + command="cmd_email"/> + <menuitem id="attendee-popup-sendtentativeemail-menuitem" + label="&event.email.tentative.attendees.label;" + accesskey="&event.email.tentative.attendees.accesskey;" + command="cmd_email_undecided"/> + <menuseparator id="attendee-popup-second-separator"/> + <menuitem id="attendee-popup-emailattendee-menuitem" + oncommand="sendMailToAttendees([event.target.attendee])" + crop="end"/> + </menupopup> + <menupopup id="attachment-popup"> + <menuitem id="attachment-popup-open" + label="&event.attachments.popup.open.label;" + accesskey="&event.attachments.popup.open.accesskey;" + command="cmd_openAttachment"/> + <menuitem id="attachment-popup-copy" + label="&calendar.copylink.label;" + accesskey="&calendar.copylink.accesskey;" + command="cmd_copyAttachment"/> + <menuitem id="attachment-popup-delete" + label="&event.attachments.popup.remove.label;" + accesskey="&event.attachments.popup.remove.accesskey;" + command="cmd_deleteAttachment"/> + <menuitem id="attachment-popup-deleteAll" + label="&event.attachments.popup.removeAll.label;" + accesskey="&event.attachments.popup.removeAll.accesskey;" + command="cmd_deleteAllAttachments"/> + <menuseparator/> + <menuitem id="attachment-popup-attachPage" + label="&event.attachments.popup.attachPage.label;" + accesskey="&event.attachments.popup.attachPage.accesskey;" + command="cmd_attach_url"/> + </menupopup> + <menupopup id="timezone-popup" + position="after_start" + oncommand="chooseRecentTimezone(event)"> + <menuitem id="timezone-popup-defaulttz"/> + <menuseparator id="timezone-popup-menuseparator"/> + <menuitem id="timezone-custom-menuitem" + label="&event.timezone.custom.label;" + value="custom" + oncommand="this.parentNode.editTimezone()"/> + </menupopup> + </popupset> + + <!-- attendee box template --> + <vbox id="item-attendees-box-template" + hidden="true"> + <hbox flex="1" class="item-attendees-row" equalsize="always" hidden="true"> + <box class="item-attendees-cell" + hidden="true" + flex="1" + context="attendee-popup" + ondblclick="attendeeDblClick(event)" + onclick="attendeeClick(event)"> + <img class="itip-icon"/> + <label class="item-attendees-cell-label" + crop="end" + flex="1"/> + </box> + <box hidden="true" flex="1"/> + </hbox> + </vbox> +</window> diff --git a/calendar/lightning/content/lightning-item-panel.js b/calendar/lightning/content/lightning-item-panel.js new file mode 100644 index 000000000..e98d4318a --- /dev/null +++ b/calendar/lightning/content/lightning-item-panel.js @@ -0,0 +1,1096 @@ +/* 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/. */ + +/* exported onLoadLightningItemPanel, onAccept, onCancel, onCommandSave, + * onCommandDeleteItem, editAttendees, rotatePrivacy, editPrivacy, + * rotatePriority, editPriority, rotateStatus, editStatus, + * rotateShowTimeAs, editShowTimeAs, updateShowTimeAs, editToDoStatus, + * postponeTask, toggleTimezoneLinks, toggleLink, attachURL, + * onCommandViewToolbar, onCommandCustomize, attachFileByAccountKey, + */ + +// XXX Need to determine which of these we really need here. +Components.utils.import("resource://calendar/modules/calUtils.jsm"); +Components.utils.import("resource://gre/modules/Services.jsm"); +Components.utils.import("resource://calendar/modules/calRecurrenceUtils.jsm"); +Components.utils.import("resource:///modules/mailServices.js"); +Components.utils.import("resource://gre/modules/PluralForm.jsm"); +Components.utils.import("resource://gre/modules/Preferences.jsm"); +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); + +try { + Components.utils.import("resource:///modules/cloudFileAccounts.js"); +} catch (e) { + // This will fail on Seamonkey, but thats ok since the pref for cloudfiles + // is false, which means the UI will not be shown +} + +// gTabmail is null if we are in a dialog window and not in a tab. +var gTabmail = document.getElementById("tabmail") || null; + +if (!gTabmail) { + // In a dialog window the following menu item functions need to be + // defined. In a tab they are defined elsewhere. To prevent errors in + // the log they are defined here (before the onLoad function is called). + /** + * Update menu items that rely on focus. + */ + window.goUpdateGlobalEditMenuItems = () => { + goUpdateCommand("cmd_undo"); + goUpdateCommand("cmd_redo"); + goUpdateCommand("cmd_cut"); + goUpdateCommand("cmd_copy"); + goUpdateCommand("cmd_paste"); + goUpdateCommand("cmd_selectAll"); + }; + /** + * Update menu items that rely on the current selection. + */ + window.goUpdateSelectEditMenuItems = () => { + goUpdateCommand("cmd_cut"); + goUpdateCommand("cmd_copy"); + goUpdateCommand("cmd_delete"); + goUpdateCommand("cmd_selectAll"); + }; + /** + * Update menu items that relate to undo/redo. + */ + window.goUpdateUndoEditMenuItems = () => { + goUpdateCommand("cmd_undo"); + goUpdateCommand("cmd_redo"); + }; + /** + * Update menu items that depend on clipboard contents. + */ + window.goUpdatePasteMenuItems = () => { + goUpdateCommand("cmd_paste"); + }; +} + +// Stores the ids of the iframes of currently open event/task tabs, used +// when window is closed to prompt for saving changes. +var gItemTabIds = []; +var gItemTabIdsCopy; + +// gConfig is used when switching tabs to restore the state of +// toolbar, statusbar, and menubar for the current tab. +var gConfig = { + isEvent: null, + privacy: null, + hasPrivacy: null, + calendarType: null, + privacyValues: null, + priority: null, + hasPriority: null, + status: null, + percentComplete: null, + showTimeAs: null, + // whether commands are enabled or disabled + attendeesCommand: null, // cmd_attendees + attachUrlCommand: null, // cmd_attach_url + timezonesEnabled: false, // cmd_timezone + // XXX Currently there is no toolbar button or menu item for + // cmd_toggle_link for event/task tabs + toggleLinkCommand: null // cmd_toggle_link +}; + +/** + * Receive an asynchronous message from the iframe. + * + * @param {MessageEvent} aEvent Contains the message being received + */ +function receiveMessage(aEvent) { + if (aEvent.origin !== "chrome://lightning") { + return; + } + switch (aEvent.data.command) { + case "initializeItemMenu": + initializeItemMenu(aEvent.data.label, aEvent.data.accessKey); + break; + case "disableLinkCommand": { + let linkCommand = document.getElementById("cmd_toggle_link"); + if (linkCommand) { + setElementValue(linkCommand, "true", "disabled"); + } + break; + } + case "cancelDialog": + document.documentElement.cancelDialog(); + break; + case "closeWindowOrTab": + closeWindowOrTab(aEvent.data.iframeId); + break; + case "showCmdStatusNone": + document.getElementById("cmd_status_none").removeAttribute("hidden"); + break; + case "updateTitle": + updateTitle(aEvent.data.argument); + break; + case "updateConfigState": + updateItemTabState(aEvent.data.argument); + Object.assign(gConfig, aEvent.data.argument); + break; + case "enableAcceptCommand": + enableAcceptCommand(aEvent.data.argument); + break; + case "replyToClosingWindowWithTabs": + handleWindowClose(aEvent.data.response); + break; + case "removeDisableAndCollapseOnReadonly": + removeDisableAndCollapseOnReadonly(); + break; + case "setElementAttribute": { + let arg = aEvent.data.argument; + setElementValue(arg.id, arg.value, arg.attribute); + break; + } + case "loadCloudProviders": + loadCloudProviders(aEvent.data.items); + break; + } +} + +window.addEventListener("message", receiveMessage, false); + +/** + * Send an asynchronous message to an iframe. Additional properties of + * aMessage are generally arguments that will be passed to the function + * named in aMessage.command. If aIframeId is omitted, the message will + * be sent to the iframe of the current tab. + * + * @param {Object} aMessage Contains the message being sent + * @param {string} aMessage.command The name of a function to call + * @param {string} aIframeId (optional) id of an iframe to send the message to + */ +function sendMessage(aMessage, aIframeId) { + let iframeId = gTabmail ? aIframeId || gTabmail.currentTabInfo.iframe.id + : "lightning-item-panel-iframe"; + let iframe = document.getElementById(iframeId); + iframe.contentWindow.postMessage(aMessage, "*"); +} + +/** + * When the user closes the window, this function handles prompting them + * to save any unsaved changes for any open item tabs, before closing the + * window, or not if 'cancel' was clicked. Requires sending and receiving + * async messages from the iframes of all open item tabs. + * + * @param {boolean} aResponse The response from the tab's iframe + */ +function handleWindowClose(aResponse) { + if (!aResponse) { + // Cancel was clicked, just leave the window open. We're done. + return; + } else if (gItemTabIdsCopy.length > 0) { + // There are more unsaved changes in tabs to prompt the user about. + let nextId = gItemTabIdsCopy.shift(); + sendMessage({ command: "closingWindowWithTabs", id: nextId }, nextId); + } else { + // Close the window, there are no more unsaved changes in tabs. + window.removeEventListener("close", windowCloseListener, false); + window.close(); + } +} + +/** + * Listener function for window close. We prevent the window from + * closing, then for each open tab we prompt the user to save any + * unsaved changes with handleWindowClose. + * + * @param {Object} aEvent The window close event + */ +function windowCloseListener(aEvent) { + aEvent.preventDefault(); + gItemTabIdsCopy = gItemTabIds.slice(); + handleWindowClose(true); +} + +/** + * Load handler for the outer parent context that contains the iframe. + * + * @param {string} aIframeId (optional) Id of the iframe in this tab + * @param {string} aUrl (optional) The url to load in the iframe + */ +function onLoadLightningItemPanel(aIframeId, aUrl) { + let iframe; + let iframeSrc; + + if (!gTabmail) { + gTabmail = document.getElementById("tabmail") || null; + } + if (gTabmail) { + // tab case + let iframeId = aIframeId || gTabmail.currentTabInfo.iframe.id; + iframe = document.getElementById(iframeId); + iframeSrc = aUrl; + + // Add a listener to detect close events, prompt user about saving changes. + window.addEventListener("close", windowCloseListener, false); + } else { + // window dialog case + iframe = document.createElement("iframe"); + iframeSrc = window.arguments[0].useNewItemUI + ? "chrome://lightning/content/html-item-editing/lightning-item-iframe.html" + : "chrome://lightning/content/lightning-item-iframe.xul"; + + iframe.setAttribute("id", "lightning-item-panel-iframe"); + iframe.setAttribute("flex", "1"); + + let dialog = document.getElementById("calendar-event-dialog"); + let statusbar = document.getElementById("status-bar"); + + // Note: iframe.contentWindow is undefined before the iframe is inserted here. + dialog.insertBefore(iframe, statusbar); + + // Move the args so they are positioned relative to the iframe, + // for the window dialog just as they are for the tab. + // XXX Should we delete the arguments here in the parent context + // so they are only accessible in one place? + iframe.contentWindow.arguments = [window.arguments[0]]; + + // hide the ok and cancel dialog buttons + let accept = document.documentElement.getButton("accept"); + let cancel = document.documentElement.getButton("cancel"); + accept.setAttribute("collapsed", "true"); + cancel.setAttribute("collapsed", "true"); + cancel.parentNode.setAttribute("collapsed", "true"); + + // set toolbar icon color for light or dark themes + if (typeof ToolbarIconColor !== "undefined") { + ToolbarIconColor.init(); + } + } + + // event or task + let calendarItem = iframe.contentWindow.arguments[0].calendarEvent; + gConfig.isEvent = cal.isEvent(calendarItem); + + // for tasks in a window dialog, set the dialog id for CSS selection, etc. + if (!gTabmail && !gConfig.isEvent) { + setDialogId(document.documentElement, "calendar-task-dialog"); + } + + // timezones enabled + gConfig.timezonesEnabled = getTimezoneCommandState(); + iframe.contentWindow.gTimezonesEnabled = gConfig.timezonesEnabled; + + // toggle link + let cmdToggleLink = document.getElementById("cmd_toggle_link"); + gConfig.toggleLinkCommand = cmdToggleLink.getAttribute("checked") == "true"; + iframe.contentWindow.gShowLink = gConfig.toggleLinkCommand; + + // set the iframe src, which loads the iframe's contents + iframe.setAttribute("src", iframeSrc); +} + +/** + * Unload handler for the outer parent context that contains the iframe. + * Currently only called for windows and not tabs. + */ +function onUnloadLightningItemPanel() { + if (!gTabmail) { + // window dialog case + if (typeof ToolbarIconColor !== "undefined") { + ToolbarIconColor.uninit(); + } + } +} + +/** + * Updates the UI. Called when a user makes a change and when an + * event/task tab is shown. When a tab is shown aArg contains the gConfig + * data for that event/task. We pass the full tab state object to the + * update functions and they just use the properties they need from it. + * + * @param {Object} aArg Its properties hold data about the event/task + */ +function updateItemTabState(aArg) { + const lookup = { + privacy: updatePrivacy, + priority: updatePriority, + status: updateStatus, + showTimeAs: updateShowTimeAs, + percentComplete: updateMarkCompletedMenuItem, + attendeesCommand: updateAttendeesCommand, + attachUrlCommand: updateAttachment, + timezonesEnabled: updateTimezoneCommand + }; + for (let key of Object.keys(aArg)) { + let procedure = lookup[key]; + if (procedure) { + procedure(aArg); + } + } +} + +/** + * When in a window, set Item-Menu label to Event or Task. + * + * @param {string} aLabel The new name for the menu + * @param {string} aAccessKey The access key for the menu + */ +function initializeItemMenu(aLabel, aAccessKey) { + let menuItem = document.getElementById("item-menu"); + menuItem.setAttribute("label", aLabel); + menuItem.setAttribute("accesskey", aAccessKey); +} + +/** + * Handler for when dialog is accepted. + */ +function onAccept() { + sendMessage({ command: "onAccept" }); + return false; +} + +/** + * Handler for when dialog is canceled. + * + * @param {string} aIframeId The id of the iframe + */ +function onCancel(aIframeId) { + sendMessage({ command: "onCancel", iframeId: aIframeId }, aIframeId); + // We return false to prevent closing of a window until we + // can ask the user about saving any unsaved changes. + return false; +} + +/** + * Closes tab or window. Called after prompting to save any unsaved changes. + * + * @param {string} aIframeId The id of the iframe + */ +function closeWindowOrTab(iframeId) { + if (gTabmail) { + if (iframeId) { + // Find the tab associated with this iframeId, and close it. + let myTabInfo = gTabmail.tabInfo.filter((x) => "iframe" in x && x.iframe.id == iframeId)[0]; + myTabInfo.allowTabClose = true; + gTabmail.closeTab(myTabInfo); + } else { + gTabmail.currentTabInfo.allowTabClose = true; + gTabmail.removeCurrentTab(); + } + } else { + window.close(); + } +} + +/** + * Handler for saving the event or task. + * + * @param {boolean} aIsClosing Is the tab or window closing + */ +function onCommandSave(aIsClosing) { + sendMessage({ command: "onCommandSave", isClosing: aIsClosing }); +} + +/** + * Handler for deleting the event or task. + */ +function onCommandDeleteItem() { + sendMessage({ command: "onCommandDeleteItem" }); +} + +/** + * Update the title of the tab or window. + * + * @param {string} aNewTitle The new title + */ +function updateTitle(aNewTitle) { + if (gTabmail) { + gTabmail.currentTabInfo.title = aNewTitle; + gTabmail.setTabTitle(gTabmail.currentTabInfo); + } else { + document.title = aNewTitle; + } +} + +/** + * Open a new event. + */ +function openNewEvent() { + sendMessage({ command: "openNewEvent" }); +} + +/** + * Open a new task. + */ +function openNewTask() { + sendMessage({ command: "openNewTask" }); +} + + +/** + * Open a new Thunderbird compose window. + */ +function openNewMessage() { + MailServices.compose.OpenComposeWindow( + null, + null, + null, + Components.interfaces.nsIMsgCompType.New, + Components.interfaces.nsIMsgCompFormat.Default, + null, + null + ); +} + +/** + * Open a new addressbook window + */ +function openNewCardDialog() { + window.openDialog( + "chrome://messenger/content/addressbook/abNewCardDialog.xul", + "", + "chrome,modal,resizable=no,centerscreen" + ); +} + +/** + * Handler for edit attendees command. + */ +function editAttendees() { + sendMessage({ command: "editAttendees" }); +} + +/** + * Sends a message to set the gConfig values in the iframe. + * + * @param {Object} aArg Container + * @param {string} aArg.privacy (optional) New privacy value + * @param {short} aArg.priority (optional) New priority value + * @param {string} aArg.status (optional) New status value + * @param {string} aArg.showTimeAs (optional) New showTimeAs / transparency value + */ +function editConfigState(aArg) { + sendMessage({ command: "editConfigState", argument: aArg }); +} + +/** + * Rotates the Privacy of an item to the next value + * following the sequence -> PUBLIC -> CONFIDENTIAL -> PRIVATE ->. + */ +function rotatePrivacy() { + const states = ["PUBLIC", "CONFIDENTIAL", "PRIVATE"]; + let oldPrivacy = gConfig.privacy; + let newPrivacy = states[(states.indexOf(oldPrivacy) + 1) % states.length]; + editConfigState({ privacy: newPrivacy }); +} + +/** + * Handler for changing privacy. aEvent is used for the popup menu + * event-privacy-menupopup in the Privacy toolbar button. + * + * @param {nsIDOMNode} aTarget Has the new privacy in its "value" attribute + * @param {XULCommandEvent} aEvent (optional) the UI element selection event + */ +function editPrivacy(aTarget, aEvent) { + if (aEvent) { + aEvent.stopPropagation(); + } + // "privacy" is indeed the correct attribute to use here + let newPrivacy = aTarget.getAttribute("privacy"); + editConfigState({ privacy: newPrivacy }); +} + +/** + * Updates the UI according to the privacy setting and the selected + * calendar. If the selected calendar does not support privacy or only + * certain values, these are removed from the UI. This function should + * be called any time that privacy setting is updated. + * + * @param {Object} aArg Contains privacy properties + * @param {string} aArg.privacy The new privacy value + * @param {boolean} aArg.hasPrivacy Whether privacy is supported + * @param {string} aArg.calendarType The type of calendar + * @param {string[]} aArg.privacyValues The possible privacy values + */ +function updatePrivacy(aArg) { + if (aArg.hasPrivacy) { + // Update privacy capabilities (toolbar) + let menupopup = document.getElementById("event-privacy-menupopup"); + if (menupopup) { + // Only update the toolbar if the button is actually there + for (let node of menupopup.childNodes) { + let currentProvider = node.getAttribute("provider"); + if (node.hasAttribute("privacy")) { + let currentPrivacyValue = node.getAttribute("privacy"); + // Collapsed state + + // Hide the toolbar if the value is unsupported or is for a + // specific provider and doesn't belong to the current provider. + if (!aArg.privacyValues.includes(currentPrivacyValue) || + (currentProvider && currentProvider != aArg.calendarType)) { + node.setAttribute("collapsed", "true"); + } else { + node.removeAttribute("collapsed"); + } + + // Checked state + if (aArg.privacy == currentPrivacyValue) { + node.setAttribute("checked", "true"); + } else { + node.removeAttribute("checked"); + } + } + } + } + + // Update privacy capabilities (menu) but only if we are not in a tab. + if (!gTabmail) { + menupopup = document.getElementById("options-privacy-menupopup"); + for (let node of menupopup.childNodes) { + let currentProvider = node.getAttribute("provider"); + if (node.hasAttribute("privacy")) { + let currentPrivacyValue = node.getAttribute("privacy"); + // Collapsed state + + // Hide the menu if the value is unsupported or is for a + // specific provider and doesn't belong to the current provider. + if (!aArg.privacyValues.includes(currentPrivacyValue) || + (currentProvider && currentProvider != aArg.calendarType)) { + node.setAttribute("collapsed", "true"); + } else { + node.removeAttribute("collapsed"); + } + + // Checked state + if (aArg.privacy == currentPrivacyValue) { + node.setAttribute("checked", "true"); + } else { + node.removeAttribute("checked"); + } + } + } + } + + // Update privacy capabilities (statusbar) + let privacyPanel = document.getElementById("status-privacy"); + let hasAnyPrivacyValue = false; + for (let node of privacyPanel.childNodes) { + let currentProvider = node.getAttribute("provider"); + if (node.hasAttribute("privacy")) { + let currentPrivacyValue = node.getAttribute("privacy"); + + // Hide the panel if the value is unsupported or is for a + // specific provider and doesn't belong to the current provider, + // or is not the items privacy value + if (!aArg.privacyValues.includes(currentPrivacyValue) || + (currentProvider && currentProvider != aArg.calendarType) || + aArg.privacy != currentPrivacyValue) { + node.setAttribute("collapsed", "true"); + } else { + node.removeAttribute("collapsed"); + hasAnyPrivacyValue = true; + } + } + } + + // Don't show the status panel if no valid privacy value is selected + if (hasAnyPrivacyValue) { + privacyPanel.removeAttribute("collapsed"); + } else { + privacyPanel.setAttribute("collapsed", "true"); + } + } else { + // aArg.hasPrivacy is false + setElementValue("button-privacy", "true", "disabled"); + setElementValue("status-privacy", "true", "collapsed"); + // in the tab case the menu item does not exist + let privacyMenuItem = document.getElementById("options-privacy-menu"); + if (privacyMenuItem) { + setElementValue("options-privacy-menu", "true", "disabled"); + } + } +} + +/** + * Rotates the Priority of an item to the next value + * following the sequence -> Not specified -> Low -> Normal -> High ->. + */ +function rotatePriority() { + let now = gConfig.priority; + let next; + if (now <= 0 || now > 9) { // not specified -> low + next = 9; + } else if (now >= 1 && now <= 4) { // high -> not specified + next = 0; + } else if (now == 5) { // normal -> high + next = 1; + } else if (now >= 6 && now <= 9) { // low -> normal + next = 5; + } + editConfigState({ priority: next }); +} + +/** + * Handler to change the priority. + * + * @param {nsIDOMNode} aTarget Has the new priority in its "value" attribute + */ +function editPriority(aTarget) { + let newPriority = parseInt(aTarget.getAttribute("value"), 10); + editConfigState({ priority: newPriority }); +} + +/** + * Updates the dialog controls related to priority. + * + * @param {Object} aArg Contains priority properties + * @param {string} aArg.priority The new priority value + * @param {boolean} aArg.hasPriority Whether priority is supported + */ +function updatePriority(aArg) { + // Set up capabilities + if (document.getElementById("button-priority")) { + setElementValue("button-priority", !aArg.hasPriority && "true", "disabled"); + } + if (!gTabmail && document.getElementById("options-priority-menu")) { + setElementValue("options-priority-menu", !aArg.hasPriority && "true", "disabled"); + } + setElementValue("status-priority", !aArg.hasPriority && "true", "collapsed"); + + if (aArg.hasPriority) { + let priorityLevel = "none"; + if (aArg.priority >= 1 && aArg.priority <= 4) { + priorityLevel = "high"; + } else if (aArg.priority == 5) { + priorityLevel = "normal"; + } else if (aArg.priority >= 6 && aArg.priority <= 9) { + priorityLevel = "low"; + } + + let priorityNone = document.getElementById("cmd_priority_none"); + let priorityLow = document.getElementById("cmd_priority_low"); + let priorityNormal = document.getElementById("cmd_priority_normal"); + let priorityHigh = document.getElementById("cmd_priority_high"); + + priorityNone.setAttribute("checked", + priorityLevel == "none" ? "true" : "false"); + priorityLow.setAttribute("checked", + priorityLevel == "low" ? "true" : "false"); + priorityNormal.setAttribute("checked", + priorityLevel == "normal" ? "true" : "false"); + priorityHigh.setAttribute("checked", + priorityLevel == "high" ? "true" : "false"); + + // Status bar panel + let priorityPanel = document.getElementById("status-priority"); + if (priorityLevel == "none") { + // If the priority is none, don't show the status bar panel + priorityPanel.setAttribute("collapsed", "true"); + } else { + priorityPanel.removeAttribute("collapsed"); + let foundPriority = false; + for (let node of priorityPanel.childNodes) { + if (foundPriority) { + node.setAttribute("collapsed", "true"); + } else { + node.removeAttribute("collapsed"); + } + if (node.getAttribute("value") == priorityLevel) { + foundPriority = true; + } + } + } + } +} + +/** + * Rotate the Status of an item to the next value following + * the sequence -> NONE -> TENTATIVE -> CONFIRMED -> CANCELLED ->. + */ +function rotateStatus() { + let oldStatus = gConfig.status; + let noneCommand = document.getElementById("cmd_status_none"); + let noneCommandIsVisible = !noneCommand.hasAttribute("hidden"); + let states = ["TENTATIVE", "CONFIRMED", "CANCELLED"]; + + // If control for status "NONE" ("cmd_status_none") is visible, + // allow rotating to it. + if (gConfig.isEvent && noneCommandIsVisible) { + states.unshift("NONE"); + } + + let newStatus = states[(states.indexOf(oldStatus) + 1) % states.length]; + editConfigState({ status: newStatus }); +} + +/** + * Handler for changing the status. + * + * @param {nsIDOMNode} aTarget Has the new status in its "value" attribute + */ +function editStatus(aTarget) { + let newStatus = aTarget.getAttribute("value"); + editConfigState({ status: newStatus }); +} + +/** + * Update the dialog controls related to status. + * + * @param {Object} aArg Contains the new status value + * @param {string} aArg.status The new status value + */ +function updateStatus(aArg) { + const statusLabels = ["status-status-tentative-label", + "status-status-confirmed-label", + "status-status-cancelled-label"]; + const commands = ["cmd_status_none", + "cmd_status_tentative", + "cmd_status_confirmed", + "cmd_status_cancelled"]; + let found = false; + setBooleanAttribute("status-status", "collapsed", true); + commands.forEach((aElement, aIndex, aArray) => { + let node = document.getElementById(aElement); + let matches = (node.getAttribute("value") == aArg.status); + found = found || matches; + + node.setAttribute("checked", matches ? "true" : "false"); + + if (aIndex > 0) { + setBooleanAttribute(statusLabels[aIndex-1], "hidden", !matches); + if (matches) { + setBooleanAttribute("status-status", "collapsed", false); + } + } + }); + if (!found) { + // The current Status value is invalid. Change the status to + // "not specified" and update the status again. + sendMessage({ command: "editStatus", value: "NONE" }); + } +} + +/** + * Toggles the transparency ("Show Time As" property) of an item + * from BUSY (Opaque) to FREE (Transparent). + */ +function rotateShowTimeAs() { + const states = ["OPAQUE", "TRANSPARENT"]; + let oldValue = gConfig.showTimeAs; + let newValue = states[(states.indexOf(oldValue) + 1) % states.length]; + editConfigState({ showTimeAs: newValue }); +} + +/** + * Handler for changing the transparency. + * + * @param {nsIDOMNode} aTarget Has the new transparency in its "value" attribute + */ +function editShowTimeAs(aTarget) { + let newValue = aTarget.getAttribute("value"); + editConfigState({ showTimeAs: newValue }); +} + +/** + * Update the dialog controls related to transparency. + * + * @param {Object} aArg Contains the new transparency value + * @param {string} aArg.showTimeAs The new transparency value + */ +function updateShowTimeAs(aArg) { + let showAsBusy = document.getElementById("cmd_showtimeas_busy"); + let showAsFree = document.getElementById("cmd_showtimeas_free"); + + showAsBusy.setAttribute("checked", + aArg.showTimeAs == "OPAQUE" ? "true" : "false"); + showAsFree.setAttribute("checked", + aArg.showTimeAs == "TRANSPARENT" ? "true" : "false"); + + setBooleanAttribute("status-freebusy", + "collapsed", + aArg.showTimeAs != "OPAQUE" && aArg.showTimeAs != "TRANSPARENT"); + setBooleanAttribute("status-freebusy-free-label", "hidden", aArg.showTimeAs == "OPAQUE"); + setBooleanAttribute("status-freebusy-busy-label", "hidden", aArg.showTimeAs == "TRANSPARENT"); +} + +/** + * Change the task percent complete (and thus task status). + * + * @param {short} aPercentComplete The new percent complete value + */ +function editToDoStatus(aPercentComplete) { + sendMessage({ command: "editToDoStatus", value: aPercentComplete }); +} + +/** + * Check or uncheck the "Mark updated" menu item in "Events and Tasks" + * menu based on the percent complete value. (The percent complete menu + * items are updated by changeMenuByPropertyName in calendar-menus.xml) + * + * @param {Object} aArg Container + * @param {short} aArg.percentComplete The percent complete value + */ +function updateMarkCompletedMenuItem(aArg) { + // Command only for tab case, function only to be executed in dialog windows. + if (gTabmail) { + let completedCommand = document.getElementById("calendar_toggle_completed_command"); + let isCompleted = aArg.percentComplete == 100; + completedCommand.setAttribute("checked", isCompleted); + } +} + +/** + * Postpone the task's start date/time and due date/time. ISO 8601 + * format: "PT1H", "P1D", and "P1W" are 1 hour, 1 day, and 1 week. (We + * use this format intentionally instead of a calIDuration object because + * those objects cannot be serialized for message passing with iframes.) + * + * @param {string} aDuration A duration in ISO 8601 format + */ +function postponeTask(aDuration) { + sendMessage({ command: "postponeTask", value: aDuration }); +} + +/** + * Get the timezone button state. + * + * @return {boolean} True is active/checked and false is inactive/unchecked + */ +function getTimezoneCommandState() { + let cmdTimezone = document.getElementById("cmd_timezone"); + return cmdTimezone.getAttribute("checked") == "true"; +} + +/** + * Set the timezone button state. Used to keep the toolbar button in + * sync when switching tabs. + * + * @param {Object} aArg Contains timezones property + * @param {boolean} aArg.timezonesEnabled Are timezones enabled? + */ +function updateTimezoneCommand(aArg) { + let cmdTimezone = document.getElementById("cmd_timezone"); + cmdTimezone.setAttribute("checked", aArg.timezonesEnabled); + gConfig.timezonesEnabled = aArg.timezonesEnabled; +} + +/** + * Toggles the command that allows enabling the timezone links in the dialog. + */ +function toggleTimezoneLinks() { + let cmdTimezone = document.getElementById("cmd_timezone"); + let currentState = getTimezoneCommandState(); + cmdTimezone.setAttribute("checked", currentState ? "false" : "true"); + gConfig.timezonesEnabled = !currentState; + sendMessage({ command: "toggleTimezoneLinks", checked: !currentState }); +} + +/** + * Toggles the visibility of the related link (rfc2445 URL property). + */ +function toggleLink() { + let linkCommand = document.getElementById("cmd_toggle_link"); + let checked = linkCommand.getAttribute("checked") == "true"; + + linkCommand.setAttribute("checked", checked ? "false" : "true"); + sendMessage({ command: "toggleLink", checked: !checked }); +} + +/** + * Prompts the user to attach an url to this item. + */ +function attachURL() { + sendMessage({ command: "attachURL" }); +} + +/** + * Updates dialog controls related to item attachments. + * + * @param {Object} aArg Container + * @param {boolean} aArg.attachUrlCommand Enable the attach url command? + */ +function updateAttachment(aArg) { + setElementValue("cmd_attach_url", !aArg.attachUrlCommand && "true", "disabled"); +} + +/** + * Updates attendees command enabled/disabled state. + * + * @param {Object} aArg Container + * @param {boolean} aArg.attendeesCommand Enable the attendees command? + */ +function updateAttendeesCommand(aArg) { + setElementValue("cmd_attendees", !aArg.attendeesCommand, "disabled"); +} + +/** + * Enables/disables the commands cmd_accept and cmd_save related to the + * save operation. + * + * @param {boolean} aEnable Enable the commands? + */ +function enableAcceptCommand(aEnable) { + setElementValue("cmd_accept", !aEnable, "disabled"); + setElementValue("cmd_save", !aEnable, "disabled"); +} + +/** + * Enable and un-collapse all elements that are disable-on-readonly and + * collapse-on-readonly. + */ +function removeDisableAndCollapseOnReadonly() { + let enableElements = document.getElementsByAttribute("disable-on-readonly", "true"); + for (let element of enableElements) { + element.removeAttribute("disabled"); + } + let collapseElements = document.getElementsByAttribute("collapse-on-readonly", "true"); + for (let element of collapseElements) { + element.removeAttribute("collapsed"); + } +} + +/** + * Handler to toggle toolbar visibility. + * + * @param {string} aToolbarId The id of the toolbar node to toggle + * @param {string} aMenuitemId The corresponding menuitem in the view menu + */ +function onCommandViewToolbar(aToolbarId, aMenuItemId) { + let toolbar = document.getElementById(aToolbarId); + let menuItem = document.getElementById(aMenuItemId); + + if (!toolbar || !menuItem) { + return; + } + + let toolbarCollapsed = toolbar.collapsed; + + // toggle the checkbox + menuItem.setAttribute("checked", toolbarCollapsed); + + // toggle visibility of the toolbar + toolbar.collapsed = !toolbarCollapsed; + + document.persist(aToolbarId, "collapsed"); + document.persist(aMenuItemId, "checked"); +} + +/** + * Called after the customize toolbar dialog has been closed by the + * user. We need to restore the state of all buttons and commands of + * all customizable toolbars. + * + * @param {boolean} aToolboxChanged When true the toolbox has changed + */ +function dialogToolboxCustomizeDone(aToolboxChanged) { + // Re-enable menu items (disabled during toolbar customization). + let menubarId = gTabmail ? "mail-menubar" : "event-menubar"; + let menubar = document.getElementById(menubarId); + for (let menuitem of menubar.childNodes) { + menuitem.removeAttribute("disabled"); + } + + // make sure our toolbar buttons have the correct enabled state restored to them... + document.commandDispatcher.updateCommands("itemCommands"); + + // Enable the toolbar context menu items + document.getElementById("cmd_customize").removeAttribute("disabled"); + + // Update privacy items to make sure the toolbarbutton's menupopup is set + // correctly + updatePrivacy(gConfig); +} + +/** + * Handler to start the customize toolbar dialog for the event dialog's toolbar. + */ +function onCommandCustomize() { + // install the callback that handles what needs to be + // done after a toolbar has been customized. + let toolboxId = "event-toolbox"; + + let toolbox = document.getElementById(toolboxId); + toolbox.customizeDone = dialogToolboxCustomizeDone; + + // Disable menu items during toolbar customization. + let menubarId = gTabmail ? "mail-menubar" : "event-menubar"; + let menubar = document.getElementById(menubarId); + for (let menuitem of menubar.childNodes) { + menuitem.setAttribute("disabled", true); + } + + // Disable the toolbar context menu items + document.getElementById("cmd_customize").setAttribute("disabled", "true"); + + let wintype = document.documentElement.getAttribute("windowtype"); + wintype = wintype.replace(/:/g, ""); + + window.openDialog("chrome://global/content/customizeToolbar.xul", + "CustomizeToolbar" + wintype, + "chrome,all,dependent", + document.getElementById(toolboxId), // toolbox dom node + false, // is mode toolbar yes/no? + null, // callback function + "dialog"); // name of this mode +} + +/** + * Add menu items to the UI for attaching files using a cloud provider. + * + * @param {Object[]} aItemObjects Array of objects that each contain + * data to create a menuitem + */ +function loadCloudProviders(aItemObjects) { + /** + * Deletes any existing menu items in aParentNode that have a + * cloudProviderAccountKey attribute. + * + * @param {nsIDOMNode} aParentNode A menupopup containing menu items + */ + function deleteAlreadyExisting(aParentNode) { + for (let node of aParentNode.childNodes) { + if (node.cloudProviderAccountKey) { + aParentNode.removeChild(node); + } + } + } + + // Delete any existing menu items with a cloudProviderAccountKey, + // needed for the tab case to prevent duplicate menu items, and + // helps keep the menu items current. + let toolbarPopup = document.getElementById("button-attach-menupopup"); + if (toolbarPopup) { + deleteAlreadyExisting(toolbarPopup); + } + let optionsPopup = document.getElementById("options-attachments-menupopup"); + if (optionsPopup) { + deleteAlreadyExisting(optionsPopup); + } + + for (let itemObject of aItemObjects) { + // Create a menu item. + let item = createXULElement("menuitem"); + item.setAttribute("label", itemObject.label); + item.setAttribute("observes", "cmd_attach_cloud"); + item.setAttribute("oncommand", "attachFileByAccountKey(event.target.cloudProviderAccountKey); event.stopPropagation();"); + + if (itemObject.class) { + item.setAttribute("class", itemObject.class); + item.setAttribute("image", itemObject.image); + } + + // Add the menu item to the UI. + if (toolbarPopup) { + toolbarPopup.appendChild(item.cloneNode(true)).cloudProviderAccountKey = itemObject.cloudProviderAccountKey; + } + if (optionsPopup) { + // This one doesn't need to clone, just use the item itself. + optionsPopup.appendChild(item).cloudProviderAccountKey = itemObject.cloudProviderAccountKey; + } + } +} + +/** + * Send a message to attach a file using a given cloud provider, + * to be identified by the cloud provider's accountKey. + * + * @param {string} aAccountKey The accountKey for a cloud provider + */ +function attachFileByAccountKey(aAccountKey) { + sendMessage({ command: "attachFileByAccountKey", accountKey: aAccountKey }); +} diff --git a/calendar/lightning/content/lightning-item-panel.xul b/calendar/lightning/content/lightning-item-panel.xul new file mode 100644 index 000000000..810c52ccf --- /dev/null +++ b/calendar/lightning/content/lightning-item-panel.xul @@ -0,0 +1,165 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- 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/. --> + +<!-- lightning-toolbar.dtd is only needed for the app menu button --> +<!DOCTYPE overlay [ + <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> + <!ENTITY % globalDTD SYSTEM "chrome://calendar/locale/global.dtd"> + <!ENTITY % calendarDTD SYSTEM "chrome://calendar/locale/calendar.dtd"> + <!ENTITY % eventDialogDTD SYSTEM "chrome://calendar/locale/calendar-event-dialog.dtd"> + <!ENTITY % toolbarDTD SYSTEM "chrome://lightning/locale/lightning-toolbar.dtd"> + %brandDTD; + %globalDTD; + %calendarDTD; + %eventDialogDTD; + %toolbarDTD; +]> + +<?xul-overlay href="chrome://lightning/content/lightning-item-toolbar.xul"?> + +<overlay id="ltnCalendarItemPanelContentOverlay" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <tabpanels id="tabpanelcontainer"> + <vbox id="lightningItemPanel" collapsed="true"> + + <!-- The id of the inner vbox and the iframe are set dynamically + when a tab is created. --> + <vbox flex="1" + id="dummy-calendar-event-dialog-tab" + class="calendar-event-dialog-tab"> + + <!-- Commands --> + <commandset id="itemCommands"> + <command id="cmd_save" + disable-on-readonly="true" + oncommand="onCommandSave()"/> + <command id="cmd_item_delete" + disable-on-readonly="true" + oncommand="onCommandDeleteItem()"/> + + <!-- View menu --> + <command id="cmd_customize" + oncommand="onCommandCustomize()"/> + <command id="cmd_toggle_link" + persist="checked" + oncommand="toggleLink()"/> + + <!-- status --> + <command id="cmd_status_none" + oncommand="editStatus(event.target)" + hidden="true" + value="NONE"/> + <command id="cmd_status_tentative" + oncommand="editStatus(event.target)" + value="TENTATIVE"/> + <command id="cmd_status_confirmed" + oncommand="editStatus(event.target)" + value="CONFIRMED"/> + <command id="cmd_status_cancelled" + oncommand="editStatus(event.target)" + value="CANCELLED"/> + + <!-- priority --> + <command id="cmd_priority_none" + oncommand="editPriority(event.target)" + value="0"/> + <command id="cmd_priority_low" + oncommand="editPriority(event.target)" + value="9"/> + <command id="cmd_priority_normal" + oncommand="editPriority(event.target)" + value="5"/> + <command id="cmd_priority_high" + oncommand="editPriority(event.target)" + value="1"/> + + <!-- freebusy --> + <command id="cmd_showtimeas_busy" + oncommand="editShowTimeAs(event.target)" + value="OPAQUE"/> + <command id="cmd_showtimeas_free" + oncommand="editShowTimeAs(event.target)" + value="TRANSPARENT"/> + + <!-- attendees --> + <command id="cmd_attendees" + oncommand="editAttendees();"/> + + <!-- accept, attachments, timezone --> + <command id="cmd_accept" + disable-on-readonly="true" + oncommand="onAccept();"/> + <command id="cmd_attach_url" + disable-on-readonly="true" + oncommand="attachURL()"/> + <command id="cmd_attach_cloud" + disable-on-readonly="true"/> + <command id="cmd_timezone" + persist="checked" + checked="false" + oncommand="toggleTimezoneLinks()"/> + </commandset> + + <keyset id="calendar-event-dialog-keyset"> + <key id="save-key" + modifiers="accel, shift" + key="&event.dialog.save.key;" + command="cmd_save"/> + <key id="saveandclose-key" + modifiers="accel" + key="&event.dialog.saveandclose.key;" + command="cmd_accept"/> + <key id="saveandclose-key2" + modifiers="accel" + keycode="VK_RETURN" + command="cmd_accept"/> + </keyset> + + <toolbox id="event-toolbox" + class="mail-toolbox" + mode="full" + defaultmode="full" + labelalign="end" + defaultlabelalign="end"> + <!-- more toolbarpalette items are added with an overlay --> + <toolbarpalette id="event-toolbarpalette"> + <toolbarbutton id="calendar-item-appmenu-button" + class="toolbarbutton-1 button-appmenu" + label="&lightning.toolbar.appmenuButton.label;" + tooltiptext="&lightning.toolbar.appmenuButton1.tooltip;"/> + </toolbarpalette> + <!-- toolboxid is set here since we move the toolbar around for tabs --> + <toolbar id="event-tab-toolbar" + toolbarname="&event.menu.view.toolbars.event.label;" + accesskey="&event.menu.view.toolbars.event.accesskey;" + toolboxid="event-toolbox" + class="chromeclass-toolbar inline-toolbar" + customizable="true" + labelalign="end" + defaultlabelalign="end" + context="event-dialog-toolbar-context-menu" + defaultset="button-saveandclose,button-attendees,button-privacy,button-url,button-priority,button-status,button-freebusy,button-delete,spring,calendar-item-appmenu-button"/> + <toolbarset id="custom-toolbars" context="event-dialog-toolbar-context-menu"/> + </toolbox> + + <iframe id="lightning-item-panel-iframe" flex="1"/> + + </vbox> + </vbox> + </tabpanels> + + <popupset id="calendar-popupset"> + <menupopup id="event-dialog-toolbar-context-menu" + onpopupshowing="onToolbarsPopupShowingForTabType(event);"> + <menuseparator id="customizeEventToolbarMenuSeparator"/> + <menuitem id="CustomizeDialogToolbar" + label="&event.menu.view.toolbars.customize.label;" + command="cmd_customize"/> + </menupopup> + </popupset> + +</overlay> + diff --git a/calendar/lightning/content/lightning-item-toolbar.xul b/calendar/lightning/content/lightning-item-toolbar.xul new file mode 100644 index 000000000..982df7ecb --- /dev/null +++ b/calendar/lightning/content/lightning-item-toolbar.xul @@ -0,0 +1,181 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- 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/. --> + +<!DOCTYPE overlay [ + <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> + <!ENTITY % globalDTD SYSTEM "chrome://calendar/locale/global.dtd"> + <!ENTITY % calendarDTD SYSTEM "chrome://calendar/locale/calendar.dtd"> + <!ENTITY % eventDialogDTD SYSTEM "chrome://calendar/locale/calendar-event-dialog.dtd"> + %brandDTD; + %globalDTD; + %calendarDTD; + %eventDialogDTD; +]> + +<overlay id="ltnCalendarItemPanelContentOverlay" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <toolbarpalette id="event-toolbarpalette"> + <toolbarbutton id="button-save" + mode="dialog" + class="cal-event-toolbarbutton toolbarbutton-1" + label="&event.toolbar.save.label2;" + tooltiptext="&event.toolbar.save.tooltip2;" + command="cmd_save"/> + <toolbarbutton id="button-saveandclose" + mode="dialog" + class="cal-event-toolbarbutton toolbarbutton-1" + label="&event.toolbar.saveandclose.label;" + tooltiptext="&event.toolbar.saveandclose.tooltip;" + command="cmd_accept"/> + <toolbarbutton id="button-attendees" + mode="dialog" + class="cal-event-toolbarbutton toolbarbutton-1 event-only" + disable-on-readonly="true" + label="&event.toolbar.attendees.label;" + tooltiptext="&event.toolbar.attendees.tooltip;" + command="cmd_attendees"/> + <toolbarbutton id="button-privacy" + mode="dialog" + class="cal-event-toolbarbutton toolbarbutton-1" + type="menu-button" + disable-on-readonly="true" + label="&event.toolbar.privacy.label;" + tooltiptext="&event.toolbar.privacy.tooltip;" + oncommand="rotatePrivacy()"> + <menupopup id="event-privacy-menupopup"> + <menuitem id="event-privacy-public-menuitem" + name="event-privacy-group" + label="&event.menu.options.privacy.public.label;" + type="radio" + privacy="PUBLIC" + oncommand="editPrivacy(this, event)"/> + <menuitem id="event-privacy-confidential-menuitem" + name="event-privacy-group" + label="&event.menu.options.privacy.confidential.label;" + type="radio" + privacy="CONFIDENTIAL" + oncommand="editPrivacy(this, event)"/> + <menuitem id="event-privacy-private-menuitem" + name="event-privacy-group" + label="&event.menu.options.privacy.private.label;" + type="radio" + privacy="PRIVATE" + oncommand="editPrivacy(this, event)"/> + </menupopup> + </toolbarbutton> + <toolbarbutton id="button-url" + type="menu-button" + mode="dialog" + class="cal-event-toolbarbutton toolbarbutton-1" + label="&event.attachments.menubutton.label;" + tooltiptext="&event.toolbar.attachments.tooltip;" + command="cmd_attach_url" + disable-on-readonly="true"> + <menupopup id="button-attach-menupopup"> + <menuitem id="button-attach-url" + label="&event.attachments.url.label;" + command="cmd_attach_url"/> + </menupopup> + </toolbarbutton> + <toolbarbutton id="button-delete" + mode="dialog" + class="cal-event-toolbarbutton toolbarbutton-1" + label="&event.toolbar.delete.label;" + tooltiptext="&event.toolbar.delete.tooltip;" + command="cmd_item_delete" + disable-on-readonly="true"/> + <toolbarbutton id="button-priority" + mode="dialog" + class="cal-event-toolbarbutton toolbarbutton-1" + type="menu-button" + disable-on-readonly="true" + label="&event.menu.options.priority2.label;" + tooltiptext="&event.toolbar.priority.tooltip;" + oncommand="rotatePriority()"> + <menupopup id="event-priority-menupopup"> + <menuitem id="event-priority-none-menuitem" + name="event-priority-group" + label="&event.menu.options.priority.notspecified.label;" + type="radio" + command="cmd_priority_none"/> + <menuitem id="event-priority-low-menuitem" + name="event-priority-group" + label="&event.menu.options.priority.low.label;" + type="radio" + command="cmd_priority_low"/> + <menuitem id="event-priority-normal-menuitem" + name="event-priority-group" + label="&event.menu.options.priority.normal.label;" + type="radio" + command="cmd_priority_normal"/> + <menuitem id="event-priority-high-menuitem" + name="event-priority-group" + label="&event.menu.options.priority.high.label;" + type="radio" + command="cmd_priority_high"/> + </menupopup> + </toolbarbutton> + <toolbarbutton id="button-status" + mode="dialog" + class="cal-event-toolbarbutton toolbarbutton-1 event-only" + type="menu-button" + disable-on-readonly="true" + label="&newevent.status.label;" + tooltiptext="&event.toolbar.status.tooltip;" + oncommand="rotateStatus()"> + <menupopup id="event-status-menupopup"> + <menuitem id="event-status-none-menuitem" + name="event-status-group" + label="&newevent.eventStatus.none.label;" + type="radio" + command="cmd_status_none"/> + <menuitem id="event-status-tentative-menuitem" + name="event-status-group" + label="&newevent.status.tentative.label;" + type="radio" + command="cmd_status_tentative"/> + <menuitem id="event-status-confirmed-menuitem" + name="event-status-group" + label="&newevent.status.confirmed.label;" + type="radio" + command="cmd_status_confirmed"/> + <menuitem id="event-status-cancelled-menuitem" + name="event-status-group" + label="&newevent.eventStatus.cancelled.label;" + type="radio" + command="cmd_status_cancelled"/> + </menupopup> + </toolbarbutton> + <toolbarbutton id="button-freebusy" + mode="dialog" + class="cal-event-toolbarbutton toolbarbutton-1 event-only" + type="menu-button" + disable-on-readonly="true" + label="&event.menu.options.show.time.label;" + tooltiptext="&event.toolbar.freebusy.tooltip;" + oncommand="rotateShowTimeAs()"> + <menupopup id="event-freebusy-menupopup"> + <menuitem id="event-freebusy-busy-menuitem" + name="event-freebusy-group" + label="&event.menu.options.show.time.busy.label;" + type="radio" + command="cmd_showtimeas_busy"/> + <menuitem id="event-freebusy-free-menuitem" + name="event-freebusy-group" + label="&event.menu.options.show.time.free.label;" + type="radio" + command="cmd_showtimeas_free"/> + </menupopup> + </toolbarbutton> + <toolbarbutton id="button-timezones" + mode="dialog" + type="checkbox" + class="cal-event-toolbarbutton toolbarbutton-1" + label="&event.menu.options.timezone2.label;" + tooltiptext="&event.menu.options.timezone2.label;" + command="cmd_timezone"/> + </toolbarpalette> +</overlay> diff --git a/calendar/lightning/content/lightning-menus.xul b/calendar/lightning/content/lightning-menus.xul new file mode 100644 index 000000000..9fdebb9ac --- /dev/null +++ b/calendar/lightning/content/lightning-menus.xul @@ -0,0 +1,814 @@ +<?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/. --> + +<!DOCTYPE overlay [ + <!ENTITY % lightningDTD SYSTEM "chrome://lightning/locale/lightning.dtd"> %lightningDTD; + <!ENTITY % calendarDTD SYSTEM "chrome://calendar/locale/calendar.dtd" > %calendarDTD; + <!ENTITY % toolbarDTD SYSTEM "chrome://lightning/locale/lightning-toolbar.dtd" > %toolbarDTD; + <!ENTITY % menuOverlayDTD SYSTEM "chrome://calendar/locale/menuOverlay.dtd" > %menuOverlayDTD; + <!ENTITY % eventDialogDTD SYSTEM "chrome://calendar/locale/calendar-event-dialog.dtd" > %eventDialogDTD; +]> + +<overlay id="ltnMenusOverlay" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <menupopup id="menu_FilePopup"> + <menu id="menu_Open" + mode="calendar" + label="&lightning.menupopup.open.label;" + accesskey="&lightning.menupopup.open.accesskey;" + insertafter="menu_New"> + <menupopup id="menu_OpenPopup"> + <menuitem id="ltnOpenMessageFileMenuitem" + label="&lightning.menupopup.open.message.label;" + accesskey="&lightning.menupopup.open.message.accesskey;" + oncommand="MsgOpenFromFile();"/> + <menuitem id="ltnOpenCalendarFileMenuitem" + label="&lightning.menupopup.open.calendar.label;" + accesskey="&lightning.menupopup.open.calendar.accesskey;" + oncommand="openLocalCalendar();"/> + </menupopup> + </menu> + <menuitem id="ltnSave" + insertbefore="menu_saveAs" + label="&event.menu.item.save.label;" + accesskey="&event.menu.item.save.tab.accesskey;" + key="save-key" + command="cmd_save" + observes="cmd_save"/> + <menuitem id="ltnSaveAndClose" + insertafter="ltnSave" + label="&event.menu.item.saveandclose.label;" + accesskey="&event.menu.item.saveandclose.tab.accesskey;" + command="cmd_accept" + observes="cmd_accept"/> + </menupopup> + <menuitem id="openMessageFileMenuitem" hidden="true"/> + + <menupopup id="menu_NewPopup"> + <menuitem id="ltnNewEvent" + label="&lightning.menupopup.new.event.label;" + insertbefore="menu_newFolder" + accesskey="&lightning.menupopup.new.event.accesskey;" + key="calendar-new-event-key" + command="calendar_new_event_command" + observes="calendar_new_event_command"/> + <menuitem id="ltnNewTask" + label="&lightning.menupopup.new.task.label;" + insertbefore="menu_newFolder" + accesskey="&lightning.menupopup.new.task.accesskey;" + key="calendar-new-todo-key" + command="calendar_new_todo_command" + observes="calendar_new_todo_command"/> + <menuseparator id="afterltnNewTask" + insertbefore="menu_newFolder" + observes="menu_newFolder"/> + <menuitem id="ltnNewCalendar" label="&lightning.menupopup.new.calendar.label;" + command="calendar_new_calendar_command" + observes="calendar_new_calendar_command" + accesskey="&lightning.menupopup.new.calendar.accesskey;" + insertafter="newAccountMenuItem"/> + </menupopup> + + <menupopup id="menu_EditPopup"> + <menuitem id="ltnCalendarProperties" + insertafter="menu_properties" + label="&calendar.properties.label;" + accesskey="&calendar.properties.accesskey;" + command="calendar_edit_calendar_command" + observes="calendar_edit_calendar_command"/> + </menupopup> + + <menupopup id="menu_View_Popup"> + <menu id="menu_Toolbars" + onpopupshowing="onToolbarsPopupShowingForTabType(event);"/> + <menuseparator id="ltnViewMenuSeparator" + insertbefore="viewSortMenuSeparator"/> + <menu id="ltnTodayPaneMenu" + insertbefore="viewSortMenuSeparator" + label="&calendar.context.button.label;" + accesskey="&calendar.context.button.accesskey;"> + <menupopup id="ltnTodayPaneMenuPopup"> + <menuitem id="ltnShowTodayPane-2" + label="&todaypane.showTodayPane.label;" + accesskey="&todaypane.showTodayPane.accesskey;" + type="checkbox" + key="todaypanekey" + command="calendar_toggle_todaypane_command"/> + <menuseparator id="ltnSeparatorBeforeDisplayMiniday"/> + <menuitem id="ltnTodayPaneDisplayMiniday" + name="minidisplay" + value="miniday" + type="radio" + oncommand="TodayPane.displayMiniSection('miniday')" + label="&todaypane.showMiniday.label;" + accesskey="&todaypane.showMiniday.accesskey;"/> + <menuitem id="ltnTodayPaneDisplayMinimonth" + name="minidisplay" + value="minimonth" + type="radio" + oncommand="TodayPane.displayMiniSection('minimonth')" + label="&todaypane.showMinimonth.label;" + accesskey="&todaypane.showMinimonth.accesskey;"/> + <menuitem id="ltnTodayPaneDisplayNone" + name="minidisplay" + value="none" + type="radio" + oncommand="TodayPane.displayMiniSection('none')" + label="&todaypane.showNone.label;" + accesskey="&todaypane.showNone.accesskey;"/> + </menupopup> + </menu> + <menu id="ltnCalendarMenu" + observes="calendar_in_foreground" + insertbefore="viewSortMenuSeparator" + label="&lightning.menu.view.calendar.label;" + accesskey="&lightning.menu.view.calendar.accesskey;"> + <menupopup id="ltnCalendarMenuPopup"> + <menuitem id="ltnChangeViewDay" + label="&lightning.toolbar.day.label;" + accesskey="&lightning.toolbar.day.accesskey;" + type="radio" + name="calendarMenuViews" + key="calendar-day-view-key" + command="calendar_day-view_command"/> + <menuitem id="ltnChangeViewWeek" + label="&lightning.toolbar.week.label;" + accesskey="&lightning.toolbar.week.accesskey;" + type="radio" + name="calendarMenuViews" + key="calendar-week-view-key" + command="calendar_week-view_command"/> + <menuitem id="ltnChangeViewMultiweek" + label="&lightning.toolbar.multiweek.label;" + accesskey="&lightning.toolbar.multiweek.accesskey;" + type="radio" + name="calendarMenuViews" + key="calendar-multiweek-view-key" + command="calendar_multiweek-view_command"/> + <menuitem id="ltnChangeViewMonth" + label="&lightning.toolbar.month.label;" + accesskey="&lightning.toolbar.month.accesskey;" + type="radio" + name="calendarMenuViews" + key="calendar-month-view-key" + command="calendar_month-view_command"/> + <menuseparator id="ltnBeforeCalendarViewSection"/> + <menu id="ltnCalendarPaneMenu" + label="&lightning.toolbar.calendarmenu.label;" + accesskey="&lightning.toolbar.calendarmenu.accesskey;"> + <menupopup id="ltnCalendarPanePopup" + onpopupshowing="InitViewCalendarPaneMenu()"> + <menuitem id="ltnViewCalendarPane" + type="checkbox" + label="&lightning.toolbar.calendarpane.label;" + accesskey="&lightning.toolbar.calendarpane.accesskey;" + command="calendar_toggle_calendarsidebar_command"/> + <menuseparator id="ltnCalendarPaneMenuSeparator"/> + <menuitem id="ltnTasksViewMinimonth" + type="checkbox" + label="&calendar.tasks.view.minimonth.label;" + accesskey="&calendar.tasks.view.minimonth.accesskey;" + command="calendar_toggle_minimonthpane_command"/> + <menuitem id="ltnTasksViewCalendarlist" + type="checkbox" + label="&calendar.tasks.view.calendarlist.label;" + accesskey="&calendar.tasks.view.calendarlist.accesskey;" + command="calendar_toggle_calendarlist_command"/> + </menupopup> + </menu> + <menuseparator id="ltnBeforeCurrentViewMenu"/> + <menu id="ltnCalendarCurrentViewMenu" + observes="calendar_mode_calendar" + label="&showCurrentView.label;" + accesskey="&showCurrentView.accesskey;"> + <menupopup id="ltnCalendarCurrentViewMenuPopup"> + <menuitem type="checkbox" + id="ltnWorkdaysOnlyMenuitem" + label="&calendar.onlyworkday.checkbox.label;" + accesskey="&calendar.onlyworkday.checkbox.accesskey;" + observes="calendar_toggle_workdays_only_command"/> + <menuitem type="checkbox" + id="ltnTasksInViewMenuitem" + label="&calendar.displaytodos.checkbox.label;" + accesskey="&calendar.displaytodos.checkbox.accesskey;" + observes="calendar_toggle_tasks_in_view_command"/> + <menuitem type="checkbox" + id="ltnShowCompletedInViewMenuItem" + label="&calendar.completedtasks.checkbox.label;" + accesskey="&calendar.completedtasks.checkbox.accesskey;" + observes="calendar_toggle_show_completed_in_view_command"/> + <menuitem type="checkbox" + id="ltnViewRotated" + label="&calendar.orientation.label;" + accesskey="&calendar.orientation.accesskey;" + command="calendar_toggle_orientation_command" + observes="calendar_toggle_orientation_command"/> + </menupopup> + </menu> + </menupopup> + </menu> + <menu id="ltnTasksMenu" + observes="calendar_mode_task" + insertbefore="viewSortMenuSeparator" + label="&lightning.menu.view.tasks.label;" + accesskey="&lightning.menu.view.tasks.accesskey;"> + <menupopup id="ltnTasksMenuPopup"> + <observes element="filterBroadcaster" + attribute="value" + onbroadcast="checkRadioControl(this.parentNode, document.getElementById(this.getAttribute('element')).getAttribute('value'));"/> + <menuitem id="ltnTasksViewFilterTasks" + type="checkbox" + label="&calendar.tasks.view.filtertasks.label;" + accesskey="&calendar.tasks.view.filtertasks.accesskey;" + command="calendar_toggle_filter_command"/> + <menuseparator id="ltnTasksViewSeparator"/> + <menuitem id="ltnTasksViewFilterCurrent" + name="filtergroup" + value="throughcurrent" + type="radio" + command="calendar_task_filter_command" + label="&calendar.task.filter.current.label;" + accesskey="&calendar.task.filter.current.accesskey;"/> + <menuitem id="ltnTasksViewFilterToday" + name="filtergroup" + value="throughtoday" + type="radio" + command="calendar_task_filter_command" + label="&calendar.task.filter.today.label;" + accesskey="&calendar.task.filter.today.accesskey;"/> + <menuitem id="ltnTasksViewFilterNext7days" + name="filtergroup" + value="throughsevendays" + type="radio" + command="calendar_task_filter_command" + label="&calendar.task.filter.next7days.label;" + accesskey="&calendar.task.filter.next7days.accesskey;"/> + <menuitem id="ltnTasksViewFilterNotstartedtasks" + name="filtergroup" + value="notstarted" + type="radio" + command="calendar_task_filter_command" + label="&calendar.task.filter.notstarted.label;" + accesskey="&calendar.task.filter.notstarted.accesskey;"/> + <menuitem id="ltnTasksViewFilterOverdue" + name="filtergroup" + value="overdue" + type="radio" + command="calendar_task_filter_command" + label="&calendar.task.filter.overdue.label;" + accesskey="&calendar.task.filter.overdue.accesskey;"/> + <menuitem id="ltnTasksViewFilterCompleted" + name="filtergroup" + type="radio" + value="completed" + command="calendar_task_filter_command" + label="&calendar.task.filter.completed.label;" + accesskey="&calendar.task.filter.completed.accesskey;"/> + <menuitem id="ltnTasksViewFilterOpen" + name="filtergroup" + type="radio" + value="open" + command="calendar_task_filter_command" + label="&calendar.task.filter.open.label;" + accesskey="&calendar.task.filter.open.accesskey;"/> + <menuitem id="ltnTasksViewFilterAll" + name="filtergroup" + value="all" + type="radio" + command="calendar_task_filter_command" + label="&calendar.task.filter.all.label;" + accesskey="&calendar.task.filter.all.accesskey;"/> + </menupopup> + </menu> + </menupopup> + + <menupopup id="menu_GoPopup"> + <menuitem id="ltnGoToToday" + insertafter="goNextSeparator" + label="&goTodayCmd.label;" + accesskey="&goTodayCmd.accesskey;" + observes="calendar_go_to_today_command" + key="calendar-go-to-today-key"/> + </menupopup> + + <menupopup id="menu_GoNextPopup"> + <menuseparator id="ltnGoNextSeparator"/> + <!-- Label is set up automatically using the view id. When writing a + view extension, overlay this menuitem and add a label-<myviewtype> + attribute with the correct label --> + <menuitem id="calendar-go-menu-next" + label="" + label-day="&lightning.toolbar.day.label;" + label-week="&lightning.toolbar.week.label;" + label-multiweek="&lightning.toolbar.week.label;" + label-month="&lightning.toolbar.month.label;" + accesskey-day="&lightning.toolbar.day.accesskey;" + accesskey-week="&lightning.toolbar.week.accesskey;" + accesskey-multiweek="&lightning.toolbar.week.accesskey;" + accesskey-month="&lightning.toolbar.month.accesskey;" + observes="calendar_view_next_command"/> + </menupopup> + + <menupopup id="menu_GoPreviousPopup"> + <menuseparator id="ltnGoPreviousSeparator"/> + <!-- Label is set up automatically using the view id. When writing a + view extension, overlay this menuitem and add a label-<myviewtype> + attribute with the correct label --> + <menuitem id="calendar-go-menu-previous" + label="" + label-day="&lightning.toolbar.day.label;" + label-week="&lightning.toolbar.week.label;" + label-multiweek="&lightning.toolbar.week.label;" + label-month="&lightning.toolbar.month.label;" + accesskey-day="&lightning.toolbar.day.accesskey;" + accesskey-week="&lightning.toolbar.week.accesskey;" + accesskey-multiweek="&lightning.toolbar.week.accesskey;" + accesskey-month="&lightning.toolbar.month.accesskey;" + observes="calendar_view_prev_command"/> + </menupopup> + + <menubar id="mail-menubar"> + <menu id="menu_Event_Task" + label="&lightning.menu.eventtask.label;" + accesskey="&lightning.menu.eventtask.accesskey;" + insertafter="messageMenu"> + <menupopup id="menu_Event_Task_Popup" onpopupshowing="changeMenuForTask(event); setupDeleteMenuitem('ltnDeleteSelectedCalendar')"> + <menuitem id="ltnNewEvent2" + label="&event.new.event;" + accesskey="&event.new.event.accesskey;" + key="calendar-new-event-key" + command="calendar_new_event_command" + observes="calendar_new_event_command"/> + <menuitem id="ltnNewTask2" + label="&event.new.task;" + accesskey="&event.new.task.accesskey;" + key="calendar-new-todo-key" + command="calendar_new_todo_command" + observes="calendar_new_todo_command"/> + <menuseparator id="before-Calendar-Mode-Section"/> + <menuitem id="ltnMenuSwitchToCalendar" + type="checkbox" + label="&lightning.toolbar.calendar.label;" + accesskey="&lightning.toolbar.calendar.accesskey;" + command="switch2calendar" + key="openLightningKey" + autocheck="false"> + <observes element="modeBroadcaster" + attribute="mode" + onbroadcast="this.parentNode.setAttribute('checked', '' + document.getElementById('modeBroadcaster').getAttribute('mode') == 'calendar')"/> + </menuitem> + <menuitem id="ltnMenuSwitchToTask" + type="checkbox" + label="&lightning.toolbar.task.label;" + accesskey="&lightning.toolbar.task.accesskey;" + command="switch2task" + key="openTasksKey" + autocheck="false"> + <observes element="modeBroadcaster" + attribute="mode" + onbroadcast="this.parentNode.setAttribute('checked', '' + document.getElementById('modeBroadcaster').getAttribute('mode') == 'task')"/> + </menuitem> + <menuseparator id="ltnBeforeCalendarSection"/> + <!-- Menuitems have different schema just to match sunbird --> + <menuitem id="calendar-export-menu" + label="&calendar.export.label;" + accesskey="&calendar.export.accesskey;" + command="calendar_export_command" + observes="calendar_export_command"/> + <menuitem id="calendar-import-menu" + label="&calendar.import.label;" + accesskey="&calendar.import.accesskey;" + command="calendar_import_command" + observes="calendar_import_command"/> + <menuitem id="ltnPublishCalendar" + label="&calendar.publish.label;" + accesskey="&calendar.publish.accesskey;" + commmand="calendar_publish_calendar_command" + observes="calendar_publish_calendar_command"/> + <menuitem id="ltnDeleteSelectedCalendar" + labeldelete="&calendar.deletecalendar.label;" + labelremove="&calendar.removecalendar.label;" + labelunsubscribe="&calendar.unsubscribecalendar.label;" + accesskeydelete="&calendar.deletecalendar.accesskey;" + accesskeyremove="&calendar.removecalendar.accesskey;" + accesskeyunsubscribe="&calendar.unsubscribecalendar.accesskey;" + command="calendar_delete_calendar_command" + observes="calendar_delete_calendar_command"/> + <menuseparator id="ltnBeforeTaskActions"/> + <menuitem id="ltnTaskActionsMarkCompletedMenuitem" + type="checkbox" + label="&calendar.context.markcompleted.label;" + accesskey="&calendar.context.markcompleted.accesskey;" + command="calendar_toggle_completed_command" + observes="calendar_toggle_completed_command"/> + <menu id="ltnTaskActionsPriorityMenuitem" + label="&calendar.context.priority.label;" + accesskey="&calendar.context.priority.accesskey;" + command="calendar_general-priority_command" + observes="calendar_general-priority_command"> + <menupopup type="task-priority"/> + </menu> + <menu id="ltnTaskActionsProgressMenuitem" + label="&calendar.context.progress.label;" + accesskey="&calendar.context.progress.accesskey;" + command="calendar_general-progress_command" + observes="calendar_general-progress_command"> + <menupopup type="task-progress"/> + </menu> + <menu id="ltnTaskActionsPostponeMenuitem" + label="&calendar.context.postpone.label;" + accesskey="&calendar.context.postpone.accesskey;" + observes="calendar_general-postpone_command"> + <menupopup id="ltnTaskActionsPostponeMenuPopup"> + <menuitem id="ltnTaskActionsPostponeMenu-1hour" + label="&calendar.context.postpone.1hour.label;" + accesskey="&calendar.context.postpone.1hour.accesskey;" + observes="calendar_postpone-1hour_command"/> + <menuitem id="ltnTaskActionsPostponeMenu-1day" + label="&calendar.context.postpone.1day.label;" + accesskey="&calendar.context.postpone.1day.accesskey;" + observes="calendar_postpone-1day_command"/> + <menuitem id="ltnTaskActionsPostponeMenu-1week" + label="&calendar.context.postpone.1week.label;" + accesskey="&calendar.context.postpone.1week.accesskey;" + observes="calendar_postpone-1week_command"/> + </menupopup> + </menu> + <menuseparator id="ltnBeforeUnifinderSection" /> + <!-- menuitem has different schema just to match sunbird --> + <menuitem id="calendar-show-unifinder-menu" + type="checkbox" + checked="true" + label="&showUnifinderCmd.label;" + accesskey="&showUnifinderCmd.accesskey;" + observes="calendar_show_unifinder_command" + command="calendar_show_unifinder_command"/> + </menupopup> + </menu> + </menubar> + + <menupopup id="mailContext"> + <menu id="mailContext-calendar-convert-menu" + insertafter="mailContext-moveToFolderAgain" + label="&calendar.context.convertmenu.label;" + accesskey="&calendar.context.convertmenu.accesskey.mail;"> + <menupopup id="mailContext-calendar-convert-menupopup"> + <menuitem id="mailContext-calendar-convert-event-menuitem" + label="&calendar.context.convertmenu.event.label;" + accesskey="&calendar.context.convertmenu.event.accesskey;" + oncommand="calendarExtract.extractFromEmail(true)" /> + <menuitem id="mailContext-calendar-convert-task-menuitem" + label="&calendar.context.convertmenu.task.label;" + accesskey="&calendar.context.convertmenu.task.accesskey;" + oncommand="calendarExtract.extractFromEmail(false)" /> + </menupopup> + </menu> + </menupopup> + + <menupopup id="toolbar-context-menu" + onpopupshowing="onToolbarsPopupShowingForTabType(event);"/> + +<!-- AppMenu integration --> + <menupopup id="appmenu_newMenupopup"> + <menuitem id="appmenu_ltnNewEvent" + label="&lightning.menupopup.new.event.label;" + insertbefore="appmenu_newFolder" + command="calendar_new_event_command" + observes="calendar_new_event_command"/> + <menuitem id="appmenu_ltnNewTask" + label="&lightning.menupopup.new.task.label;" + insertbefore="appmenu_newFolder" + command="calendar_new_todo_command" + observes="calendar_new_todo_command"/> + <menuseparator id="appmenu_afterltnNewTask" + insertbefore="appmenu_newFolder" + observes="appmenu_newFolder"/> + <menuitem id="appmenu_ltnNewCalendar" label="&lightning.menupopup.new.calendar.label;" + command="calendar_new_calendar_command" + observes="calendar_new_calendar_command" + insertafter="appmenu_newAccountMenuItem"/> + </menupopup> + <splitmenu id="appmenu_customize"> + <menupopup id="appmenu_customizeMenu" + onpopupshowing="onToolbarsPopupShowingForTabType(event, document.getElementById('appmenu_quickFilterBar'));"/> + </splitmenu> + <menupopup id="appmenu_FilePopup"> + <menu id="appmenu_Open" + mode="calendar" + label="&lightning.menupopup.open.label;" + accesskey="&lightning.menupopup.open.accesskey;" + insertbefore="appmenu_openMessageFileMenuitem"> + <menupopup id="appmenu_OpenPopup"> + <menuitem id="appmenu_OpenMessageFileMenuitem" + label="&lightning.menupopup.open.message.label;" + accesskey="&lightning.menupopup.open.message.accesskey;" + oncommand="MsgOpenFromFile();"/> + <menuitem id="appmenu_OpenCalendarFileMenuitem" + label="&lightning.menupopup.open.calendar.label;" + accesskey="&lightning.menupopup.open.calendar.accesskey;" + oncommand="openLocalCalendar();"/> + </menupopup> + </menu> + </menupopup> + <menuitem id="appmenu_openMessageFileMenuitem" hidden="true"/> + <menupopup id="appmenu_View_Popup"> + <menuseparator id="appmenu_ltnViewMenuSeparator"/> + <menu id="appmenu_ltnTodayPaneMenu" + label="&calendar.context.button.label;"> + <menupopup id="appmenu_ltnTodayPaneMenuPopup"> + <menuitem id="appmenu_ltnShowTodayPane-2" + label="&todaypane.showTodayPane.label;" + type="checkbox" + command="calendar_toggle_todaypane_command"/> + <menuseparator id="appmenu_ltnSeparatorBeforeDisplayMiniday"/> + <menuitem id="appmenu_ltnTodayPaneDisplayMiniday" + name="minidisplay" + value="miniday" + type="radio" + oncommand="TodayPane.displayMiniSection('miniday')" + observes="ltnTodayPaneDisplayMiniday" + label="&todaypane.showMiniday.label;"/> + <menuitem id="appmenu_ltnTodayPaneDisplayMinimonth" + name="minidisplay" + value="minimonth" + type="radio" + oncommand="TodayPane.displayMiniSection('minimonth')" + observes="ltnTodayPaneDisplayMinimonth" + label="&todaypane.showMinimonth.label;"/> + <menuitem id="appmenu_ltnTodayPaneDisplayNone" + name="minidisplay" + value="none" + type="radio" + oncommand="TodayPane.displayMiniSection('none')" + observes="ltnTodayPaneDisplayNone" + label="&todaypane.showNone.label;"/> + </menupopup> + </menu> + <menu id="appmenu_ltnCalendarMenu" + observes="calendar_in_foreground" + insertbefore="viewSortMenuSeparator" + label="&lightning.menu.view.calendar.label;"> + <menupopup id="appmenu_ltnCalendarMenuPopup"> + <menuitem id="appmenu_ltnChangeViewDay" + label="&lightning.toolbar.day.label;" + type="radio" + name="calendarMenuViews" + command="calendar_day-view_command"/> + <menuitem id="appmenu_ltnChangeViewWeek" + label="&lightning.toolbar.week.label;" + type="radio" + name="calendarMenuViews" + command="calendar_week-view_command"/> + <menuitem id="appmenu_ltnChangeViewMultiweek" + label="&lightning.toolbar.multiweek.label;" + type="radio" + name="calendarMenuViews" + command="calendar_multiweek-view_command"/> + <menuitem id="appmenu_ltnChangeViewMonth" + label="&lightning.toolbar.month.label;" + type="radio" + name="calendarMenuViews" + command="calendar_month-view_command"/> + <menuseparator id="appmenu_ltnBeforeCalendarViewSection"/> + <menu id="appmenu_ltnCalendarPaneMenu" + label="&lightning.toolbar.calendarmenu.label;"> + <menupopup id="appmenu_ltnCalendarPanePopup" + onpopupshowing="InitViewCalendarPaneMenu()"> + <menuitem id="appmenu_ltnViewCalendarPane" + type="checkbox" + label="&lightning.toolbar.calendarpane.label;" + command="calendar_toggle_calendarsidebar_command"/> + <menuseparator id="appmenu_ltnCalendarPaneMenuSeparator"/> + <menuitem id="appmenu_ltnTasksViewMinimonth" + type="checkbox" + label="&calendar.tasks.view.minimonth.label;" + command="calendar_toggle_minimonthpane_command"/> + <menuitem id="appmenu_ltnTasksViewCalendarlist" + type="checkbox" + label="&calendar.tasks.view.calendarlist.label;" + command="calendar_toggle_calendarlist_command"/> + </menupopup> + </menu> + <menuseparator id="appmenu_ltnBeforeCurrentViewMenu"/> + <menu id="appmenu_ltnCalendarCurrentViewMenu" + observes="calendar_mode_calendar" + label="&showCurrentView.label;"> + <menupopup id="appmenu_ltnCalendarCurrentViewMenuPopup"> + <menuitem type="checkbox" + id="appmenu_ltnWorkdaysOnlyMenuitem" + label="&calendar.onlyworkday.checkbox.label;" + observes="calendar_toggle_workdays_only_command"/> + <menuitem type="checkbox" + id="appmenu_ltnTasksInViewMenuitem" + label="&calendar.displaytodos.checkbox.label;" + observes="calendar_toggle_tasks_in_view_command"/> + <menuitem type="checkbox" + id="appmenu_ltnShowCompletedInViewMenuItem" + label="&calendar.completedtasks.checkbox.label;" + observes="calendar_toggle_show_completed_in_view_command"/> + <menuitem type="checkbox" + id="appmenu_ltnViewRotated" + label="&calendar.orientation.label;" + command="calendar_toggle_orientation_command" + observes="calendar_toggle_orientation_command"/> + </menupopup> + </menu> + </menupopup> + </menu> + <menu id="appmenu_ltnTasksMenu" + observes="calendar_mode_task" + insertbefore="viewSortMenuSeparator" + label="&lightning.menu.view.tasks.label;"> + <menupopup id="appmenu_ltnTasksMenuPopup"> + <observes element="filterBroadcaster" + attribute="value" + onbroadcast="checkRadioControl(this.parentNode, document.getElementById(this.getAttribute('element')).getAttribute('value'));"/> + <menuitem id="appmenu_ltnTasksViewFilterTasks" + type="checkbox" + label="&calendar.tasks.view.filtertasks.label;" + command="calendar_toggle_filter_command"/> + <menuseparator id="appmenu_ltnTasksViewSeparator"/> + <menuitem id="appmenu_ltnTasksViewFilterCurrent" + name="filtergroup" + value="throughcurrent" + type="radio" + command="calendar_task_filter_command" + label="&calendar.task.filter.current.label;"/> + <menuitem id="appmenu_ltnTasksViewFilterToday" + name="filtergroup" + value="throughtoday" + type="radio" + command="calendar_task_filter_command" + label="&calendar.task.filter.today.label;"/> + <menuitem id="appmenu_ltnTasksViewFilterNext7days" + name="filtergroup" + value="throughsevendays" + type="radio" + command="calendar_task_filter_command" + label="&calendar.task.filter.next7days.label;"/> + <menuitem id="appmenu_ltnTasksViewFilterNotstartedtasks" + name="filtergroup" + value="notstarted" + type="radio" + command="calendar_task_filter_command" + label="&calendar.task.filter.notstarted.label;"/> + <menuitem id="appmenu_ltnTasksViewFilterOverdue" + name="filtergroup" + value="overdue" + type="radio" + command="calendar_task_filter_command" + label="&calendar.task.filter.overdue.label;"/> + <menuitem id="appmenu_ltnTasksViewFilterCompleted" + name="filtergroup" + type="radio" + value="completed" + command="calendar_task_filter_command" + label="&calendar.task.filter.completed.label;"/> + <menuitem id="appmenu_ltnTasksViewFilterOpen" + name="filtergroup" + type="radio" + value="open" + command="calendar_task_filter_command" + label="&calendar.task.filter.open.label;"/> + <menuitem id="appmenu_ltnTasksViewFilterAll" + name="filtergroup" + value="all" + type="radio" + command="calendar_task_filter_command" + label="&calendar.task.filter.all.label;"/> + </menupopup> + </menu> + </menupopup> + + <menupopup id="appmenu_GoPopup"> + <menuitem id="appmenu_ltnGoToToday" + insertafter="appmenu_goNextSeparator" + label="&goTodayCmd.label;" + observes="calendar_go_to_today_command"/> + </menupopup> + + <menupopup id="appmenu_GoNextPopup"> + <menuseparator id="appmenu_ltnGoNextSeparator"/> + <!-- Label is set up automatically using the view id. When writing a + view extension, overlay this menuitem and add a label-<myviewtype> + attribute with the correct label --> + <menuitem id="appmenu_calendar-go-menu-next" + label="" + label-day="&lightning.toolbar.day.label;" + label-week="&lightning.toolbar.week.label;" + label-multiweek="&lightning.toolbar.week.label;" + label-month="&lightning.toolbar.month.label;" + observes="calendar_view_next_command"/> + </menupopup> + <menupopup id="appmenu_GoPreviousPopup"> + <menuseparator id="appmenu_ltnGoPreviousSeparator"/> + <!-- Label is set up automatically using the view id. When writing a + view extension, overlay this menuitem and add a label-<myviewtype> + attribute with the correct label --> + <menuitem id="appmenu_calendar-go-menu-previous" + label="" + label-day="&lightning.toolbar.day.label;" + label-week="&lightning.toolbar.week.label;" + label-multiweek="&lightning.toolbar.week.label;" + label-month="&lightning.toolbar.month.label;" + observes="calendar_view_prev_command"/> + </menupopup> + + <vbox id="appmenuSecondaryPane"> + <menu id="appmenu_Event_Task" + label="&lightning.menu.eventtask.label;" + insertafter="appmenu_messageMenu"> + <menupopup id="appmenu_Event_Task_Popup" onpopupshowing="changeMenuForTask(event); setupDeleteMenuitem('appmenu_ltnDeleteSelectedCalendar')"> + <menuitem id="appmenu_ltnMenuSwitchToCalendar" + type="checkbox" + label="&lightning.toolbar.calendar.label;" + command="switch2calendar" + autocheck="false"> + <observes element="modeBroadcaster" + attribute="mode" + onbroadcast="this.parentNode.setAttribute('checked', '' + document.getElementById('modeBroadcaster').getAttribute('mode') == 'calendar')"/> + </menuitem> + <menuitem id="appmenu_ltnMenuSwitchToTask" + type="checkbox" + label="&lightning.toolbar.task.label;" + command="switch2task" + autocheck="false"> + <observes element="modeBroadcaster" + attribute="mode" + onbroadcast="this.parentNode.setAttribute('checked', '' + document.getElementById('modeBroadcaster').getAttribute('mode') == 'task')"/> + </menuitem> + <menuseparator id="appmenu_ltnBeforeCalendarSection"/> + <!-- Menuitems have different schema just to match sunbird --> + <menuitem id="appmenu_calendar-export-menu" + label="&calendar.export.label;" + command="calendar_export_command" + observes="calendar_export_command"/> + <menuitem id="appmenu_calendar-import-menu" + label="&calendar.import.label;" + command="calendar_import_command" + observes="calendar_import_command"/> + <menuitem id="appmenu_ltnPublishCalendar" + label="&calendar.publish.label;" + commmand="calendar_publish_calendar_command" + observes="calendar_publish_calendar_command"/> + <menuitem id="appmenu_ltnDeleteSelectedCalendar" + labeldelete="&calendar.deletecalendar.label;" + labelremove="&calendar.removecalendar.label;" + labelunsubscribe="&calendar.unsubscribecalendar.label;" + accesskeydelete="&calendar.deletecalendar.accesskey;" + accesskeyremove="&calendar.removecalendar.accesskey;" + accesskeyunsubscribe="&calendar.unsubscribecalendar.accesskey;" + command="calendar_delete_calendar_command" + observes="calendar_delete_calendar_command"/> + <menuseparator id="ltnBeforeTaskActions"/> + <menuitem id="appmenu_ltnTaskActionsMarkCompletedMenuitem" + type="checkbox" + label="&calendar.context.markcompleted.label;" + command="calendar_toggle_completed_command" + observes="calendar_toggle_completed_command"/> + <menu id="appmenu_ltnTaskActionsPriorityMenuitem" + label="&calendar.context.priority.label;" + command="calendar_general-priority_command" + observes="calendar_general-priority_command"> + <menupopup type="task-priority"/> + </menu> + <menu id="appmenu_ltnTaskActionsProgressMenuitem" + label="&calendar.context.progress.label;" + command="calendar_general-progress_command" + observes="calendar_general-progress_command"> + <menupopup type="task-progress"/> + </menu> + <menu id="appmenu_ltnTaskActionsPostponeMenuitem" + label="&calendar.context.postpone.label;" + observes="calendar_general-postpone_command"> + <menupopup id="appmenu_ltnTaskActionsPostponeMenuPopup"> + <menuitem id="ltnTaskActionsPostponeMenu-1hour" + label="&calendar.context.postpone.1hour.label;" + observes="calendar_postpone-1hour_command"/> + <menuitem id="appmenu_ltnTaskActionsPostponeMenu-1day" + label="&calendar.context.postpone.1day.label;" + observes="calendar_postpone-1day_command"/> + <menuitem id="appmenu_ltnTaskActionsPostponeMenu-1week" + label="&calendar.context.postpone.1week.label;" + observes="calendar_postpone-1week_command"/> + </menupopup> + </menu> + <menuseparator id="appmenu_ltnBeforeUnifinderSection" /> + <!-- menuitem has different schema just to match sunbird --> + <menuitem id="appmenu_calendar-show-unifinder-menu" + type="checkbox" + checked="true" + label="&showUnifinderCmd.label;" + observes="calendar_show_unifinder_command" + command="calendar_show_unifinder_command"/> + <menuseparator id="appmenu_ltnBeforeCalendarProperties" /> + <menuitem id="appmenu_ltnCalendarProperties" + insertafter="menu_properties" + label="&calendar.properties.label;" + command="calendar_edit_calendar_command" + observes="calendar_edit_calendar_command"/> + </menupopup> + </menu> + </vbox> +</overlay> diff --git a/calendar/lightning/content/lightning-migration.xul b/calendar/lightning/content/lightning-migration.xul new file mode 100644 index 000000000..201c39661 --- /dev/null +++ b/calendar/lightning/content/lightning-migration.xul @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- 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/. --> + +<!-- The old calendar extension, if it is installed in the same profile as + - Lightning, will break Lightning because it ships several files that + - have the same chrome address as files that Lightning ships. This file + - exists so we can check for whether that extension is installed and nuke it + - in that case. Note that this check *cannot* be done in any file that may + - die as a result of the conflict (including messanger-overlay-sidebar.js). + - Nor can it depend on files which may conflict. + --> + +<!-- DTD File with all strings specific to the file --> +<!DOCTYPE overlay +[ +]> + +<overlay id="ltnMigrationOverlay" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script type="application/javascript" src="chrome://calendar/content/calendar-migration-dialog.js"/> + <script type="application/javascript"><![CDATA[ + function checkOld() { + window.removeEventListener("load", checkOld, false); + var calMgr = Components.classes["@mozilla.org/calendar/manager;1"] + .getService(Components.interfaces.calICalendarManager); + var cals = calMgr.getCalendars({}); + if (!cals.length) { + // There are no calendars, so we are running for the first time + gDataMigrator.checkAndMigrate(); + } + } + window.addEventListener("load", checkOld, false); + ]]></script> + + <deck id="calendarDisplayDeck"/> + +</overlay> diff --git a/calendar/lightning/content/lightning-toolbar.xul b/calendar/lightning/content/lightning-toolbar.xul new file mode 100644 index 000000000..3dda61880 --- /dev/null +++ b/calendar/lightning/content/lightning-toolbar.xul @@ -0,0 +1,223 @@ +<?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/. --> + +<!DOCTYPE overlay [ + <!ENTITY % messengerDTD SYSTEM "chrome://messenger/locale/messenger.dtd"> %messengerDTD; + <!ENTITY % mailOverlayDTD SYSTEM "chrome://messenger/locale/mailOverlay.dtd"> %mailOverlayDTD; + <!ENTITY % menuOverlayDTD SYSTEM "chrome://calendar/locale/menuOverlay.dtd" > %menuOverlayDTD; + <!ENTITY % lightningDTD SYSTEM "chrome://lightning/locale/lightning.dtd"> %lightningDTD; + <!ENTITY % calendarDTD SYSTEM "chrome://calendar/locale/calendar.dtd" > %calendarDTD; + <!ENTITY % toolbarDTD SYSTEM "chrome://lightning/locale/lightning-toolbar.dtd" > %toolbarDTD; + <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" > %brandDTD; +]> + +<?xml-stylesheet href="chrome://lightning/skin/lightning-toolbar.css" type="text/css"?> + +<overlay id="ltnToolbarOverlay" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <popupset id="calendar-popupset"> + <menupopup id="calendar-toolbar-context" + onpopupshowing="onViewToolbarsPopupShowing(event, ['navigation-toolbox', 'calendar-toolbox']);"> + <menuseparator id="customizeCalendarToolbarMenuSeparator"/> + <menuitem id="CustomizeCalendarToolbar" + label="&calendar.menu.customize.label;" + accesskey="&calendar.menu.customize.accesskey;" + oncommand="CustomizeMailToolbar('calendar-toolbox', 'CustomizeCalendarToolbar')"/> + </menupopup> + <menupopup id="task-toolbar-context" + onpopupshowing="onViewToolbarsPopupShowing(event, ['navigation-toolbox', 'task-toolbox']);"> + <menuseparator id="customizeTaskToolbarMenuSeparator"/> + <menuitem id="CustomizeTaskToolbar" + label="&calendar.menu.customize.label;" + accesskey="&calendar.menu.customize.accesskey;" + oncommand="CustomizeMailToolbar('task-toolbox', 'CustomizeTaskToolbar')"/> + </menupopup> + </popupset> + + <!-- The popup id here must match the popup id in the SeaMonkey + New Message button. See Bug 506461 --> + <toolbarbutton id="button-newmsg" + type="menu-button"> + <menupopup id="button-newMsgPopup"> + <menuitem id="newMsgButton-mail-menuitem" + label="&newMessageCmd.label;" + class="menuitem-iconic" + oncommand="event.stopPropagation(); MsgNewMessage(event)"/> + <menuitem id="newMsgButton-calendar-menuitem" + label="&lightning.toolbar.newevent.label;" + class="menuitem-iconic" + command="calendar_new_event_command" + observes="calendar_new_event_command"/> + <menuitem id="newMsgButton-task-menuitem" + label="&lightning.toolbar.newtask.label;" + class="menuitem-iconic" + command="calendar_new_todo_command" + observes="calendar_new_todo_command"/> + </menupopup> + </toolbarbutton> + + <toolbarpalette id="MailToolbarPalette"> + <toolbarbutton id="lightning-button-calendar" + class="toolbarbutton-1" + label="&lightning.toolbar.calendar.label;" + tooltiptext="&lightning.toolbar.calendar.tooltip;" + command="new_calendar_tab"/> + <toolbarbutton id="lightning-button-tasks" + class="toolbarbutton-1" + label="&lightning.toolbar.task.label;" + tooltiptext="&lightning.toolbar.task.tooltip;" + command="new_task_tab"/> + <toolbarbutton id="extractEventButton" + class="toolbarbutton-1" + type="menu-button" + label="&calendar.extract.event.button;" + tooltiptext="&calendar.extract.event.button.tooltip;" + oncommand="calendarExtract.extractFromEmail(true);"> + <menupopup id="extractEventLocaleList" + oncommand="calendarExtract.extractWithLocale(event, true);" + onpopupshowing="calendarExtract.onShowLocaleMenu(event.target);"/> + </toolbarbutton> + <toolbarbutton id="extractTaskButton" + class="toolbarbutton-1" + type="menu-button" + label="&calendar.extract.task.button;" + tooltiptext="&calendar.extract.task.button.tooltip;" + oncommand="calendarExtract.extractFromEmail(false);"> + <menupopup id="extractTaskLocaleList" + oncommand="calendarExtract.extractWithLocale(event, false);" + onpopupshowing="calendarExtract.onShowLocaleMenu(event.target);"/> + </toolbarbutton> + </toolbarpalette> + + <toolbarpalette id="header-view-toolbar-palette"> + <toolbarbutton id="hdrExtractEventButton" + class="toolbarbutton-1 msgHeaderView-button" + type="menu-button" + label="&calendar.extract.event.button;" + tooltiptext="&calendar.extract.event.button.tooltip;" + oncommand="calendarExtract.extractFromEmail(true)"> + <menupopup id="hdrExtractEventLocaleList" + oncommand="calendarExtract.extractWithLocale(event, true);" + onpopupshowing="calendarExtract.onShowLocaleMenu(event.target);"/> + </toolbarbutton> + <toolbarbutton id="hdrExtractTaskButton" + class="toolbarbutton-1 msgHeaderView-button" + type="menu-button" + label="&calendar.extract.task.button;" + tooltiptext="&calendar.extract.task.button.tooltip;" + oncommand="calendarExtract.extractFromEmail(false)"> + <menupopup id="hdrExtractTaskLocaleList" + oncommand="calendarExtract.extractWithLocale(event, false);" + onpopupshowing="calendarExtract.onShowLocaleMenu(event.target);"/> + </toolbarbutton> + </toolbarpalette> + + <toolbox id="calendar-toolbox" + class="mail-toolbox" + mode="full" + defaultmode="full" + labelalign="end" + defaultlabelalign="end"> + <toolbarpalette id="CalendarToolbarPalette"> + <toolbarbutton id="calendar-synchronize-button" + class="toolbarbutton-1 calbar-toolbarbutton-1" + label="&lightning.toolbar.sync.label;" + tooltiptext="&lightning.toolbar.sync.tooltip;" + observes="calendar_reload_remote_calendars"/> + <toolbarbutton id="calendar-newevent-button" + class="toolbarbutton-1 calbar-toolbarbutton-1" + label="&lightning.toolbar.newevent.label;" + tooltiptext="&lightning.toolbar.newevent.tooltip;" + observes="calendar_new_event_command"/> + <toolbarbutton id="calendar-newtask-button" + class="toolbarbutton-1 calbar-toolbarbutton-1" + label="&lightning.toolbar.newtask.label;" + tooltiptext="&lightning.toolbar.newtask.tooltip;" + observes="calendar_new_todo_command"/> + <toolbarbutton id="calendar-goto-today-button" + class="toolbarbutton-1 calbar-toolbarbutton-1" + label="&lightning.toolbar.gototoday.label;" + tooltiptext="&lightning.toolbar.gototoday.tooltip;" + observes="calendar_go_to_today_command"/> + <toolbarbutton id="calendar-edit-button" + class="toolbarbutton-1 calbar-toolbarbutton-1" + label="&lightning.toolbar.edit.label;" + tooltiptext="&lightning.toolbar.edit.tooltip;" + observes="calendar_modify_focused_item_command"/> + <toolbarbutton id="calendar-delete-button" + class="toolbarbutton-1 calbar-toolbarbutton-1" + label="&lightning.toolbar.delete.label;" + tooltiptext="&lightning.toolbar.delete.tooltip;" + observes="calendar_delete_focused_item_command"/> + <toolbarbutton id="calendar-print-button" + class="toolbarbutton-1 calbar-toolbarbutton-1" + label="&lightning.toolbar.print.label;" + tooltiptext="&lightning.toolbar.print.tooltip;" + observes="cmd_print"/> + <toolbarbutton id="calendar-unifinder-button" + class="toolbarbutton-1 calbar-toolbarbutton-1" + label="&showUnifinderCmd.label;" + tooltiptext="&showUnifinderCmd.tooltip;" + observes="calendar_show_unifinder_command"/> + </toolbarpalette> + + <toolbar id="calendar-toolbar2" class="inline-toolbar chromeclass-toolbar" + toolbarname="&lightning.toolbar.calendar.name;" + accesskey="&lightning.toolbar.calendar.name.accesskey;" + fullscreentoolbar="true" mode="full" + customizable="true" + context="calendar-toolbar-context" + defaultset="calendar-synchronize-button,calendar-newevent-button,calendar-newtask-button,calendar-edit-button,calendar-delete-button,spring"/> + <toolbarset id="calendarToolbars" context="calendar-toolbar-context"/> + </toolbox> + + <toolbox id="task-toolbox" + class="mail-toolbox" + mode="full" + defaultmode="full" + labelalign="end" + defaultlabelalign="end"> + <toolbarpalette id="TaskToolbarPalette"> + <toolbarbutton id="task-synchronize-button" + class="toolbarbutton-1 calbar-toolbarbutton-1" + label="&lightning.toolbar.sync.label;" + tooltiptext="&lightning.toolbar.sync.tooltip;" + observes="calendar_reload_remote_calendars"/> + <toolbarbutton id="task-newevent-button" + class="toolbarbutton-1 calbar-toolbarbutton-1" + label="&lightning.toolbar.newevent.label;" + tooltiptext="&lightning.toolbar.newevent.tooltip;" + observes="calendar_new_event_command"/> + <toolbarbutton id="task-newtask-button" + class="toolbarbutton-1 calbar-toolbarbutton-1" + label="&lightning.toolbar.newtask.label;" + tooltiptext="&lightning.toolbar.newtask.tooltip;" + observes="calendar_new_todo_command"/> + <toolbarbutton id="task-edit-button" + class="toolbarbutton-1 calbar-toolbarbutton-1" + label="&lightning.toolbar.edit.label;" + tooltiptext="&lightning.toolbar.edit.tooltip;" + observes="calendar_modify_focused_item_command"/> + <toolbarbutton id="task-delete-button" + class="toolbarbutton-1 calbar-toolbarbutton-1" + label="&lightning.toolbar.delete.label;" + tooltiptext="&lightning.toolbar.delete.tooltip;" + observes="calendar_delete_focused_item_command"/> + <toolbarbutton id="task-print-button" + class="toolbarbutton-1 calbar-toolbarbutton-1" + label="&lightning.toolbar.print.label;" + tooltiptext="&lightning.toolbar.print.tooltip;" + observes="cmd_print"/> + </toolbarpalette> + + <toolbar id="task-toolbar2" class="inline-toolbar chromeclass-toolbar" + toolbarname="&lightning.toolbar.task.name;" + accesskey="&lightning.toolbar.task.name.accesskey;" + fullscreentoolbar="true" mode="full" + customizable="true" + context="task-toolbar-context" + defaultset="task-synchronize-button,task-newevent-button,task-newtask-button,task-edit-button,task-delete-button,spring"/> + <toolbarset id="taskToolbars" context="task-toolbar-context"/> + </toolbox> +</overlay> diff --git a/calendar/lightning/content/lightning-utils.js b/calendar/lightning/content/lightning-utils.js new file mode 100644 index 000000000..61383a3d3 --- /dev/null +++ b/calendar/lightning/content/lightning-utils.js @@ -0,0 +1,86 @@ +/* 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/. */ + +/* exported ltnInitMailIdentitiesRow, ltnSaveMailIdentitySelection */ + +Components.utils.import("resource:///modules/iteratorUtils.jsm"); +Components.utils.import("resource:///modules/mailServices.js"); +Components.utils.import("resource://calendar/modules/calUtils.jsm"); + +/** + * Gets the value of a string in a .properties file from the lightning bundle + * + * @param aBundleName the name of the properties file. It is assumed that the + * file lives in chrome://lightning/locale/ + * @param aStringName the name of the string within the properties file + * @param aParams optional array of parameters to format the string + */ +function ltnGetString(aBundleName, aStringName, aParams) { + return cal.calGetString(aBundleName, aStringName, aParams, "lightning"); +} + +// shared by lightning-calendar-properties.js and lightning-calendar-creation.js: +function ltnInitMailIdentitiesRow() { + if (!gCalendar) { + collapseElement("calendar-email-identity-row"); + } + + let imipIdentityDisabled = gCalendar.getProperty("imip.identity.disabled"); + setElementValue("calendar-email-identity-row", + imipIdentityDisabled && "true", + "collapsed"); + + if (imipIdentityDisabled) { + // If the imip identity is disabled, we don't have to set up the + // menulist. + return; + } + + // If there is no transport but also no organizer id, then the + // provider has not statically configured an organizer id. This is + // basically what happens when "None" is selected. + let menuPopup = document.getElementById("email-identity-menupopup"); + + // Remove all children from the email list to avoid duplicates if the list + // has already been populated during a previous step in the calendar + // creation wizard. + while (menuPopup.hasChildNodes()) { + menuPopup.lastChild.remove(); + } + + addMenuItem(menuPopup, ltnGetString("lightning", "imipNoIdentity"), "none"); + let identities; + if (gCalendar && gCalendar.aclEntry && gCalendar.aclEntry.hasAccessControl) { + identities = gCalendar.aclEntry.getOwnerIdentities({}); + } else { + identities = MailServices.accounts.allIdentities; + } + for (let identity in fixIterator(identities, Components.interfaces.nsIMsgIdentity)) { + addMenuItem(menuPopup, identity.identityName, identity.key); + } + try { + let sel = gCalendar.getProperty("imip.identity"); + if (sel) { + sel = sel.QueryInterface(Components.interfaces.nsIMsgIdentity); + } + menuListSelectItem("email-identity-menulist", sel ? sel.key : "none"); + } catch (exc) { + // Don't select anything if the message identity can't be found + } +} + +function ltnSaveMailIdentitySelection() { + if (!gCalendar) { + return; + } + let sel = "none"; + let imipIdentityDisabled = gCalendar.getProperty("imip.identity.disabled"); + let selItem = document.getElementById("email-identity-menulist").selectedItem; + if (!imipIdentityDisabled && selItem) { + sel = selItem.getAttribute("value"); + } + // no imip.identity.key will default to the default account/identity, whereas + // an empty key indicates no imip; that identity will not be found + gCalendar.setProperty("imip.identity.key", sel == "none" ? "" : sel); +} diff --git a/calendar/lightning/content/lightning-widgets.css b/calendar/lightning/content/lightning-widgets.css new file mode 100644 index 000000000..f641b24c2 --- /dev/null +++ b/calendar/lightning/content/lightning-widgets.css @@ -0,0 +1,11 @@ +/* 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/. */ + +lightning-notification-bar { + -moz-binding: url(chrome://lightning/content/lightning-widgets.xml#lightning-notification-bar); +} + +#calendar-task-tree-detail > calendar-task-tree { + -moz-binding: url(chrome://calendar/content/calendar-task-tree.xml#calendar-task-tree-todaypane); +} diff --git a/calendar/lightning/content/lightning-widgets.xml b/calendar/lightning/content/lightning-widgets.xml new file mode 100644 index 000000000..9c3070b16 --- /dev/null +++ b/calendar/lightning/content/lightning-widgets.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- 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="lightning-widgets" + xmlns="http://www.mozilla.org/xbl" + xmlns:xbl="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <binding id="lightning-notification-bar" extends="xul:hbox"> + <resources> + <stylesheet src="chrome://lightning/skin/lightning-widgets.css"/> + </resources> + <content pack="center" align="center"> + <xul:image anonid="notification-image"/> + <xul:description anonid="notification-description" + class="msgNotificationBarText" + flex="1" + xbl:inherits="xbl:text=label"/> + <xul:box anonid="notification-children"> + <children/> + </xul:box> + </content> + </binding> +</bindings> diff --git a/calendar/lightning/content/lightning.js b/calendar/lightning/content/lightning.js new file mode 100644 index 000000000..2cc01f311 --- /dev/null +++ b/calendar/lightning/content/lightning.js @@ -0,0 +1,170 @@ +/* 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/. */ + +// This file contains all of the default preference values for Lightning + +// Turns on basic calendar logging. +pref("calendar.debug.log", false); +// Turns on verbose calendar logging. +pref("calendar.debug.log.verbose", false); + +// addon description +pref("extensions.{e2fda1a4-762b-4020-b5ad-a41df1933103}.description", + "chrome://lightning/locale/lightning.properties"); +pref("extensions.{e2fda1a4-762b-4020-b5ad-a41df1933103}.name", + "chrome://lightning/locale/lightning.properties"); +pref("extensions.{e2fda1a4-762b-4020-b5ad-a41df1933103}.creator", + "chrome://lightning/locale/lightning.properties"); + +// general settings +pref("calendar.date.format", 0); +pref("calendar.event.defaultlength", 60); +pref("calendar.task.defaultstart", "none"); +pref("calendar.task.defaultstartoffset", 0); +pref("calendar.task.defaultstartoffsetunits", "minutes"); +pref("calendar.task.defaultdue", "none"); +pref("calendar.task.defaultdueoffset", 60); +pref("calendar.task.defaultdueoffsetunits", "minutes"); + +// default transparency (free-busy status) of standard and all-day events +pref("calendar.events.defaultTransparency.allday.transparent", true); +pref("calendar.events.defaultTransparency.standard.transparent", false); + +// number of days in "Soon" section +pref("calendar.agendaListbox.soondays", 5); + +// alarm settings +pref("calendar.alarms.show", true); +pref("calendar.alarms.showmissed", true); +pref("calendar.alarms.playsound", true); +pref("calendar.alarms.soundURL", "chrome://calendar/content/sound.wav"); +pref("calendar.alarms.defaultsnoozelength", 5); +pref("calendar.alarms.indicator.show", true); +pref("calendar.alarms.indicator.totaltime", 3600); + +// default alarm settings for new event +pref("calendar.alarms.onforevents", 0); +pref("calendar.alarms.eventalarmlen", 15); +pref("calendar.alarms.eventalarmunit", "minutes"); + +// default alarm settings for new task +pref("calendar.alarms.onfortodos", 0); +pref("calendar.alarms.todoalarmlen", 15); +pref("calendar.alarms.todoalarmunit", "minutes"); + +// open invitations autorefresh settings +pref("calendar.invitations.autorefresh.enabled", true); +pref("calendar.invitations.autorefresh.timeout", 3); + +// iTIP compatibility send mode +// 0 -- Outlook 2003 and following with text/plain and application/ics (default) +// 1 -- all Outlook, but no text/plain nor application/ics +// We may extend the compat mode if necessary. +pref("calendar.itip.compatSendMode", 0); + +// whether "notify" is checked by default when creating new events/todos with attendees +pref("calendar.itip.notify", true); + +// whether the organizer propagates replies of attendees to all attendees +pref("calendar.itip.notify-replies", false); + +// whether email invitation updates are send out to all attendees if (only) adding a new attendee +pref("calendar.itip.updateInvitationForNewAttendeesOnly", false); + +//whether changes in email invitation updates should be displayed +pref("calendar.itip.displayInvitationChanges", true); + +//whether for delegated invitations a delegatee's replies will be send also to delegator(s) +pref("calendar.itip.notifyDelegatorOnReply", true); + +// whether to prefix the subject field for email invitation invites or updates. +pref("calendar.itip.useInvitationSubjectPrefixes", true); + +// whether CalDAV (experimental) scheduling is enabled or not. +pref("calendar.caldav.sched.enabled", false); + +// 0=Sunday, 1=Monday, 2=Tuesday, etc. One day we might want to move this to +// a locale specific file. +pref("calendar.week.start", 0); +pref("calendar.weeks.inview", 4); +pref("calendar.previousweeks.inview", 0); + +// Show week number in minimonth and multiweek/month views +pref("calendar.view-minimonth.showWeekNumber", true); + +// Default days off +pref("calendar.week.d0sundaysoff", true); +pref("calendar.week.d1mondaysoff", false); +pref("calendar.week.d2tuesdaysoff", false); +pref("calendar.week.d3wednesdaysoff", false); +pref("calendar.week.d4thursdaysoff", false); +pref("calendar.week.d5fridaysoff", false); +pref("calendar.week.d6saturdaysoff", true); + +// 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", ""); + +// Recent timezone list +pref("calendar.timezone.recent", "[]"); + +// categories settings +// XXX One day we might want to move this to a locale specific file +// and include a list of locale specific default categories +pref("calendar.categories.names", ""); + +// Make sure mouse wheel shift and no key actions to scroll lines. +pref("mousewheel.withnokey.action", 0); +pref("mousewheel.withshiftkey.action", 0); + +// Disable use of worker threads. Restart needed. +pref("calendar.threading.disabled", false); + +// The maximum time in microseconds that a cal.forEach event can take (soft limit). +pref("calendar.threading.latency ", 250); + +// Enable support for multiple realms on one server with the payoff that you +// will get multiple password dialogs (one for each calendar) +pref("calendar.network.multirealm", false); + +// Set up user agent +#expand pref("calendar.useragent.extra", "Lightning/__LIGHTNING_VERSION__"); + +// Disable use of system colors in minimonth and calendar views +pref("calendar.view.useSystemColors", false); + +// Disable hiding the label on todayPane button +pref("calendar.view.showTodayPaneStatusLabel", true); + +// Maximum number of iterations allowed when searching for the next matching +// occurrence of a repeating item in calFilter +pref("calendar.filter.maxiterations", 50); + +// Edit events and tasks in a tab rather than a window. +pref("calendar.item.editInTab", false); + +// Edit events and tasks in the new (HTML-based) UI for tabs and windows +pref("calendar.item.useNewItemUI", false); + +// Backend to use. false: libical, true: ical.js +#ifdef NIGHTLY_BUILD +pref("calendar.icaljs", true); +#else +pref("calendar.icaljs", false); +#endif + +// Calendar integration notification +pref("calendar.integration.notify", true); diff --git a/calendar/lightning/content/messenger-overlay-accountCentral.xul b/calendar/lightning/content/messenger-overlay-accountCentral.xul new file mode 100644 index 000000000..2c82e7677 --- /dev/null +++ b/calendar/lightning/content/messenger-overlay-accountCentral.xul @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- 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/. --> + +<!DOCTYPE overlay [ + <!ENTITY % lightningDTD SYSTEM "chrome://lightning/locale/lightning.dtd"> + <!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd"> + %lightningDTD; + %globalDTD; +]> + +<?xml-stylesheet href="chrome://lightning/skin/accountCentral.css" type="text/css"?> + +<overlay id="calendar-list-overlay" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <rows id="acctCentralRows"> + <spacer id="lightning-newCalendar-separator" + flex="1" + insertbefore="AccountsSection.spacer"/> + <row id="lightning-newCalendar-row" + class="acctCentralRow" + insertbefore="AccountsSection.spacer"> + <hbox> + <label class="acctCentralText acctCentralLinkText" + value="&lightning.acctCentral.newCalendar.label;" + onclick="window.parent.openCalendarWizard();"/> + </hbox> + </row> + </rows> +</overlay> diff --git a/calendar/lightning/content/messenger-overlay-messageWindow.xul b/calendar/lightning/content/messenger-overlay-messageWindow.xul new file mode 100644 index 000000000..43fd078e3 --- /dev/null +++ b/calendar/lightning/content/messenger-overlay-messageWindow.xul @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- 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/. --> + +<overlay id="messsenger-overlay-messageWindow" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script type="application/javascript" src="chrome://calendar/content/calendar-statusbar.js"/> + <script type="application/javascript" src="chrome://calendar/content/calendar-item-editing.js"/> +</overlay> diff --git a/calendar/lightning/content/messenger-overlay-preferences.js b/calendar/lightning/content/messenger-overlay-preferences.js new file mode 100644 index 000000000..8bd34802d --- /dev/null +++ b/calendar/lightning/content/messenger-overlay-preferences.js @@ -0,0 +1,27 @@ +/* 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/. */ + +/* exported gLightningPane */ + +var gLightningPane = { + mInitialized: false, + + init: function() { + let preference = document.getElementById("calendar.preferences.lightning.selectedTabIndex"); + if (preference.value) { + let ltnPrefs = document.getElementById("calPreferencesTabbox"); + ltnPrefs.selectedIndex = preference.value; + } + this.mInitialized = true; + }, + + tabSelectionChanged: function() { + if (!this.mInitialized) { + return; + } + let ltnPrefs = document.getElementById("calPreferencesTabbox"); + let preference = document.getElementById("calendar.preferences.lightning.selectedTabIndex"); + preference.valueFromPreferences = ltnPrefs.selectedIndex; + } +}; diff --git a/calendar/lightning/content/messenger-overlay-preferences.xul b/calendar/lightning/content/messenger-overlay-preferences.xul new file mode 100644 index 000000000..b5759aa70 --- /dev/null +++ b/calendar/lightning/content/messenger-overlay-preferences.xul @@ -0,0 +1,67 @@ +<?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/. --> + +<!DOCTYPE overlay [ + <!ENTITY % lightningDTD SYSTEM "chrome://lightning/locale/lightning.dtd"> + <!ENTITY % preferencesDTD SYSTEM "chrome://calendar/locale/preferences/preferences.dtd"> + %lightningDTD; + %preferencesDTD; +]> + +<?xml-stylesheet href="chrome://lightning/skin/lightning.css"?> + +<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <prefwindow id="MailPreferences"> + <prefpane id="paneLightning" + insertbefore="paneAdvanced" + label="&lightning.preferencesLabel;" + onpaneload="gCalendarGeneralPane.init(); gAlarmsPane.init(); + gCategoriesPane.init(); gViewsPane.init(); + gLightningPane.init();"> + <preferences> + <preference id="calendar.preferences.lightning.selectedTabIndex" + name="calendar.preferences.lightning.selectedTabIndex" + type="int"/> + </preferences> + <tabbox id="calPreferencesTabbox" + flex="1" + onselect="gLightningPane.tabSelectionChanged();"> + <tabs> + <tab id="calPreferencesTabGeneral" + label="&paneGeneral.title;"/> + <tab id="calPreferencesTabAlarms" + label="&paneAlarms.title;"/> + <tab id="calPreferencesTabCategories" + label="&paneCategories.title;"/> + <tab id="calPreferencesTabViews" + label="&paneViews.title;"/> + </tabs> + <tabpanels flex="1"> + <tabpanel orient="vertical"> + <vbox id="calPreferencesBoxGeneral"/> + </tabpanel> + <tabpanel orient="vertical"> + <vbox id="calPreferencesBoxAlarms"/> + </tabpanel> + <tabpanel orient="vertical"> + <vbox id="calPreferencesBoxCategories"/> + </tabpanel> + <tabpanel orient="vertical"> + <vbox id="calPreferencesBoxViews"/> + </tabpanel> + </tabpanels> + </tabbox> + </prefpane> + + <script type="application/javascript" + src="chrome://calendar/content/calUtils.js"/> + <script type="application/javascript" + src="chrome://lightning/content/messenger-overlay-preferences.js"/> + + </prefwindow> + +</overlay> diff --git a/calendar/lightning/content/messenger-overlay-sidebar.js b/calendar/lightning/content/messenger-overlay-sidebar.js new file mode 100644 index 000000000..27ab090dc --- /dev/null +++ b/calendar/lightning/content/messenger-overlay-sidebar.js @@ -0,0 +1,946 @@ +/* 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/. */ + +/* exported refreshUIBits, switchCalendarView, rescheduleInvitationsUpdate, + * openInvitationsDialog, onToolbarsPopupShowingWithMode, + * InitViewCalendarPaneMenu, onToolbarsPopupShowingForTabType, + * customizeMailToolbarForTabType + */ + +Components.utils.import("resource://gre/modules/Services.jsm"); +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/Promise.jsm"); +Components.utils.import("resource://gre/modules/Task.jsm"); +Components.utils.import("resource://gre/modules/AddonManager.jsm"); +Components.utils.import("resource://calendar/modules/calAsyncUtils.jsm"); + +var gLastShownCalendarView = null; + +var calendarTabMonitor = { + monitorName: "lightning", + + // Unused, but needed functions + onTabTitleChanged: function() {}, + onTabOpened: function() {}, + onTabClosing: function() {}, + onTabPersist: function() {}, + onTabRestored: function() {}, + + onTabSwitched: function(aNewTab, aOldTab) { + // Unfortunately, tabmail doesn't provide a hideTab function on the tab + // type definitions. To make sure the commands are correctly disabled, + // we want to update calendar/task commands when switching away from + // those tabs. + if (aOldTab.mode.name == "calendar" || + aOldTab.mode.name == "task") { + calendarController.updateCommands(); + calendarController2.updateCommands(); + } + } +}; + +var calendarTabType = { + name: "calendar", + panelId: "calendarTabPanel", + modes: { + calendar: { + type: "calendar", + maxTabs: 1, + openTab: function(aTab, aArgs) { + aTab.title = aArgs.title; + if (!("background" in aArgs) || !aArgs.background) { + // Only do calendar mode switching if the tab is opened in + // foreground. + ltnSwitch2Calendar(); + } + }, + + showTab: function(aTab) { + ltnSwitch2Calendar(); + }, + closeTab: function(aTab) { + if (gCurrentMode == "calendar") { + // Only revert menu hacks if closing the active tab, otherwise we + // would switch to mail mode even if in task mode and closing the + // calendar tab. + ltnSwitch2Mail(); + } + }, + + persistTab: function(aTab) { + let tabmail = document.getElementById("tabmail"); + return { + // Since we do strange tab switching logic in ltnSwitch2Calendar, + // we should store the current tab state ourselves. + background: (aTab != tabmail.currentTabInfo) + }; + }, + + restoreTab: function(aTabmail, aState) { + aState.title = ltnGetString("lightning", "tabTitleCalendar"); + aTabmail.openTab("calendar", aState); + }, + + onTitleChanged: function(aTab) { + aTab.title = ltnGetString("lightning", "tabTitleCalendar"); + }, + + supportsCommand: (aCommand, aTab) => calendarController2.supportsCommand(aCommand), + isCommandEnabled: (aCommand, aTab) => calendarController2.isCommandEnabled(aCommand), + doCommand: (aCommand, aTab) => calendarController2.doCommand(aCommand), + onEvent: (aEvent, aTab) => calendarController2.onEvent(aEvent) + }, + + tasks: { + type: "tasks", + maxTabs: 1, + openTab: function(aTab, aArgs) { + aTab.title = aArgs.title; + if (!("background" in aArgs) || !aArgs.background) { + ltnSwitch2Task(); + } + }, + showTab: function(aTab) { + ltnSwitch2Task(); + }, + closeTab: function(aTab) { + if (gCurrentMode == "task") { + // Only revert menu hacks if closing the active tab, otherwise we + // would switch to mail mode even if in calendar mode and closing the + // tasks tab. + ltnSwitch2Mail(); + } + }, + + persistTab: function(aTab) { + let tabmail = document.getElementById("tabmail"); + return { + // Since we do strange tab switching logic in ltnSwitch2Task, + // we should store the current tab state ourselves. + background: (aTab != tabmail.currentTabInfo) + }; + }, + + restoreTab: function(aTabmail, aState) { + aState.title = ltnGetString("lightning", "tabTitleTasks"); + aTabmail.openTab("tasks", aState); + }, + + onTitleChanged: function(aTab) { + aTab.title = ltnGetString("lightning", "tabTitleTasks"); + }, + + supportsCommand: (aCommand, aTab) => calendarController2.supportsCommand(aCommand), + isCommandEnabled: (aCommand, aTab) => calendarController2.isCommandEnabled(aCommand), + doCommand: (aCommand, aTab) => calendarController2.doCommand(aCommand), + onEvent: (aEvent, aTab) => calendarController2.onEvent(aEvent) + } + }, + + /** + * Because calendar does some direct menu manipulation, we need to + * change to the mail mode to clean up after those hacks. + * + * @param {Object} aTab A tab info object + */ + saveTabState: function(aTab) { + ltnSwitch2Mail(); + } +}; + +/** + * For details about tab info objects and the tabmail interface see: + * comm-central/mail/base/content/mailTabs.js + * comm-central/mail/base/content/tabmail.xml + */ +var calendarItemTabType = { + name: "calendarItem", + perTabPanel: "vbox", + idNumber: 0, + modes: { + calendarEvent: { type: "calendarEvent" }, + calendarTask: { type: "calendarTask" } + }, + /** + * Opens an event tab or a task tab. + * + * @param {Object} aTab A tab info object + * @param {Object} aArgs Contains data about the event/task + */ + openTab: function(aTab, aArgs) { + // Create a clone to use for this tab. Remove the cloned toolbox + // and move the original toolbox into its place. There is only + // one toolbox/toolbar so its settings are the same for all item tabs. + let original = document.getElementById("lightningItemPanel").firstChild; + let clone = original.cloneNode(true); + + clone.querySelector("toolbox").remove(); + moveEventToolbox(clone); + clone.setAttribute("id", "calendarItemTab" + this.idNumber); + + if (aTab.mode.type == "calendarTask") { + // For task tabs, css class hides event-specific toolbar buttons. + clone.setAttribute("class", "calendar-task-dialog-tab"); + } + + aTab.panel.appendChild(clone); + + // Set up the iframe and store the iframe's id. The iframe's + // src is set in onLoadLightningItemPanel() that is called below. + aTab.iframe = aTab.panel.querySelector("iframe"); + let iframeId = "calendarItemTabIframe" + this.idNumber; + aTab.iframe.setAttribute("id", iframeId); + gItemTabIds.push(iframeId); + + // Generate and set the tab title. + let strName; + if (aTab.mode.type == "calendarEvent") { + strName = aArgs.calendarEvent.title ? "editEventDialog" : "newEventDialog"; + } else if (aTab.mode.type == "calendarTask") { + strName = aArgs.calendarEvent.title ? "editTaskDialog" : "newTaskDialog"; + } else { + throw Components.results.NS_ERROR_NOT_IMPLEMENTED; + } + // name is "New Event", "Edit Task", etc. + let name = cal.calGetString("calendar", strName); + aTab.title = name + ": " + (aArgs.calendarEvent.title || name); + + // allowTabClose prevents the tab from being closed until we ask + // the user if they want to save any unsaved changes. + aTab.allowTabClose = false; + + // Put the arguments where they can be accessed easily + // from the iframe. (window.arguments[0]) + aTab.iframe.contentWindow.arguments = [aArgs]; + + // activate or de-activate 'Events and Tasks' menu items + document.commandDispatcher.updateCommands("calendar_commands"); + + onLoadLightningItemPanel(iframeId, aArgs.url); + + this.idNumber += 1; + }, + /** + * Saves a tab's state when it is deactivated / hidden. The opposite of showTab. + * + * @param {Object} aTab A tab info object + */ + saveTabState: function(aTab) { + // save state + aTab.itemTabConfig = {}; + Object.assign(aTab.itemTabConfig, gConfig); + + // clear statusbar + let statusbar = document.getElementById("status-bar"); + let items = statusbar.getElementsByClassName("event-dialog"); + for (let item of items) { + item.setAttribute("collapsed", true); + } + // move toolbox to the place where it can be accessed later + let to = document.getElementById("lightningItemPanel").firstChild; + moveEventToolbox(to); + }, + /** + * Called when a tab is activated / shown. The opposite of saveTabState. + * + * @param {Object} aTab A tab info object + */ + showTab: function(aTab) { + // move toolbox into place then load state + moveEventToolbox(aTab.panel.firstChild); + Object.assign(gConfig, aTab.itemTabConfig); + updateItemTabState(gConfig); + + // activate or de-activate 'Events and Tasks' menu items + document.commandDispatcher.updateCommands("calendar_commands"); + }, + /** + * Called when there is a request to close a tab. Using aTab.allowTabClose + * we first prevent the tab from closing so we can prompt the user + * about saving changes, then we allow the tab to close. + * + * @param {Object} aTab A tab info object + */ + tryCloseTab: function(aTab) { + if (aTab.allowTabClose) { + return true; + } else { + onCancel(aTab.iframe.id); + return false; + } + }, + /** + * Closes a tab. + * + * @param {Object} aTab A tab info object + */ + closeTab: function(aTab) { + // Remove the iframe id from the array where they are stored. + let index = gItemTabIds.indexOf(aTab.iframe.id); + if (index != -1) { + gItemTabIds.splice(index, 1); + } + aTab.itemTabConfig = null; + }, + /** + * Called when quitting the application (and/or closing the window). + * Saves an open tab's state to be able to restore it later. + * + * @param {Object} aTab A tab info object + */ + persistTab: function(aTab) { + let args = aTab.iframe.contentWindow.arguments[0]; + // Serialize args, with manual handling of some properties. + // persistTab is called even for new events/tasks in tabs that + // were closed and never saved (for 'undo close tab' + // functionality), thus we confirm we have the expected values. + if (!args || !args.calendar || !args.calendar.id || + !args.calendarEvent || !args.calendarEvent.id) { + return {}; + } + + let calendarId = args.calendar.id; + let itemId = args.calendarEvent.id; + // Handle null args.initialStartDateValue, just for good measure. + // Note that this is not the start date for the event or task. + let hasDateValue = args.initialStartDateValue && + args.initialStartDateValue.icalString; + let initialStartDate = hasDateValue + ? args.initialStartDateValue.icalString : null; + + args.calendar = null; + args.calendarEvent = null; + args.initialStartDateValue = null; + + return { + calendarId: calendarId, + itemId: itemId, + initialStartDate: initialStartDate, + args: args, + tabType: aTab.mode.type + }; + }, + /** + * Called when starting the application (and/or opening the window). + * Restores a tab that was open when the application was quit previously. + * + * @param {Object} aTabmail The tabmail interface + * @param {Object} aState The state of the tab to restore + */ + restoreTab: function(aTabmail, aState) { + // Sometimes restoreTab is called for tabs that were never saved + // and never meant to be persisted or restored. See persistTab. + if (aState.args && aState.calendarId && aState.itemId) { + aState.args.initialStartDateValue = aState.initialStartDate + ? cal.createDateTime(aState.initialStartDate) : getDefaultStartDate(); + + aState.args.onOk = doTransaction.bind(null, "modify"); + + aState.args.calendar = getCalendarManager().getCalendarById(aState.calendarId); + if (aState.args.calendar) { + // using wrappedJSObject is a hack that is needed to prevent a proxy error + let pcal = cal.async.promisifyCalendar(aState.args.calendar.wrappedJSObject); + pcal.getItem(aState.itemId).then((item) => { + if (item[0]) { + aState.args.calendarEvent = item[0]; + aTabmail.openTab(aState.tabType, aState.args); + } + }); + } + } + } +}; + +window.addEventListener("load", (e) => { + let tabmail = document.getElementById("tabmail"); + tabmail.registerTabType(calendarTabType); + tabmail.registerTabType(calendarItemTabType); + tabmail.registerTabMonitor(calendarTabMonitor); +}, false); + + +function ltnOnLoad(event) { + // nuke the onload, or we get called every time there's + // any load that occurs + window.removeEventListener("load", ltnOnLoad, false); + + // Check if the binary component was loaded + checkCalendarBinaryComponent(); + + document.getElementById("calendarDisplayDeck") + .addEventListener("select", LtnObserveDisplayDeckChange, true); + + // Take care of common initialization + commonInitCalendar(); + + // Add an unload function to the window so we don't leak any listeners + window.addEventListener("unload", ltnFinish, false); + + // Set up invitations manager + scheduleInvitationsUpdate(FIRST_DELAY_STARTUP); + getCalendarManager().addObserver(gInvitationsCalendarManagerObserver); + + let filter = document.getElementById("task-tree-filtergroup"); + filter.value = filter.value || "all"; + document.getElementById("modeBroadcaster").setAttribute("mode", gCurrentMode); + document.getElementById("modeBroadcaster").setAttribute("checked", "true"); + + let mailContextPopup = document.getElementById("mailContext"); + if (mailContextPopup) { + mailContextPopup.addEventListener("popupshowing", + gCalSetupMailContext.popup, false); + } + + // Setup customizeDone handlers for our toolbars + let toolbox = document.getElementById("calendar-toolbox"); + toolbox.customizeDone = function(aEvent) { + MailToolboxCustomizeDone(aEvent, "CustomizeCalendarToolbar"); + }; + toolbox = document.getElementById("task-toolbox"); + toolbox.customizeDone = function(aEvent) { + MailToolboxCustomizeDone(aEvent, "CustomizeTaskToolbar"); + }; + + ltnIntegrationCheck(); + + Services.obs.notifyObservers(window, "lightning-startup-done", false); +} + +/** + * Displays the Lightning integration notification bar + */ +function ltnIntegrationNotification() { + const kOptOut = "mail.calendar-integration.opt-out"; // default: false + const kNotify = "calendar.integration.notify"; // default: true + const kSupportUri = "https://support.mozilla.org/kb/thunderbird-calendar-integration"; + const kLightningGuuid = "{e2fda1a4-762b-4020-b5ad-a41df1933103}"; + + // we fall back to messagepanebox for Seamonkey + let notifyBox = document.getElementById("mail-notification-box") || + document.getElementById("messagepanebox"); + + let appBrand = cal.calGetString("brand", "brandShortName", null, "branding"); + let ltnBrand = ltnGetString("lightning", "brandShortName"); + let label = ltnGetString("lightning", "integrationLabel", [appBrand, ltnBrand]); + + // call backs for doing/undoing Lightning removal + let cbRemoveLightning = function(aAddon) { + aAddon.userDisabled = true; + }; + let cbUndoRemoveLightning = function(aAddon) { + aAddon.userDisabled = false; + }; + + // call backs for the undo opt-out bar + let cbRestartNow = function(aNotificationBar, aButton) { + Services.startup.quit(Components.interfaces.nsIAppStartup.eRestart | + Components.interfaces.nsIAppStartup.eForceQuit); + }; + let cbUndoOptOut = function(aNotificationBar, aButton) { + Preferences.set(kNotify, true); + Preferences.set(kOptOut, false); + AddonManager.getAddonByID(kLightningGuuid, cbUndoRemoveLightning); + // display notification bar again + ltnIntegrationNotification(); + }; + + // call backs for the opt-out bar + let cbLearnMore = function(aNotificationBar, aButton) { + // In SeaMonkey the second parameter should be either null or an + // event object with a non null target.ownerDocument. + openUILink(kSupportUri, { + button: 0, + target: { ownerDocument: document } + }); + return true; + }; + let cbKeepIt = function(aNotificationBar, aButton) { + Preferences.set(kNotify, false); + }; + let cbOptOut = function(aNotificationBar, aButton) { + Preferences.set(kNotify, false); + Preferences.set(kOptOut, true); + AddonManager.getAddonByID(kLightningGuuid, cbRemoveLightning); + // let the user know that removal will be applied after restart + let restartLabel = ltnGetString("lightning", "integrationRestartLabel", [ltnBrand, appBrand]); + let button = [{ + label: ltnGetString("lightning", "integrationUndoButton"), + accessKey: ltnGetString("lightning", "integrationUndoAccessKey"), + popup: null, + callback: cbUndoOptOut + }, { + label: ltnGetString("lightning", "integrationRestartButton"), + accessKey: ltnGetString("lightning", "integrationRestartAccessKey"), + popup: null, + callback: cbRestartNow + }]; + notifyBox.appendNotification(restartLabel, + "restart-required", + null, + notifyBox.PRIORITY_INFO_MEDIUM, + button); + }; + + let buttons = [{ + label: ltnGetString("lightning", "integrationLearnMoreButton"), + accessKey: ltnGetString("lightning", "integrationLearnMoreAccessKey"), + popup: null, + callback: cbLearnMore + }, { + label: ltnGetString("lightning", "integrationOptOutButton"), + accessKey: ltnGetString("lightning", "integrationOptOutAccessKey"), + popup: null, + callback: cbOptOut + }, { + label: ltnGetString("lightning", "integrationKeepItButton"), + accessKey: ltnGetString("lightning", "integrationKeepItAccessKey"), + popup: null, + callback: cbKeepIt + }]; + + // we use PRIORITY_INFO_MEDIUM to overrule notifications from specialTabs.js if any + let notification = notifyBox.appendNotification(label, + "calendar-integration", + null, + notifyBox.PRIORITY_INFO_MEDIUM, + buttons); + notification.persistence = 3; +} + +/** + * Checks whether to display the opt-out notification for Lightning integration + */ +function ltnIntegrationCheck() { + const kOptOut = "mail.calendar-integration.opt-out"; // default: false + const kNotify = "calendar.integration.notify"; // default: true + // don't do anything if the opt-out pref doesn't exist or is enabled by the user or the user has + // already decided to keep Lightning + if (!Preferences.get(kOptOut, true) && Preferences.get(kNotify, false)) { + // action is only needed, if hasn't used Lightning before, so lets check whether this looks + // like a default calendar setup + let cnt = {}; + let calMgr = cal.getCalendarManager(); + let cals = calMgr.getCalendars(cnt); + let homeCalName = cal.calGetString("calendar", "homeCalendarName", null, "calendar"); + if (cnt.value == 1 && + calMgr.getCalendarPref_(cals[0], "type") == "storage" && + calMgr.getCalendarPref_(cals[0], "name") == homeCalName) { + // this looks like a default setup, so let's see whether the calendar contains any items + let pCal = cal.async.promisifyCalendar(cals[0]); + // we look at all items at any time, but we can stop if the first item was found + // if we've found no items, we call ltnIntegrationNotification to display the bar + pCal.getItems(Components.interfaces.calICalendar.ITEM_FILTER_ALL_ITEMS, 1, null, null) + .then((aItems) => { if (!aItems.length) { ltnIntegrationNotification(); } }); + } + } +} + +/* Called at midnight to tell us to redraw date-specific widgets. Do NOT call + * this for normal refresh, since it also calls scheduleMidnightRefresh. + */ +function refreshUIBits() { + try { + getMinimonth().refreshDisplay(); + + // Refresh the current view and just allow the refresh for the others + // views when will be displayed. + let currView = currentView(); + currView.goToDay(); + ["day-view", + "week-view", + "multiweek-view", + "month-view"].forEach((view) => { + if (view != currView.id) { + document.getElementById(view).mToggleStatus = -1; + } + }); + + if (!TodayPane.showsToday()) { + TodayPane.setDay(now()); + } + + // update the unifinder + refreshEventTree(); + + // update today's date on todaypane button + document.getElementById("calendar-status-todaypane-button").setUpTodayDate(); + } catch (exc) { + ASSERT(false, exc); + } + + // schedule our next update... + scheduleMidnightUpdate(refreshUIBits); +} + +/** + * Switch the calendar view, and optionally switch to calendar mode. + * + * @param aType The type of view to select. + * @param aShow If true, the mode will be switched to calendar if not + * already there. + */ +function switchCalendarView(aType, aShow) { + gLastShownCalendarView = aType; + + if (aShow && gCurrentMode != "calendar") { + // This function in turn calls switchToView(), so return afterwards. + ltnSwitch2Calendar(); + return; + } + + // Sunbird/Lightning common view switching code + switchToView(aType); +} + +/** + * This function has the sole responsibility to switch back to + * mail mode (by calling ltnSwitch2Mail()) if we are getting + * notifications from other panels (besides the calendar views) + * but find out that we're not in mail mode. This situation can + * for example happen if we're in calendar mode but the 'new mail' + * slider gets clicked and wants to display the appropriate mail. + * All necessary logic for switching between the different modes + * should live inside of the corresponding functions: + * - ltnSwitch2Mail() + * - ltnSwitch2Calendar() + * - ltnSwitch2Task() + */ +function LtnObserveDisplayDeckChange(event) { + let deck = event.target; + + // Bug 309505: The 'select' event also fires when we change the selected + // panel of calendar-view-box. Workaround with this check. + if (deck.id != "calendarDisplayDeck") { + return; + } + + let id = deck.selectedPanel && deck.selectedPanel.id; + + // Switch back to mail mode in case we find that this + // notification has been fired but we're still in calendar or task mode. + // Specifically, switch back if we're *not* in mail mode but the notification + // did *not* come from either the "calendar-view-box" or the "calendar-task-box". + if (gCurrentMode != "mail") { + if (id != "calendar-view-box" && id != "calendar-task-box") { + ltnSwitch2Mail(); + } + } +} + +function ltnFinish() { + getCalendarManager().removeObserver(gInvitationsCalendarManagerObserver); + + // Remove listener for mailContext. + let mailContextPopup = document.getElementById("mailContext"); + if (mailContextPopup) { + mailContextPopup.removeEventListener("popupshowing", + gCalSetupMailContext.popup, false); + } + + // Common finish steps + commonFinishCalendar(); +} + +// == invitations link +var FIRST_DELAY_STARTUP = 100; +var FIRST_DELAY_RESCHEDULE = 100; +var FIRST_DELAY_REGISTER = 10000; +var FIRST_DELAY_UNREGISTER = 0; + +var gInvitationsOperationListener = { + mCount: 0, + + QueryInterface: XPCOMUtils.generateQI([Components.interfaces.calIOperationListener]), + onOperationComplete: function(aCalendar, aStatus, aOperationType, aId, aDetail) { + let invitationsBox = document.getElementById("calendar-invitations-panel"); + if (Components.isSuccessCode(aStatus)) { + let value = ltnGetString("lightning", "invitationsLink.label", [this.mCount]); + document.getElementById("calendar-invitations-label").value = value; + setElementValue(invitationsBox, this.mCount < 1 && "true", "hidden"); + } else { + invitationsBox.setAttribute("hidden", "true"); + } + this.mCount = 0; + }, + + onGetResult: function(aCalendar, aStatus, aItemType, aDetail, aCount, aItems) { + if (Components.isSuccessCode(aStatus)) { + this.mCount += aCount; + } + } +}; + +var gInvitationsCalendarManagerObserver = { + mSideBar: this, + + QueryInterface: XPCOMUtils.generateQI([Components.interfaces.calICalendarManagerObserver]), + + onCalendarRegistered: function(aCalendar) { + this.mSideBar.rescheduleInvitationsUpdate(FIRST_DELAY_REGISTER); + }, + + onCalendarUnregistering: function(aCalendar) { + this.mSideBar.rescheduleInvitationsUpdate(FIRST_DELAY_UNREGISTER); + }, + + onCalendarDeleting: function(aCalendar) { + } +}; + +function scheduleInvitationsUpdate(firstDelay) { + gInvitationsOperationListener.mCount = 0; + getInvitationsManager().scheduleInvitationsUpdate(firstDelay, + gInvitationsOperationListener); +} + +function rescheduleInvitationsUpdate(firstDelay) { + getInvitationsManager().cancelInvitationsUpdate(); + scheduleInvitationsUpdate(firstDelay); +} + +function openInvitationsDialog() { + getInvitationsManager().cancelInvitationsUpdate(); + gInvitationsOperationListener.mCount = 0; + getInvitationsManager().openInvitationsDialog( + gInvitationsOperationListener, + () => scheduleInvitationsUpdate(FIRST_DELAY_RESCHEDULE)); +} + +/** + * the current mode is set to a string defining the current + * mode we're in. allowed values are: + * - 'mail' + * - 'calendar' + * - 'task' + */ +var gCurrentMode = "mail"; + +/** + * ltnSwitch2Mail() switches to the mail mode + */ + +function ltnSwitch2Mail() { + if (gCurrentMode != "mail") { + gCurrentMode = "mail"; + document.getElementById("modeBroadcaster").setAttribute("mode", gCurrentMode); + + document.commandDispatcher.updateCommands("calendar_commands"); + window.setCursor("auto"); + } +} + +/** + * ltnSwitch2Calendar() switches to the calendar mode + */ + +function ltnSwitch2Calendar() { + if (gCurrentMode != "calendar") { + gCurrentMode = "calendar"; + document.getElementById("modeBroadcaster").setAttribute("mode", gCurrentMode); + + // display the calendar panel on the display deck + let deck = document.getElementById("calendarDisplayDeck"); + deck.selectedPanel = document.getElementById("calendar-view-box"); + + // show the last displayed type of calendar view + switchToView(gLastShownCalendarView); + + document.commandDispatcher.updateCommands("calendar_commands"); + window.setCursor("auto"); + + // make sure the view is sized correctly + onCalendarViewResize(); + } +} + +/** + * ltnSwitch2Task() switches to the task mode + */ + +function ltnSwitch2Task() { + if (gCurrentMode != "task") { + gCurrentMode = "task"; + document.getElementById("modeBroadcaster").setAttribute("mode", gCurrentMode); + + // display the task panel on the display deck + let deck = document.getElementById("calendarDisplayDeck"); + deck.selectedPanel = document.getElementById("calendar-task-box"); + + document.commandDispatcher.updateCommands("calendar_commands"); + window.setCursor("auto"); + } +} + +var gCalSetupMailContext = { + popup: function() { + let hasSelection = (gFolderDisplay.selectedMessage != null); + // Disable the convert menu altogether. + setElementValue("mailContext-calendar-convert-menu", + !hasSelection && "true", "hidden"); + } +}; + +// Overwrite the InitMessageMenu function, since we never know in which order +// the popupshowing event will be processed. This function takes care of +// disabling the message menu when in calendar or task mode. +function calInitMessageMenu() { + calInitMessageMenu.origFunc(); + + document.getElementById("markMenu").disabled = (gCurrentMode != "mail"); +} +calInitMessageMenu.origFunc = InitMessageMenu; +InitMessageMenu = calInitMessageMenu; + +window.addEventListener("load", ltnOnLoad, false); + +/** + * Get the toolbox id for the current tab type. + * + * @return {string} A toolbox id or null + */ +function getToolboxIdForCurrentTabType() { + // A mapping from calendar tab types to toolbox ids. + const calendarToolboxIds = { + calendar: "calendar-toolbox", + tasks: "task-toolbox", + calendarEvent: "event-toolbox", + calendarTask: "event-toolbox" + }; + let tabmail = document.getElementById("tabmail"); + let tabType = tabmail.currentTabInfo.mode.type; + + return calendarToolboxIds[tabType] || "mail-toolbox"; +} + +/** + * Modify the contents of the "Toolbars" context menu for the current + * tab type. Menu items are inserted before (appear above) aInsertPoint. + * + * @param {MouseEvent} aEvent The popupshowing event + * @param {nsIDOMXULElement} aInsertPoint (optional) menuitem node + */ +function onToolbarsPopupShowingForTabType(aEvent, aInsertPoint) { + if (onViewToolbarsPopupShowing.length < 3) { + // SeaMonkey + onViewToolbarsPopupShowing(aEvent); + return; + } + + let toolboxes = []; + let toolboxId = getToolboxIdForCurrentTabType(); + + // We add navigation-toolbox ("Menu Bar") for all tab types except + // mail tabs because mail-toolbox already includes navigation-toolbox, + // so we do not need to add it separately in that case. + if (toolboxId != "mail-toolbox") { + toolboxes.push("navigation-toolbox"); + } + toolboxes.push(toolboxId); + + if (toolboxId == "event-toolbox") { + // Clear the event/task tab's toolbox.externalToolbars to prevent + // duplicate entries for its toolbar in "Toolbars" menu. + // (The cloning and/or moving of this toolbox and toolbar on + // openTab causes the toolbar to be added to + // toolbox.externalToolbars, in addition to being a child node + // of the toolbox, leading to duplicate menu entries.) + let eventToolbox = document.getElementById("event-toolbox"); + if (eventToolbox) { + eventToolbox.externalToolbars = []; + } + } + + onViewToolbarsPopupShowing(aEvent, toolboxes, aInsertPoint); +} + +/** + * Open the customize dialog for the toolbar for the current tab type. + */ +function customizeMailToolbarForTabType() { + let toolboxId = getToolboxIdForCurrentTabType(); + if (toolboxId == "event-toolbox") { + onCommandCustomize(); + } else { + CustomizeMailToolbar(toolboxId, "CustomizeMailToolbar"); + } +} + +// Initialize the Calendar sidebar menu state +function InitViewCalendarPaneMenu() { + let calSidebar = document.getElementById("ltnSidebar"); + + setBooleanAttribute("ltnViewCalendarPane", "checked", + !calSidebar.getAttribute("collapsed")); + + if (document.getElementById("appmenu_ltnViewCalendarPane")) { + setBooleanAttribute("appmenu_ltnViewCalendarPane", "checked", + !calSidebar.getAttribute("collapsed")); + } +} + + +/** + * Move the event toolbox, containing the toolbar, into view for a tab + * or back to its hiding place where it is accessed again for other tabs. + * + * @param {nsIDOMNode} aDestination Destination where the toolbox will be moved + */ +function moveEventToolbox(aDestination) { + let toolbox = document.getElementById("event-toolbox"); + // the <toolbarpalette> has to be copied manually + let palette = toolbox.palette; + let iframe = aDestination.querySelector("iframe"); + aDestination.insertBefore(toolbox, iframe); + toolbox.palette = palette; +} + +/** + * Checks if Lightning's binary component was successfully loaded. + */ +function checkCalendarBinaryComponent() { + // Don't even get started if we are running ical.js or the binary component + // was successfully loaded. + if ("@mozilla.org/calendar/datetime;1" in Components.classes || + Preferences.get("calendar.icaljs", false)) { + return; + } + + const THUNDERBIRD_GUID = "{3550f703-e582-4d05-9a08-453d09bdfdc6}"; + const SEAMONKEY_GUID = "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}"; + const LIGHTNING_GUID = "{e2fda1a4-762b-4020-b5ad-a41df1933103}"; + + AddonManager.getAddonByID(LIGHTNING_GUID, (ext) => { + if (!ext) { + return; + } + + let version; + let appversion = Services.appinfo.version; + let versionparts = appversion.split("."); + let extbrand = ltnGetString("lightning", "brandShortName"); + + switch (Services.appinfo.ID) { + case THUNDERBIRD_GUID: // e.g. 31.4.0 -> 3.3 + version = ((parseInt(versionparts[0], 10) + 2) / 10).toFixed(1); + break; + case SEAMONKEY_GUID: // e.g. 2.28.4 -> 3.3 + version = ((parseInt(versionparts[1], 10) + 5) / 10).toFixed(1); + break; + } + + let text; + if (version && version != ext.version) { + let args = [extbrand, ext.version, version]; + text = ltnGetString("lightning", "binaryComponentKnown", args); + } else { + let brand = cal.calGetString("brand", "brandShortName", null, "branding"); + let args = [extbrand, brand, appversion, ext.version]; + text = ltnGetString("lightning", "binaryComponentUnknown", args); + } + + let title = ltnGetString("lightning", "binaryComponentTitle", [extbrand]); + openAddonsMgr("addons://detail/" + encodeURIComponent(LIGHTNING_GUID)); + Services.prompt.alert(window, title, text); + }); +} diff --git a/calendar/lightning/content/messenger-overlay-sidebar.xul b/calendar/lightning/content/messenger-overlay-sidebar.xul new file mode 100644 index 000000000..b00dcef73 --- /dev/null +++ b/calendar/lightning/content/messenger-overlay-sidebar.xul @@ -0,0 +1,315 @@ +<?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/. --> + +<!DOCTYPE overlay +[ + <!ENTITY % dtd1 SYSTEM "chrome://lightning/locale/lightning.dtd" > %dtd1; + <!ENTITY % dtd2 SYSTEM "chrome://calendar/locale/menuOverlay.dtd" > %dtd2; + <!ENTITY % dtd3 SYSTEM "chrome://calendar/locale/calendar.dtd" > %dtd3; + <!ENTITY % dtd4 SYSTEM "chrome://lightning/locale/lightning-toolbar.dtd" > %dtd4; + <!ENTITY % messengerDTD SYSTEM "chrome://messenger/locale/messenger.dtd" > %messengerDTD; + <!ENTITY % eventDialogDTD SYSTEM "chrome://calendar/locale/calendar-event-dialog.dtd" > %eventDialogDTD; +]> + +<?xml-stylesheet href="chrome://lightning/skin/lightning.css" type="text/css"?> + +<?xml-stylesheet href="chrome://calendar/content/calendar-view-bindings.css" type="text/css"?> +<?xml-stylesheet href="chrome://calendar/content/datetimepickers/datetimepickers.css" type="text/css"?> + +<?xml-stylesheet href="chrome://calendar/skin/calendar-event-dialog.css" type="text/css"?> +<?xml-stylesheet href="chrome://calendar/content/calendar-event-dialog.css" type="text/css"?> +<?xml-stylesheet href="chrome://calendar-common/skin/dialogs/calendar-event-dialog.css" type="text/css"?> + +<?xul-overlay href="chrome://calendar/content/calendar-calendars-list.xul"?> +<?xul-overlay href="chrome://calendar/content/calendar-common-sets.xul"?> +<?xul-overlay href="chrome://calendar/content/calendar-views.xul"?> + +<?xul-overlay href="chrome://lightning/content/lightning-toolbar.xul"?> +<?xul-overlay href="chrome://lightning/content/lightning-menus.xul"?> + +<overlay id="ltnSidebarOverlay" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <!-- NEEDED FOR MULTIPLE CALENDAR SUPPORT --> + <script type="application/javascript" src="chrome://calendar/content/calendar-management.js"/> + + <!-- NEEDED FOR CLIPBOARD SUPPORT --> + <script type="application/javascript" src="chrome://calendar/content/calendar-clipboard.js"/> + + <!-- NEEDED FOR IMPORT / EXPORT SUPPORT --> + <script type="application/javascript" src="chrome://calendar/content/import-export.js"/> + + <!-- NEEDED FOR PUBLICATION SUPPORT --> + <script type="application/javascript" src="chrome://calendar/content/publish.js"/> + + <script type="application/javascript" src="chrome://calendar/content/calendar-item-editing.js"/> + <script type="application/javascript" src="chrome://calendar/content/calendar-chrome-startup.js"/> + <script type="application/javascript" src="chrome://calendar/content/calUtils.js"/> + <script type="application/javascript" src="chrome://calendar/content/mouseoverPreviews.js"/> + <script type="application/javascript" src="chrome://calendar/content/calendar-views.js"/> + <script type="application/javascript" src="chrome://calendar/content/calendar-ui-utils.js"/> + <script type="application/javascript" src="chrome://calendar/content/calendar-creation.js"/> + <script type="application/javascript" src="chrome://calendar/content/calendar-dnd-listener.js"/> + <script type="application/javascript" src="chrome://calendar/content/calendar-statusbar.js"/> + <script type="application/javascript" src="chrome://global/content/nsDragAndDrop.js"/> + + <!-- NEEDED FOR TASK VIEW/LIST SUPPORT --> + <script type="application/javascript" src="chrome://calendar/content/calendar-task-editing.js"/> + + <script type="application/javascript" src="chrome://calendar/content/calendar-extract.js"/> + + <script type="application/javascript" src="chrome://lightning/content/lightning-utils.js"/> + <script type="application/javascript" src="chrome://lightning/content/messenger-overlay-sidebar.js"/> + <script type="application/javascript" src="chrome://calendar/content/calendar-invitations-manager.js"/> + + <!-- NEEDED FOR EVENT/TASK IN A TAB --> + <script type="application/javascript" src="chrome://lightning/content/lightning-item-panel.js"/> + + <window id="messengerWindow"> + <!-- Be sure to keep these sets, since they will be overlayed by + calendar/base/content/calendar-common-sets.xul --> + <commandset id="calendar_commands"> + <command id="agenda_delete_event_command" oncommand="agendaListbox.deleteSelectedItem(false);"/> + <command id="agenda_edit_event_command" oncommand="agendaListbox.editSelectedItem(event);"/> + <command id="switch2calendar" + oncommand="document.getElementById('tabmail').openTab('calendar', { title: document.getElementById('calendar-tab-button').getAttribute('title') })"/> + <command id="switch2task" + oncommand="document.getElementById('tabmail').openTab('tasks', { title: document.getElementById('task-tab-button').getAttribute('title') })"/> + <command id="new_calendar_tab" + oncommand="document.getElementById('tabmail').openTab('calendar', { title: document.getElementById('calendar-tab-button').getAttribute('title') })"/> + <command id="new_task_tab" + oncommand="document.getElementById('tabmail').openTab('tasks', { title: document.getElementById('task-tab-button').getAttribute('title') })"/> + <command id="calendar_go_to_today_command" + observes="calendar_mode_calendar" + oncommand="document.getElementById('tabmail').openTab('calendar', { title: document.getElementById('calendar-tab-button').getAttribute('title') }); goToDate(now())"/> + </commandset> + + <commandset id="mailCommands"> + <command id="cmd_CustomizeMailToolbar" + oncommand="customizeMailToolbarForTabType()"/> + </commandset> + + <keyset id="calendar-keys"> + <key id="openLightningKey" + key="&lightning.keys.event.showCalendar.key;" + modifiers="accel, shift" + observes="new_calendar_tab"/> + <key id="openTasksKey" + key="&lightning.keys.event.showTasks.key;" + modifiers="accel, shift" + command="new_task_tab"/> + <key id="todaypanekey" command="calendar_toggle_todaypane_command" keycode="VK_F11"/> + <key id="calendar-new-event-key" key="&lightning.keys.event.new;" modifiers="accel" command="calendar_new_event_command"/> + <key id="calendar-new-todo-key" key="&lightning.keys.todo.new;" modifiers="accel" command="calendar_new_todo_command"/> + </keyset> + + <broadcasterset id="calendar_broadcasters"> + <broadcaster id="filterBroadcaster" value="all"/> + </broadcasterset> + + <popupset id="calendar-popupset"/> + </window> + + <toolbar id="tabs-toolbar"> + <toolbarbutton id="calendar-tab-button" + class="toolbarbutton-1" + insertafter="alltabs-button" + title="&lightning.toolbar.calendar.label;" + tooltiptext="&lightning.toolbar.calendar.tooltip;" + command="new_calendar_tab" + hidden="true"/> + <toolbarbutton id="task-tab-button" + class="toolbarbutton-1" + insertafter="calendar-tab-button" + title="&lightning.toolbar.task.label;" + tooltiptext="&lightning.toolbar.task.tooltip;" + command="new_task_tab" + hidden="true"/> + </toolbar> + + <tabpanels id="tabpanelcontainer"> + <vbox id="calendarTabPanel"> + <!-- Unfortunately we use the same panel for task and calendar tabs, so + we need to differ which toolbar is being shown. The actual toolbar + content will be added via a further overlay --> + <modevbox id="calendar-toolbox-container" mode="calendar" broadcaster="modeBroadcaster"> + <toolbox id="calendar-toolbox"/> + </modevbox> + <modevbox id="task-toolbox-container" mode="task" broadcaster="modeBroadcaster"> + <toolbox id="task-toolbox"/> + </modevbox> + <hbox id="calendarContent" flex="1"> + <vbox id="ltnSidebar" + width="200" + persist="collapsed width"> + <modevbox id="minimonth-pane" mode="calendar,task" broadcaster="modeBroadcaster" refcontrol="calendar_toggle_minimonthpane_command"> + <vbox align="center"> + <hbox id="calMinimonthBox" pack="center"> + <minimonth id="calMinimonth" onchange="minimonthPick(this.value);" freebusy="true"/> + </hbox> + </vbox> + </modevbox> + <separator id="minimonth-splitter" minwidth="100"/> + <vbox id="calendar-panel" flex="1"> + <modevbox id="task-filter-pane" mode="task" broadcaster="modeBroadcaster" refcontrol="calendar_toggle_filter_command"> + <treenode-checkbox id="task-tree-filter-header" + checked="true" + class="treenode-checkbox" + label="&calendar.task.filter.title.label;"/> + <modevbox id="task-filtertree-pane" flex="1" mode="task" broadcaster="modeBroadcaster" refcontrol="task-tree-filter-header"> + <radiogroup id="task-tree-filtergroup" class="task-tree-subpane" + persist="value"> + <observes element="filterBroadcaster" + attribute="value" + onbroadcast="checkRadioControl(this.parentNode, document.getElementById('filterBroadcaster').getAttribute('value'));"/> + <radio id="opt_throughcurrent_filter" label="&calendar.task.filter.current.label;" value="throughcurrent" command="calendar_task_filter_command"/> + <radio id="opt_today_filter" label="&calendar.task.filter.today.label;" value="throughtoday" command="calendar_task_filter_command"/> + <radio id="opt_next7days_filter" label="&calendar.task.filter.next7days.label;" value="throughsevendays" command="calendar_task_filter_command"/> + <radio id="opt_notstarted_filter" label="&calendar.task.filter.notstarted.label;" value="notstarted" command="calendar_task_filter_command"/> + <radio id="opt_overdue_filter" label="&calendar.task.filter.overdue.label;" value="overdue" command="calendar_task_filter_command"/> + <radio id="opt_completed_filter" label="&calendar.task.filter.completed.label;" value="completed" command="calendar_task_filter_command"/> + <radio id="opt_open_filter" label="&calendar.task.filter.open.label;" value="open" command="calendar_task_filter_command"/> + <radio id="opt_all_filter" label="&calendar.task.filter.all.label;" value="all" command="calendar_task_filter_command"/> + </radiogroup> + </modevbox> + </modevbox> + <modevbox id="calendar-list-pane" flex="1" mode="calendar,task" broadcaster="modeBroadcaster" + refcontrol="calendar_toggle_calendarlist_command"> + <treenode-checkbox id="calendar-list-header" + checked="true" + class="treenode-checkbox" + ondrop="return document.getElementById('calendar-list-tree-widget').foreignDrop(event)" + ondragenter="return document.getElementById('calendar-list-tree-widget').foreignCanDrop(event)" + ondragover="return document.getElementById('calendar-list-tree-widget').foreignCanDrop(event)" + label="&calendar.list.header.label;"/> + <modevbox id="calendar-listtree-pane" flex="1" mode="calendar,task" broadcaster="modeBroadcaster" + refcontrol="calendar-list-header"> + + <calendar-list-tree id="calendar-list-tree-widget" + class="task-tree-subpane" + flex="1"/> + </modevbox> + </modevbox> + </vbox> + </vbox> + + <splitter id="calsidebar_splitter" + collapse="before" + persist="state" + class="calendar-sidebar-splitter"/> + + <deck id="calendarDisplayDeck" flex="1"> + <!-- vbox "calendar-view-box will be overlayed..." --> + <vbox id="calendar-view-box"/> + </deck> + </hbox> + </vbox> + </tabpanels> + + <hbox id="tabmail-container"> + <splitter id="today-splitter" + collapse="after" + resizebefore="closest" + state="collapsed" + class="calendar-sidebar-splitter" + oncommand="TodayPane.onCommandTodaySplitter();"> + <grippy/> + </splitter> + <modevbox id="today-pane-panel" /> + </hbox> + + <statusbar id="status-bar"> + <!-- event/task in tab statusbarpanels --> + <statusbarpanel id="status-privacy" + class="event-dialog" + align="center" + flex="1" + collapsed="true" + pack="start"> + <label value="&event.statusbarpanel.privacy.label;"/> + <hbox id="status-privacy-public-box" privacy="PUBLIC"> + <label value="&event.menu.options.privacy.public.label;"/> + </hbox> + <hbox id="status-privacy-confidential-box" privacy="CONFIDENTIAL"> + <label value="&event.menu.options.privacy.confidential.label;"/> + </hbox> + <hbox id="status-privacy-private-box" privacy="PRIVATE"> + <label value="&event.menu.options.privacy.private.label;"/> + </hbox> + </statusbarpanel> + <statusbarpanel id="status-priority" + class="event-dialog" + align="center" + flex="1" + collapsed="true" + pack="start"> + <label value="&event.priority2.label;"/> + <image id="image-priority-low" + class="cal-statusbar-1" + collapsed="true" + value="low"/> + <image id="image-priority-normal" + class="cal-statusbar-1" + collapsed="true" + value="normal"/> + <image id="image-priority-high" + class="cal-statusbar-1" + collapsed="true" + value="high"/> + </statusbarpanel> + <statusbarpanel id="status-status" + class="event-dialog" + align="center" + flex="1" + collapsed="true" + pack="start"> + <label value="&task.status.label;"/> + <label id="status-status-tentative-label" + value="&newevent.status.tentative.label;" + hidden="true"/> + <label id="status-status-confirmed-label" + value="&newevent.status.confirmed.label;" + hidden="true"/> + <label id="status-status-cancelled-label" + value="&newevent.eventStatus.cancelled.label;" + hidden="true"/> + </statusbarpanel> + <statusbarpanel id="status-freebusy" + class="event-only event-dialog" + align="center" + flex="1" + collapsed="true" + pack="start"> + <label value="&event.statusbarpanel.freebusy.label;"/> + <label id="status-freebusy-free-label" + value="&event.freebusy.legend.free;" + hidden="true"/> + <label id="status-freebusy-busy-label" + value="&event.freebusy.legend.busy;" + hidden="true"/> + </statusbarpanel> + <!-- end event/task in tab statusbarpanels --> + <statusbarpanel id="calendar-show-todaypane-panel" + pack="center"> + <toolbarbutton id="calendar-status-todaypane-button" + todaypane="true" + type="checkbox" + label="&todaypane.statusButton.label;" + tooltiptext="&calendar.todaypane.button.tooltip;" + observes="calendar_toggle_todaypane_command" + command="calendar_toggle_todaypane_command"/> + </statusbarpanel> + <statusbarpanel id="calendar-invitations-panel" + insertbefore="unreadMessageCount,totalMessageCount" + oncommand="openInvitationsDialog()"> + <label id="calendar-invitations-label" + class="text-link" + onclick="openInvitationsDialog()" + onkeypress="if (event.keyCode == event.VK_RETURN) { + openInvitationsDialog(); }"/> + </statusbarpanel> + </statusbar> +</overlay> diff --git a/calendar/lightning/content/suite-overlay-addons.xul b/calendar/lightning/content/suite-overlay-addons.xul new file mode 100644 index 000000000..975a0835e --- /dev/null +++ b/calendar/lightning/content/suite-overlay-addons.xul @@ -0,0 +1,39 @@ +<?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/. --> + +<overlay id="suiteAddonsOverlay" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript"><![CDATA[ + var lightningPrefs = { + guid: "{e2fda1a4-762b-4020-b5ad-a41df1933103}", + handleEvent: function(aEvent) { + var item = gListView.getListItemForID(this.guid); + if (!item) + return; + + item.showPreferences = this.showPreferences; + }, + showPreferences: function() { + var win = Services.wm.getMostRecentWindow("mozilla:preferences"); + if (win) { + win.focus(); + var doc = win.document; + var pane = doc.getElementById("paneLightning"); + doc.documentElement.syncTreeWithPane(pane, true); + } else { + openDialog("chrome://communicator/content/pref/preferences.xul", + "PrefWindow", + "non-private,chrome,titlebar,dialog=no,resizable", + "paneLightning"); + } + }, + }; + + window.addEventListener("ViewChanged", lightningPrefs, false); + ]]></script> + +</overlay> diff --git a/calendar/lightning/content/suite-overlay-preferences.xul b/calendar/lightning/content/suite-overlay-preferences.xul new file mode 100644 index 000000000..976471527 --- /dev/null +++ b/calendar/lightning/content/suite-overlay-preferences.xul @@ -0,0 +1,68 @@ +<?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/. --> + +<?xml-stylesheet href="chrome://lightning/skin/lightning.css"?> + +<?xul-overlay href="chrome://calendar/content/preferences/general.xul"?> +<?xul-overlay href="chrome://calendar/content/preferences/alarms.xul"?> +<?xul-overlay href="chrome://calendar/content/preferences/categories.xul"?> +<?xul-overlay href="chrome://calendar/content/preferences/views.xul"?> + +<!DOCTYPE overlay [ + <!ENTITY % lightningDTD SYSTEM "chrome://lightning/locale/lightning.dtd"> + %lightningDTD; + <!ENTITY % preferencesDTD SYSTEM "chrome://calendar/locale/preferences/preferences.dtd"> + %preferencesDTD; +]> + +<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <treechildren id="prefsPanelChildren"> + <treeitem container="true" + id="lightningItem" + insertafter="mailnewsItem,navigatorItem" + label="&lightning.preferencesLabel;" + prefpane="paneLightning"> + <treechildren id="lightningChildren"> + <treeitem id="lightningAlarms" + label="&paneAlarms.title;" + prefpane="paneLightningAlarms"/> + <treeitem id="lightningCategories" + label="&paneCategories.title;" + prefpane="paneLightningCategories"/> + <treeitem id="lightningViews" + label="&paneViews.title;" + prefpane="paneLightningViews"/> + </treechildren> + </treeitem> + </treechildren> + + <prefwindow id="prefDialog"> + <prefpane id="paneLightning" + label="&lightning.preferencesLabel;" + onpaneload="gCalendarGeneralPane.init();"> + <vbox id="calPreferencesBoxGeneral"/> + </prefpane> + <prefpane id="paneLightningAlarms" + label="&paneAlarms.title;" + onpaneload="gAlarmsPane.init();"> + <vbox id="calPreferencesBoxAlarms"/> + </prefpane> + <prefpane id="paneLightningCategories" + label="&paneCategories.title;" + onpaneload="gCategoriesPane.init();"> + <vbox id="calPreferencesBoxCategories"/> + </prefpane> + <prefpane id="paneLightningViews" + label="&paneViews.title;" + onpaneload="gViewsPane.init();"> + <vbox id="calPreferencesBoxViews"/> + </prefpane> + </prefwindow> + + <script type="application/javascript" + src="chrome://calendar/content/calUtils.js"/> +</overlay> diff --git a/calendar/lightning/content/suite-overlay-sidebar.js b/calendar/lightning/content/suite-overlay-sidebar.js new file mode 100644 index 000000000..0ba313dd1 --- /dev/null +++ b/calendar/lightning/content/suite-overlay-sidebar.js @@ -0,0 +1,47 @@ +/* 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/. */ + +Components.utils.import("resource://gre/modules/Services.jsm"); + +var ltnSuiteUtils = { + + addStartupObserver: function() { + Services.obs.addObserver(this.startupObserver, "lightning-startup-done", false); + Services.obs.addObserver(this.startupObserver, "calendar-taskview-startup-done", + false); + }, + + startupObserver: { + observe: function(subject, topic, state) { + if (topic != "lightning-startup-done" && + topic != "calendar-taskview-startup-done") { + return; + } + + const ids = [ + ["CustomizeTaskActionsToolbar", "task-actions-toolbox"], + ["CustomizeCalendarToolbar", "calendar-toolbox"], + ["CustomizeTaskToolbar", "task-toolbox"] + ]; + + ids.forEach(([itemID, toolboxID]) => { + let item = document.getElementById(itemID); + let toolbox = document.getElementById(toolboxID); + toolbox.customizeInit = function() { + item.setAttribute("disabled", "true"); + toolboxCustomizeInit("mail-menubar"); + }; + toolbox.customizeDone = function(aToolboxChanged) { + item.removeAttribute("disabled"); + toolboxCustomizeDone("mail-menubar", toolbox, aToolboxChanged); + }; + toolbox.customizeChange = function(aEvent) { + toolboxCustomizeChange(toolbox, aEvent); + }; + }); + } + } +}; + +ltnSuiteUtils.addStartupObserver(); diff --git a/calendar/lightning/content/suite-overlay-sidebar.xul b/calendar/lightning/content/suite-overlay-sidebar.xul new file mode 100644 index 000000000..4e00f7e48 --- /dev/null +++ b/calendar/lightning/content/suite-overlay-sidebar.xul @@ -0,0 +1,42 @@ +<?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/. --> + +<overlay id="suiteSidebarOverlay" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" src="chrome://lightning/content/suite-overlay-sidebar.js"/> + + <key id="openLightningKey" removeelement="true"/> + <key id="openTasksKey" removeelement="true"/> + <key id="calendar-new-event-key" removeelement="true"/> + <key id="calendar-new-todo-key" removeelement="true"/> + + <menuitem id="CustomizeTaskActionsToolbar" + oncommand="goCustomizeToolbar(document.getElementById('task-actions-toolbox'))"/> + + <toolbox id="calendar-toolbox" + defaultlabelalign="end" + xpfe="false"/> + <toolbox id="task-toolbox" + defaultlabelalign="end" + xpfe="false"/> + <toolbox id="task-actions-toolbox" + defaultlabelalign="end" + xpfe="false"/> + + <toolbar id="calendar-toolbar2" + defaultlabelalign="end" + context="toolbar-context-menu"/> + <toolbar id="task-toolbar2" + defaultlabelalign="end" + context="toolbar-context-menu"/> + <toolbar id="task-actions-toolbar" + context="toolbar-context-menu"/> + + <toolbarset id="calendarToolbars" context="toolbar-context-menu"/> + <toolbarset id="taskToolbars" context="toolbar-context-menu"/> + +</overlay> diff --git a/calendar/lightning/install.rdf b/calendar/lightning/install.rdf new file mode 100644 index 000000000..fd34bf63c --- /dev/null +++ b/calendar/lightning/install.rdf @@ -0,0 +1,33 @@ +<?xml version="1.0"?> +#filter substitution +<!-- 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/. --> + +<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:em="http://www.mozilla.org/2004/em-rdf#"> + + <Description about="urn:mozilla:install-manifest"> + <!-- Target Application this extension can install into, + with minimum and maximum supported versions. --> + <em:targetApplication> + <Description> + <!-- Interlink --> + <em:id>{3550f703-e582-4d05-9a08-453d09bdfdc6}</em:id> + <em:minVersion>@THUNDERBIRD_VERSION@</em:minVersion> + <em:maxVersion>@THUNDERBIRD_MAXVERSION@</em:maxVersion> + </Description> + </em:targetApplication> + <em:id>@XPI_EM_ID@</em:id> + <em:name>Lightning</em:name> + <em:version>@LIGHTNING_VERSION@</em:version> + <em:description>Integrated Calendaring & Scheduling for your Email client</em:description> + <em:creator>Mozilla Calendar Project</em:creator> + <em:homepageURL>https://www.mozilla.org/projects/calendar/</em:homepageURL> + <em:iconURL>chrome://calendar/skin/cal-icon32.png</em:iconURL> + <em:optionsURL>chrome://messenger/content/preferences/preferences.xul</em:optionsURL> + <em:targetPlatform>@TARGET_PLATFORM@</em:targetPlatform> + <em:unpack>true</em:unpack> + <em:strictCompatibility>true</em:strictCompatibility> + </Description> +</RDF> diff --git a/calendar/lightning/jar.mn b/calendar/lightning/jar.mn new file mode 100644 index 000000000..4428ef5ba --- /dev/null +++ b/calendar/lightning/jar.mn @@ -0,0 +1,111 @@ +#filter substitution +# 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/. + +lightning.jar: +% override chrome://messagebody/skin/imip.css chrome://lightning/skin/imip.css +% override chrome://messagebody/skin/calendar-event-dialog-attendees.png chrome://calendar-common/skin/calendar-event-dialog-attendees.png +% overlay chrome://messenger/content/messenger.xul chrome://lightning/content/lightning-migration.xul +% overlay chrome://messenger/content/messenger.xul chrome://lightning/content/lightning-item-panel.xul +% overlay chrome://messenger/content/msgAccountCentral.xul chrome://lightning/content/messenger-overlay-accountCentral.xul +% overlay chrome://messenger/content/messenger.xul chrome://lightning/content/messenger-overlay-sidebar.xul +% overlay chrome://messenger/content/messageWindow.xul chrome://lightning/content/imip-bar-overlay.xul +% overlay chrome://messenger/content/messageWindow.xul chrome://lightning/content/messenger-overlay-messageWindow.xul +% overlay chrome://lightning/content/messenger-overlay-sidebar.xul chrome://lightning/content/imip-bar-overlay.xul +% overlay chrome://communicator/content/pref/preferences.xul chrome://lightning/content/suite-overlay-preferences.xul application={92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a} +% overlay about:addons chrome://lightning/content/suite-overlay-addons.xul application={92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a} +% overlay chrome://mozapps/content/extensions/extensions.xul chrome://lightning/content/suite-overlay-addons.xul application={92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a} +% overlay chrome://messenger/content/preferences/preferences.xul chrome://lightning/content/messenger-overlay-preferences.xul +% overlay about:preferences chrome://lightning/content/messenger-overlay-preferences.xul +% overlay chrome://messenger/content/preferences/preferences.xul chrome://calendar/content/preferences/alarms.xul +% overlay about:preferences chrome://calendar/content/preferences/alarms.xul +% overlay chrome://messenger/content/preferences/preferences.xul chrome://calendar/content/preferences/categories.xul +% overlay about:preferences chrome://calendar/content/preferences/categories.xul +% overlay chrome://messenger/content/preferences/preferences.xul chrome://calendar/content/preferences/general.xul +% overlay about:preferences chrome://calendar/content/preferences/general.xul +% overlay chrome://messenger/content/preferences/preferences.xul chrome://calendar/content/preferences/views.xul +% overlay about:preferences chrome://calendar/content/preferences/views.xul +% overlay chrome://lightning/content/messenger-overlay-sidebar.xul chrome://calendar/content/calendar-unifinder.xul +% overlay chrome://lightning/content/messenger-overlay-sidebar.xul chrome://calendar/content/calendar-unifinder-todo.xul +% overlay chrome://lightning/content/messenger-overlay-sidebar.xul chrome://calendar/content/calendar-task-view.xul +% overlay chrome://lightning/content/messenger-overlay-sidebar.xul chrome://calendar/content/today-pane.xul +% overlay chrome://lightning/content/messenger-overlay-sidebar.xul chrome://lightning/content/suite-overlay-sidebar.xul application={92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a} +% overlay chrome://calendar/content/calendarCreation.xul chrome://lightning/content/lightning-calendar-creation.xul +% overlay chrome://calendar/content/calendar-properties-dialog.xul chrome://lightning/content/lightning-calendar-properties.xul +% override chrome://lightning/skin/accountCentral.css chrome://lightning-common/skin/suite-accountCentral.css application={92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a} +% content lightning %content/lightning/ + content/lightning/imip-bar.js (content/imip-bar.js) + content/lightning/imip-bar-overlay.xul (content/imip-bar-overlay.xul) + content/lightning/lightning-calendar-creation.xul (content/lightning-calendar-creation.xul) + content/lightning/lightning-calendar-creation.js (content/lightning-calendar-creation.js) + content/lightning/lightning-calendar-properties.xul (content/lightning-calendar-properties.xul) + content/lightning/lightning-calendar-properties.js (content/lightning-calendar-properties.js) + content/lightning/lightning-invitation.xhtml (content/lightning-invitation.xhtml) + content/lightning/lightning-menus.xul (content/lightning-menus.xul) + content/lightning/lightning-migration.xul (content/lightning-migration.xul) +* content/lightning/lightning-toolbar.xul (content/lightning-toolbar.xul) + content/lightning/lightning-utils.js (content/lightning-utils.js) + content/lightning/lightning-widgets.css (content/lightning-widgets.css) + content/lightning/lightning-widgets.xml (content/lightning-widgets.xml) + content/lightning/messenger-overlay-accountCentral.xul (content/messenger-overlay-accountCentral.xul) + content/lightning/messenger-overlay-messageWindow.xul (content/messenger-overlay-messageWindow.xul) + content/lightning/messenger-overlay-sidebar.js (content/messenger-overlay-sidebar.js) + content/lightning/messenger-overlay-sidebar.xul (content/messenger-overlay-sidebar.xul) + content/lightning/messenger-overlay-preferences.js (content/messenger-overlay-preferences.js) + content/lightning/messenger-overlay-preferences.xul (content/messenger-overlay-preferences.xul) + content/lightning/suite-overlay-addons.xul (content/suite-overlay-addons.xul) + content/lightning/suite-overlay-preferences.xul (content/suite-overlay-preferences.xul) + content/lightning/suite-overlay-sidebar.js (content/suite-overlay-sidebar.js) + content/lightning/suite-overlay-sidebar.xul (content/suite-overlay-sidebar.xul) + content/lightning/lightning-item-toolbar.xul (content/lightning-item-toolbar.xul) +* content/lightning/lightning-item-panel.xul (content/lightning-item-panel.xul) + content/lightning/lightning-item-panel.js (content/lightning-item-panel.js) + content/lightning/lightning-item-iframe.xul (content/lightning-item-iframe.xul) + content/lightning/lightning-item-iframe.js (content/lightning-item-iframe.js) + content/lightning/html-item-editing/lightning-item-iframe.html (content/html-item-editing/lightning-item-iframe.html) + content/lightning/html-item-editing/react-code.js (content/html-item-editing/react-code.js) +% skin lightning classic/1.0 chrome/skin/linux/lightning/ +% skin lightning classic/1.0 chrome/skin/windows/lightning/ os=WINNT +% skin lightning-common classic/1.0 chrome/skin/lightning-common/ +% style chrome://global/content/customizeToolbar.xul chrome://lightning/skin/lightning-toolbar.css +% style chrome://calendar/content/calendar-event-dialog.xul chrome://communicator/skin/communicator.css application={92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a} +% style chrome://global/content/customizeToolbar.xul chrome://lightning-common/skin/lightning.css application={92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a} +% style chrome://global/content/customizeToolbar.xul chrome://calendar-common/skin/dialogs/calendar-event-dialog.css application={92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a} + ../skin/lightning-common/mode-switch-icons.png (themes/common/images/mode-switch-icons.png) + ../skin/lightning-common/suite-accountCentral.css (themes/common/suite-accountCentral.css) + +# Linux theme files + ../skin/linux/lightning/accountCentral.css (themes/linux/accountCentral.css) + ../skin/linux/lightning/imip.css (themes/linux/imip.css) + ../skin/linux/lightning/lightning.css (themes/linux/lightning.css) + ../skin/linux/lightning/lightning-toolbar.css (themes/linux/lightning-toolbar.css) + ../skin/linux/lightning/lightning-widgets.css (themes/linux/lightning-widgets.css) + +# Windows theme files + ../skin/windows/lightning/accountCentral.css (themes/windows/accountCentral.css) + ../skin/windows/lightning/imip.css (themes/windows/imip.css) + ../skin/windows/lightning/imip.png (themes/windows/imip.png) + ../skin/windows/lightning/lightning.css (themes/windows/lightning.css) + ../skin/windows/lightning/lightning-toolbar.css (themes/windows/lightning-toolbar.css) + ../skin/windows/lightning/lightning-widgets.css (themes/windows/lightning-widgets.css) + ../skin/windows/lightning/imip-aero.png (themes/windows/images/imip-aero.png) + ../skin/windows/lightning/mode-switch-icons-aero.png (themes/windows/images/mode-switch-icons-aero.png) + ../skin/windows/lightning/mode-switch-icons-inverted.png (themes/windows/images/mode-switch-icons-inverted.png) + + +calendar.jar: + content/calendar/calendarCreation.xul (/calendar/resources/content/calendarCreation.xul) + content/calendar/calendarCreation.js (/calendar/resources/content/calendarCreation.js) + content/calendar/datetimepickers/datetimepickers.css (/calendar/resources/content/datetimepickers/datetimepickers.css) + content/calendar/datetimepickers/datetimepickers.xml (/calendar/resources/content/datetimepickers/datetimepickers.xml) + content/calendar/mouseoverPreviews.js (/calendar/resources/content/mouseoverPreviews.js) + content/calendar/publish.js (/calendar/resources/content/publish.js) + content/calendar/publishDialog.js (/calendar/resources/content/publishDialog.js) + content/calendar/publishDialog.xul (/calendar/resources/content/publishDialog.xul) + content/calendar/sound.wav (/calendar/resources/content/sound.wav) + ../skin/lightning-common/datetimepickers.css (/calendar/resources/skin/datetimepickers.css) + ../skin/lightning-common/dialogOverlay.css (/calendar/resources/skin/dialogOverlay.css) + ../skin/lightning-common/imip.css (themes/common/imip.css) + ../skin/lightning-common/lightning.css (themes/common/lightning.css) + ../skin/lightning-common/html-item-editing.css (themes/common/html-item-editing.css) diff --git a/calendar/lightning/lightning-packager.mk b/calendar/lightning/lightning-packager.mk new file mode 100644 index 000000000..8e36ce1b7 --- /dev/null +++ b/calendar/lightning/lightning-packager.mk @@ -0,0 +1,196 @@ +# 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/. + +# NOTE: The packager is not only used in calendar/lightning but should be +# general enough to be able to repackage other sub-extensions like +# calendar/providers/gdata. This means no lightning-specific files, no version +# numbers directly from lightning and be careful with relative paths. + +# This packager can be used to repackage extensions. To use it, set the +# following variables in your Makefile, then include this file. +# XPI_NAME = lightning # The extension path name +# XPI_PKGNAME = lightning-2.2.en-US.mac # The extension package name +# XPI_VERSION = 2.2 # The extension version +# +# The following variables are optional: +# XPI_NO_UNIVERSAL = 1 # If set, no universal path is used on mac +# +# For the upload target to work, you also need to set: +# LIGHTNING_VERSION = 2.2 # Will be used to replace the Thunderbird version +# # in POST_UPLOAD_CMD + +include $(MOZILLA_SRCDIR)/system/installer/package-name.mk + +# Set the univeral path only if we are building a univeral binary and it was +# not restricted by the calling makefile +ifeq ($(UNIVERSAL_BINARY)|$(XPI_NO_UNIVERSAL),1|) +UNIVERSAL_PATH=universal/ +else +UNIVERSAL_PATH= +endif + +XPI_STAGE_PATH = $(DIST)/$(UNIVERSAL_PATH)xpi-stage +_ABS_XPI_STAGE_PATH = $(ABS_DIST)/$(UNIVERSAL_PATH)xpi-stage +ENUS_PKGNAME=$(subst .$(AB_CD).,.en-US.,$(XPI_PKGNAME)) +XPI_ZIP_IN=$(_ABS_XPI_STAGE_PATH)/$(ENUS_PKGNAME).xpi + + +# This variable is to allow the wget-en-US target to know which ftp server to download from +ifndef EN_US_BINARY_URL +ifdef DOWNLOAD_HOST +# If this url is missing, and DOWNLOAD_HOST is defined its probably the release +# run where we can't influence the download location. Fake it from the env vars +# we have +BUILD_NR=$(shell echo $(POST_UPLOAD_CMD) | sed -n -e 's/.*-n \([0-9]*\).*/\1/p') +CANDIDATE_NR=$(if $(LIGHTNING_VERSION),$(LIGHTNING_VERSION),$(XPI_VERSION)) +EN_US_BINARY_URL=http://$(DOWNLOAD_HOST)/pub/calendar/lightning/candidates/$(CANDIDATE_NR)-candidates/build$(BUILD_NR)/$(MOZ_PKG_PLATFORM) +endif +endif + +# Check if EN_US_BINARY_URL has finally been set +ifdef EN_US_BINARY_URL +# If so, we are expected to unpack when the language pack is created +ensure-stage-dir: wget-en-US unpack +else +# If not, use the existing lightning from xpi-stage, or warn that the var is not set. +ensure-stage-dir: +ifeq (,$(wildcard $(XPI_STAGE_PATH)/$(XPI_NAME)/)) + $(error You must set EN_US_BINARY_URL) +endif +endif + +$(XPI_STAGE_PATH): + mkdir -p $@ + +# Target Directory used for the l10n files +L10N_TARGET = $(XPI_STAGE_PATH)/$(XPI_NAME)-$(AB_CD) + +# Short name of the OS used in shipped-locales file. There are no +# special cases, so assume linux for everything. +SHORTOS = linux + +# function print_ltnconfig(section,configname) +print_ltnconfig = $(shell $(PYTHON) $(MOZILLA_SRCDIR)/config/printconfigsetting.py $(XPI_STAGE_PATH)/$(XPI_NAME)/app.ini $1 $2) + +wget-en-US: +ifeq (thunderbird,$(MOZ_APP_NAME)) +FINAL_BINARY_URL = $(subst thunderbird,calendar/lightning,$(EN_US_BINARY_URL)) +else +FINAL_BINARY_URL = $(subst seamonkey,calendar/lightning,$(subst latest-comm-central-trunk,latest-comm-central,$(EN_US_BINARY_URL))) +endif +wget-en-US: $(XPI_STAGE_PATH) + (cd $(XPI_STAGE_PATH) && $(WGET) -nv -N $(FINAL_BINARY_URL)/$(ENUS_PKGNAME).xpi) + @echo "Downloaded $(FINAL_BINARY_URL)/$(ENUS_PKGNAME) to $(XPI_ZIP_IN)" + + +# We're unpacking directly into FINAL_TARGET, this keeps code to do manual +# repacks cleaner. +unpack: $(XPI_ZIP_IN) + if test -d $(XPI_STAGE_PATH)/$(XPI_NAME); then \ + $(RM) -r -v $(XPI_STAGE_PATH)/$(XPI_NAME); \ + fi + $(NSINSTALL) -D $(XPI_STAGE_PATH)/$(XPI_NAME) + cd $(XPI_STAGE_PATH)/$(XPI_NAME) && $(UNZIP) $(XPI_ZIP_IN) + @echo done unpacking + +# Nothing to package for en-US, its just the usual english xpi +langpack-en-US: + @echo "Skipping $@ as en-US is the default" + +merge-%: +ifdef LOCALE_MERGEDIR + $(RM) -rf $(LOCALE_MERGEDIR)/calendar + $(MOZILLA_SRCDIR)/mach compare-locales \ + --merge-dir $(LOCALE_MERGEDIR) \ + --l10n-ini $(topsrcdir)/calendar/locales/l10n.ini \ + $* + + # This file requires a bugfix with string changes, see bug 1154448 + [ -f $(L10NBASEDIR)/$*/calendar/chrome/calendar/calendar-extract.properties ] && \ + $(RM) $(LOCALE_MERGEDIR)/calendar/chrome/calendar/calendar-extract.properties \ + || true +else + @echo "Not merging Lightning locales due to missing LOCALE_MERGEDIR" +endif + +# Calling these targets with prerequisites causes the libs and subsequent +# targets to be switched in order due to some make voodoo. Therefore we call +# the targets explicitly, which seems to work better. +langpack-%: L10N_XPI_NAME=$(XPI_NAME)-$* +langpack-%: L10N_XPI_PKGNAME=$(subst $(AB_CD),$*,$(XPI_PKGNAME)) +langpack-%: AB_CD=$* +langpack-%: ensure-stage-dir + $(MAKE) L10N_XPI_NAME=$(L10N_XPI_NAME) L10N_XPI_PKGNAME=$(L10N_XPI_PKGNAME) AB_CD=$(AB_CD) \ + recreate-platformini repack-stage repack-process-extrafiles libs-$(AB_CD) + @echo "Done packaging $(L10N_XPI_PKGNAME).xpi" + +clobber-%: AB_CD=$* +clobber-%: + $(RM) -r $(L10N_TARGET) + +repackage-zip-%: + @echo "Already repackaged zip for $* in langpack step" + +repack-stage: + @echo "Repackaging $(XPI_PKGNAME) locale for Language $(AB_CD)" + $(RM) -rf $(L10N_TARGET) + cp -R $(XPI_STAGE_PATH)/$(XPI_NAME) $(L10N_TARGET) + grep -v '^locale [a-z\-]\+ en-US' $(L10N_TARGET)/chrome.manifest > $(L10N_TARGET)/chrome.manifest~ && \ + mv $(L10N_TARGET)/chrome.manifest~ $(L10N_TARGET)/chrome.manifest + find $(abspath $(L10N_TARGET)) -name '*en-US*' -print0 | xargs -0 rm -rf + + +# Actual locale packaging targets. If L10N_XPI_NAME is set, then use it. +# Otherwise keep the original XPI_NAME +# Overriding the final target is a bit of a hack for universal builds +# so that we can ensure we get the right xpi that gets repacked. +libs-%: FINAL_XPI_NAME=$(if $(L10N_XPI_NAME),$(L10N_XPI_NAME),$(XPI_NAME)) +libs-%: FINAL_XPI_PKGNAME=$(if $(L10N_XPI_PKGNAME),$(L10N_XPI_PKGNAME),$(XPI_PKGNAME)) +libs-%: + $(MAKE) -C locales libs AB_CD=$* FINAL_TARGET=$(ABS_DIST)/$(UNIVERSAL_PATH)xpi-stage/$(FINAL_XPI_NAME) \ + XPI_NAME=$(FINAL_XPI_NAME) XPI_PKGNAME=$(FINAL_XPI_PKGNAME) USE_EXTENSION_MANIFEST=1 + $(MAKE) -C locales tools AB_CD=$* FINAL_TARGET=$(ABS_DIST)/$(UNIVERSAL_PATH)xpi-stage/$(FINAL_XPI_NAME) \ + XPI_NAME=$(FINAL_XPI_NAME) XPI_PKGNAME=$(FINAL_XPI_PKGNAME) USE_EXTENSION_MANIFEST=1 + +# The calling makefile might need to process some extra files. Provide an empty +# rule to overwrite +repack-process-extrafiles: + +# When repackaging Lightning from the builder, platform.ini is not yet created. +# Recreate it from the app.ini bundled with the downloaded xpi. +$(DIST)/bin/platform.ini: + mkdir -p $(@D) + echo "[Build]" >> $(DIST)/bin/platform.ini + echo "Milestone=$(call print_ltnconfig,Gecko,MaxVersion)" >> $(DIST)/bin/platform.ini + echo "SourceStamp=$(call print_ltnconfig,Build,SourceStamp)" >> $(DIST)/bin/platform.ini + echo "SourceRepository=$(call print_ltnconfig,Build,SourceRepository)" >> $(DIST)/bin/platform.ini + echo "BuildID=$(call print_ltnconfig,App,BuildID)" >> $(DIST)/bin/platform.ini + +recreate-platformini: $(DIST)/bin/platform.ini + + +# Lightning uses Thunderbird's build machinery, so we need to hack the post +# upload command to use Lightning's directories and version. +upload: upload-$(AB_CD) +upload-%: LTN_UPLOAD_CMD := $(patsubst $(THUNDERBIRD_VERSION)%,$(LIGHTNING_VERSION),$(subst thunderbird,calendar/lightning,$(POST_UPLOAD_CMD))) +upload-%: stage_upload + POST_UPLOAD_CMD="$(LTN_UPLOAD_CMD)" \ + $(PYTHON) $(MOZILLA_DIR)/build/upload.py --base-path $(DIST) \ + --properties-file $(DIST)/$(XPI_NAME)_build_properties.json \ + "$(DIST)/$(MOZ_PKG_PLATFORM)/$(XPI_PKGNAME).xpi" + +stage_upload: + $(NSINSTALL) -D $(DIST)/$(MOZ_PKG_PLATFORM) + $(call install_cmd,$(IFLAGS1) $(XPI_STAGE_PATH)/$(XPI_PKGNAME).xpi $(DIST)/$(MOZ_PKG_PLATFORM)) + +ifdef XPI_INSTALL_EXTENSION +ifndef XPI_NAME +$(error XPI_NAME must be set for XPI_INSTALL_EXTENSION) +endif +tools:: + $(RM) -r '$(DIST)/bin$(DIST_SUBDIR:%=/%)/extensions/$(XPI_INSTALL_EXTENSION)' + $(NSINSTALL) -D '$(DIST)/bin$(DIST_SUBDIR:%=/%)/extensions/$(XPI_INSTALL_EXTENSION)' + $(call copy_dir,$(FINAL_TARGET),$(DIST)/bin$(DIST_SUBDIR:%=/%)/extensions/$(XPI_INSTALL_EXTENSION)) + +endif diff --git a/calendar/lightning/lightning-tests.mk b/calendar/lightning/lightning-tests.mk new file mode 100644 index 000000000..334ca6769 --- /dev/null +++ b/calendar/lightning/lightning-tests.mk @@ -0,0 +1,23 @@ +# 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/. + +PKG_STAGE = $(DIST)/test-stage + +# This is the target that should be called externally +stage-package: stage-extension stage-mozmill + +# stage the extension, avoiding per-platform differences so that the mac unify +# target works. +stage-extension: + $(NSINSTALL) -D $(PKG_STAGE)/extensions/$(XPI_EM_ID) + (cd $(FINAL_TARGET) && tar $(TAR_CREATE_FLAGS) - *) | (cd $(PKG_STAGE)/extensions/$(XPI_EM_ID) && tar -xf -) + grep -v em:targetPlatform $(FINAL_TARGET)/install.rdf > $(PKG_STAGE)/extensions/$(XPI_EM_ID)/install.rdf + +# stage mozmill tests and shared modules. Cross your fingers that there are no +# name conflicts between calendar/ and mail/ +stage-mozmill: + $(NSINSTALL) -D $(PKG_STAGE)/mozmill/shared-modules + (cd $(topsrcdir)/calendar/test/mozmill && tar $(TAR_CREATE_FLAGS) - `cat $(topsrcdir)/calendar/test/mozmill/mozmilltests.list`) | (cd $(PKG_STAGE)/mozmill && tar -xf -) + (cd $(topsrcdir)/calendar/test/mozmill/shared-modules && tar $(TAR_CREATE_FLAGS) - *) | (cd $(PKG_STAGE)/mozmill/shared-modules && tar -xf -) + $(call py_action,buildlist,$(PKG_STAGE)/mozmill/mozmilltests.list $(shell cat $(topsrcdir)/calendar/test/mozmill/mozmilltests.list)) diff --git a/calendar/lightning/locales/Makefile.in b/calendar/lightning/locales/Makefile.in new file mode 100644 index 000000000..1ef5dd430 --- /dev/null +++ b/calendar/lightning/locales/Makefile.in @@ -0,0 +1,11 @@ +# 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/. + +# Setting this to calendar/locales sets up LOCALE_SRCDIR to the correct locale +# directory +relativesrcdir = calendar/locales + +DEFINES += -DTHEME=$(THEME) \ + -DLOCALE_SRCDIR=$(LOCALE_SRCDIR) \ + $(NULL) diff --git a/calendar/lightning/locales/jar.mn b/calendar/lightning/locales/jar.mn new file mode 100644 index 000000000..aabcdcc74 --- /dev/null +++ b/calendar/lightning/locales/jar.mn @@ -0,0 +1,11 @@ +#filter substitution +# 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/. + + +lightning-@AB_CD@.jar: +% locale lightning @AB_CD@ %locale/@AB_CD@/lightning/ + locale/@AB_CD@/lightning/lightning.dtd (%chrome/lightning/lightning.dtd) + locale/@AB_CD@/lightning/lightning-toolbar.dtd (%chrome/lightning/lightning-toolbar.dtd) + locale/@AB_CD@/lightning/lightning.properties (%chrome/lightning/lightning.properties) diff --git a/calendar/lightning/locales/moz.build b/calendar/lightning/locales/moz.build new file mode 100644 index 000000000..7146412a6 --- /dev/null +++ b/calendar/lightning/locales/moz.build @@ -0,0 +1,8 @@ +# vim: set filetype=python: +# 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/. + +DIRS += ['../../locales'] + +JAR_MANIFESTS += ['jar.mn'] diff --git a/calendar/lightning/modules/ltnInvitationUtils.jsm b/calendar/lightning/modules/ltnInvitationUtils.jsm new file mode 100644 index 000000000..3b50f25c4 --- /dev/null +++ b/calendar/lightning/modules/ltnInvitationUtils.jsm @@ -0,0 +1,588 @@ +/* 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/. */ + +Components.utils.import("resource://calendar/modules/calUtils.jsm"); +Components.utils.import("resource://calendar/modules/calXMLUtils.jsm"); +Components.utils.import("resource://calendar/modules/calRecurrenceUtils.jsm"); +Components.utils.import("resource://gre/modules/Preferences.jsm"); +Components.utils.import("resource://calendar/modules/ltnUtils.jsm"); +Components.utils.import("resource:///modules/mailServices.js"); + +this.EXPORTED_SYMBOLS = ["ltn"]; // even though it's defined in ltnUtils.jsm, import needs this +ltn.invitation = { + /** + * Returns a header title for an ITIP item depending on the response method + * @param {calItipItem} aItipItem the itip item to check + * @return {String} the header title + */ + getItipHeader: function(aItipItem) { + let header; + + if (aItipItem) { + let item = aItipItem.getItemList({})[0]; + let summary = item.getProperty("SUMMARY") || ""; + let organizer = item.organizer; + let organizerString = (organizer) ? + (organizer.commonName || organizer.toString()) : ""; + + switch (aItipItem.responseMethod) { + case "REQUEST": + header = ltn.getString("lightning", + "itipRequestBody", + [organizerString, summary]); + break; + case "CANCEL": + header = ltn.getString("lightning", + "itipCancelBody", + [organizerString, summary]); + break; + case "COUNTER": + // falls through + case "REPLY": + let attendees = item.getAttendees({}); + let sender = cal.getAttendeesBySender(attendees, aItipItem.sender); + if (sender.length == 1) { + if (aItipItem.responseMethod == "COUNTER") { + header = cal.calGetString("lightning", + "itipCounterBody", + [sender[0].toString(), summary], + "lightning"); + } else { + let statusString = (sender[0].participationStatus == "DECLINED" ? + "itipReplyBodyDecline" : "itipReplyBodyAccept"); + header = cal.calGetString("lightning", + statusString, + [sender[0].toString()], + "lightning"); + } + } else { + header = ""; + } + break; + case "DECLINECOUNTER": + header = ltn.getString("lightning", + "itipDeclineCounterBody", + [organizerString, summary]); + break; + } + } + + if (!header) { + header = ltn.getString("lightning", "imipHtml.header", null); + } + + return header; + }, + + /** + * Returns the html representation of the event as a DOM document. + * + * @param {calIItemBase} aEvent The event to parse into html. + * @param {calItipItem} aItipItem The itip item, which containes aEvent. + * @return {DOM} The html representation of aEvent. + */ + createInvitationOverlay: function(aEvent, aItipItem) { + // Creates HTML using the Node strings in the properties file + let doc = cal.xml.parseFile("chrome://lightning/content/lightning-invitation.xhtml"); + let formatter = cal.getDateFormatter(); + + let linkConverter = Components.classes["@mozilla.org/txttohtmlconv;1"] + .getService(Components.interfaces.mozITXTToHTMLConv); + + let field = function(aField, aContentText, aConvert) { + let descr = doc.getElementById("imipHtml-" + aField + "-descr"); + if (descr) { + let labelText = ltn.getString("lightning", "imipHtml." + aField, null); + descr.textContent = labelText; + } + + if (aContentText) { + let content = doc.getElementById("imipHtml-" + aField + "-content"); + doc.getElementById("imipHtml-" + aField + "-row").hidden = false; + if (aConvert) { + // we convert special characters first to not mix up html conversion + let mode = Components.interfaces.mozITXTToHTMLConv.kEntities; + let contentText = linkConverter.scanTXT(aContentText, mode); + try { + // kGlyphSubstitution may lead to unexpected results when used in scanHTML + mode = Components.interfaces.mozITXTToHTMLConv.kStructPhrase + + Components.interfaces.mozITXTToHTMLConv.kGlyphSubstitution + + Components.interfaces.mozITXTToHTMLConv.kURLs; + content.innerHTML = linkConverter.scanHTML(contentText, mode); + } catch (e) { + mode = Components.interfaces.mozITXTToHTMLConv.kStructPhrase + + Components.interfaces.mozITXTToHTMLConv.kURLs; + content.innerHTML = linkConverter.scanHTML(contentText, mode); + } + } else { + content.textContent = aContentText; + } + } + }; + + // Simple fields + let headerDescr = doc.getElementById("imipHtml-header-descr"); + if (headerDescr) { + headerDescr.textContent = ltn.invitation.getItipHeader(aItipItem); + } + + field("summary", aEvent.title, true); + field("location", aEvent.getProperty("LOCATION"), true); + + let dateString = formatter.formatItemInterval(aEvent); + + if (aEvent.recurrenceInfo) { + let kDefaultTimezone = cal.calendarDefaultTimezone(); + let startDate = aEvent.startDate; + let endDate = aEvent.endDate; + startDate = startDate ? startDate.getInTimezone(kDefaultTimezone) : null; + endDate = endDate ? endDate.getInTimezone(kDefaultTimezone) : null; + let repeatString = recurrenceRule2String(aEvent.recurrenceInfo, startDate, + endDate, startDate.isDate); + if (repeatString) { + dateString = repeatString; + } + + let formattedExDates = []; + let modifiedOccurrences = []; + + let dateComptor = function(a, b) { + return a.startDate.compare(b.startDate); + }; + + // Show removed instances + for (let exc of aEvent.recurrenceInfo.getRecurrenceItems({})) { + if (exc instanceof Components.interfaces.calIRecurrenceDate) { + if (exc.isNegative) { + // This is an EXDATE + formattedExDates.push(formatter.formatDateTime(exc.date)); + } else { + // This is an RDATE, close enough to a modified occurrence + let excItem = aEvent.recurrenceInfo.getOccurrenceFor(exc.date); + cal.binaryInsert(modifiedOccurrences, excItem, dateComptor, true); + } + } + } + if (formattedExDates.length > 0) { + field("canceledOccurrences", formattedExDates.join("\n")); + } + + // Show modified occurrences + for (let recurrenceId of aEvent.recurrenceInfo.getExceptionIds({})) { + let exc = aEvent.recurrenceInfo.getExceptionFor(recurrenceId); + let excLocation = exc.getProperty("LOCATION"); + + // Only show modified occurrence if start, duration or location + // has changed. + if (exc.startDate.compare(exc.recurrenceId) != 0 || + exc.duration.compare(aEvent.duration) != 0 || + excLocation != aEvent.getProperty("LOCATION")) { + cal.binaryInsert(modifiedOccurrences, exc, dateComptor, true); + } + } + + let stringifyOcc = function(occ) { + let formattedExc = formatter.formatItemInterval(occ); + let occLocation = occ.getProperty("LOCATION"); + if (occLocation != aEvent.getProperty("LOCATION")) { + let location = ltn.getString("lightning", "imipHtml.newLocation", [occLocation]); + formattedExc += " (" + location + ")"; + } + return formattedExc; + }; + + if (modifiedOccurrences.length > 0) { + field("modifiedOccurrences", modifiedOccurrences.map(stringifyOcc).join("\n")); + } + } + + field("when", dateString); + field("comment", aEvent.getProperty("COMMENT"), true); + + // DESCRIPTION field + let eventDescription = (aEvent.getProperty("DESCRIPTION") || "") + /* Remove the useless "Outlookism" squiggle. */ + .replace("*~*~*~*~*~*~*~*~*~*", ""); + field("description", eventDescription, true); + + // URL + field("url", aEvent.getProperty("URL"), true); + + // ATTACH - we only display URI but no BINARY type attachments here + let links = []; + let attachments = aEvent.getAttachments({}); + for (let attachment of attachments) { + if (attachment.uri) { + links.push(attachment.uri.spec); + } + } + field("attachments", links.join("<br>"), true); + + // ATTENDEE and ORGANIZER fields + let attendees = aEvent.getAttendees({}); + let attendeeTemplate = doc.getElementById("attendee-template"); + let attendeeTable = doc.getElementById("attendee-table"); + let organizerTable = doc.getElementById("organizer-table"); + doc.getElementById("imipHtml-attendees-row").hidden = (attendees.length < 1); + doc.getElementById("imipHtml-organizer-row").hidden = !aEvent.organizer; + + let setupAttendee = function(aAttendee) { + let row = attendeeTemplate.cloneNode(true); + row.removeAttribute("id"); + row.removeAttribute("hidden"); + + // resolve delegatees/delegators to display also the CN + let del = cal.resolveDelegation(aAttendee, attendees); + if (del.delegators != "") { + del.delegators = " " + ltn.getString("lightning", "imipHtml.attendeeDelegatedFrom", + [del.delegators]); + } + + // display itip icon + let role = aAttendee.role || "REQ-PARTICIPANT"; + let partstat = aAttendee.participationStatus || "NEEDS-ACTION"; + let userType = aAttendee.userType || "INDIVIDUAL"; + let itipIcon = row.getElementsByClassName("itip-icon")[0]; + itipIcon.setAttribute("role", role); + itipIcon.setAttribute("usertype", userType); + itipIcon.setAttribute("partstat", partstat); + let attName = aAttendee.commonName && aAttendee.commonName.length + ? aAttendee.commonName : aAttendee.toString(); + let userTypeString = ltn.getString("lightning", "imipHtml.attendeeUserType2." + userType, + [aAttendee.toString()]); + let roleString = ltn.getString("lightning", "imipHtml.attendeeRole2." + role, + [userTypeString]); + let partstatString = ltn.getString("lightning", "imipHtml.attendeePartStat2." + partstat, + [attName, del.delegatees]); + let itipTooltip = ltn.getString("lightning", "imipHtml.attendee.combined", + [roleString, partstatString]); + row.setAttribute("title", itipTooltip); + // display attendee + row.getElementsByClassName("attendee-name")[0].textContent = aAttendee.toString() + + del.delegators; + return row; + }; + + // Fill rows for attendees and organizer + field("attendees"); + for (let attendee of attendees) { + attendeeTable.appendChild(setupAttendee(attendee)); + } + + field("organizer"); + if (aEvent.organizer) { + organizerTable.appendChild(setupAttendee(aEvent.organizer)); + } + + return doc; + }, + + /** + * Expects and return a serialized DOM - use cal.xml.serializeDOM(aDOM) + * @param {String} aOldDoc serialized DOM of the the old document + * @param {String} aNewDoc serialized DOM of the the new document + * @param {String} aIgnoreId attendee id to ignore, usually the organizer + * @return {String} updated serialized DOM of the new document + */ + compareInvitationOverlay: function(aOldDoc, aNewDoc, aIgnoreId) { + /** + * Transforms text node content to formated child nodes. Decorations are defined in imip.css + * @param {Node} aToNode text node to change + * @param {String} aType use 'newline' for the same, 'added' or 'removed' for decoration + * @param {String} aText [optional] + * @param {Boolean} aClear [optional] for consecutive changes on the same node, set to false + */ + function _content2Child(aToNode, aType, aText = "", aClear = true) { + let nodeDoc = aToNode.ownerDocument; + if (aClear && aToNode.hasChildNodes()) { + aToNode.removeChild(aToNode.firstChild); + } + let n = nodeDoc.createElement(aType.toLowerCase() == "newline" ? "br" : "span"); + switch (aType) { + case "added": + case "modified": + case "removed": + n.className = aType; + if (Preferences.get("calendar.view.useSystemColors", false)) { + n.setAttribute("systemcolors", true); + } + break; + } + n.textContent = aText; + aToNode.appendChild(n); + } + /** + * Extracts attendees from the given document + * @param {Node} aDoc document to search in + * @param {String} aElement element name as used in _compareElement() + * @returns {Array} attendee nodes + */ + function _getAttendees(aDoc, aElement) { + let attendees = []; + for (let att of aDoc.getElementsByClassName("attendee-name")) { + if (!att.parentNode.hidden && + att.parentNode.parentNode.id == (aElement + "-table")) { + attendees[att.textContent] = att; + } + } + return attendees; + } + /** + * Compares both documents for elements related to the given name + * @param {String} aElement part of the element id within the html template + */ + function _compareElement(aElement) { + let element = aElement == "attendee" ? aElement + "s" : aElement; + let oldRow = aOldDoc.getElementById("imipHtml-" + element + "-row"); + let newRow = aNewDoc.getElementById("imipHtml-" + element + "-row"); + let row = doc.getElementById("imipHtml-" + element + "-row"); + let oldContent = aOldDoc.getElementById("imipHtml-" + aElement + "-content"); + let content = doc.getElementById("imipHtml-" + aElement + "-content"); + + if (newRow.hidden && !oldRow.hidden) { + // element was removed + // we only need to check for simple elements here: attendee or organizer row + // cannot be removed + if (oldContent) { + _content2Child(content, "removed", oldContent.textContent); + row.hidden = false; + } + } else if (!newRow.hidden && oldRow.hidden) { + // the element was added + // we only need to check for simple elements here: attendee or organizer row + // must have been there before + if (content) { + _content2Child(content, "added", content.textContent); + } + } else if (!newRow.hidden && !oldRow.hidden) { + // the element may have been modified + if (content) { + if (content.textContent != oldContent.textContent) { + _content2Child(content, "added", content.textContent); + _content2Child(content, "newline", null, false); + _content2Child(content, "removed", oldContent.textContent, false); + } + } else { + content = doc.getElementById(aElement + "-table"); + oldContent = aOldDoc.getElementById(aElement + "-table"); + let excludeAddress = cal.removeMailTo(aIgnoreId); + if (content && oldContent && !content.isEqualNode(oldContent)) { + // extract attendees + let attendees = _getAttendees(doc, aElement); + let oldAttendees = _getAttendees(aOldDoc, aElement); + // decorate newly added attendees + for (let att of Object.keys(attendees)) { + if (!(att in oldAttendees)) { + _content2Child(attendees[att], "added", att); + } + } + for (let att of Object.keys(oldAttendees)) { + // if att is the user his/herself, who accepted an invitation he/she was + // not invited to, we exclude him/her from decoration + let notExcluded = excludeAddress == "" || + !att.includes(excludeAddress); + // decorate removed attendees + if (!(att in attendees) && notExcluded) { + _content2Child(oldAttendees[att], "removed", att); + content.appendChild(oldAttendees[att].parentNode.cloneNode(true)); + } else if ((att in attendees) && notExcluded) { + // highlight partstat, role or usertype changes + let oldAtts = oldAttendees[att].parentNode + .getElementsByClassName("itip-icon")[0] + .attributes; + let newAtts = attendees[att].parentNode + .getElementsByClassName("itip-icon")[0] + .attributes; + let hasChanged = function(name) { + return oldAtts.getNamedItem(name).value != + newAtts.getNamedItem(name).value; + }; + if (["role", "partstat", "usertype"].some(hasChanged)) { + _content2Child(attendees[att], "modified", att); + } + } + } + } + } + } + } + aOldDoc = cal.xml.parseString(aOldDoc); + aNewDoc = cal.xml.parseString(aNewDoc); + let doc = aNewDoc.cloneNode(true); + // elements to consider for comparison + ["summary", "location", "when", "canceledOccurrences", + "modifiedOccurrences", "organizer", "attendee"].forEach(_compareElement); + return cal.xml.serializeDOM(doc); + }, + + /** + * Returns the header section for an invitation email. + * @param {String} aMessageId the message id to use for that email + * @param {nsIMsgIdentity} aIdentity the identity to use for that email + * @returns {String} the source code of the header section of the email + */ + getHeaderSection: function(aMessageId, aIdentity, aToList, aSubject) { + let recipient = aIdentity.fullName + " <" + aIdentity.email + ">"; + let from = aIdentity.fullName.length ? cal.validateRecipientList(recipient) + : aIdentity.email; + let header = "MIME-version: 1.0\r\n" + + (aIdentity.replyTo ? "Return-path: " + + ltn.invitation.encodeMimeHeader(aIdentity.replyTo, true) + + "\r\n" : "") + + "From: " + ltn.invitation.encodeMimeHeader(from, true) + "\r\n" + + (aIdentity.organization ? "Organization: " + + ltn.invitation.encodeMimeHeader(aIdentity.organization) + + "\r\n" : "") + + "Message-ID: " + aMessageId + "\r\n" + + "To: " + ltn.invitation.encodeMimeHeader(aToList, true) + "\r\n" + + "Date: " + ltn.invitation.getRfc5322FormattedDate() + "\r\n" + + "Subject: " + ltn.invitation + .encodeMimeHeader(aSubject.replace(/(\n|\r\n)/, "|")) + "\r\n"; + let validRecipients; + if (aIdentity.doCc) { + validRecipients = cal.validateRecipientList(aIdentity.doCcList); + if (validRecipients != "") { + header += "Cc: " + ltn.invitation.encodeMimeHeader(validRecipients, true) + "\r\n"; + } + } + if (aIdentity.doBcc) { + validRecipients = cal.validateRecipientList(aIdentity.doBccList); + if (validRecipients != "") { + header += "Bcc: " + ltn.invitation.encodeMimeHeader(validRecipients, true) + "\r\n"; + } + } + return header; + }, + + /** + * Returns a datetime string according to section 3.3 of RfC5322 + * @param {Date} [optional] Js Date object to format; if not provided current DateTime is used + * @return {String} Datetime string with a modified tz-offset notation compared to + * Date.toString() like "Fri, 20 Nov 2015 09:45:36 +0100" + */ + getRfc5322FormattedDate: function(aDate = null) { + let date = aDate || new Date(); + let str = date.toString() + .replace(/^(\w{3}) (\w{3}) (\d{2}) (\d{4}) ([0-9:]{8}) GMT([+-])(\d{4}).*$/, + "$1, $3 $2 $4 $5 $6$7"); + // according to section 3.3 of RfC5322, +0000 should be used for defined timezones using + // UTC time, while -0000 should indicate a floating time instead + let timezone = cal.calendarDefaultTimezone(); + if (timezone && timezone.isFloating) { + str.replace(/\+0000$/, "-0000"); + } + return str; + }, + + /** + * Converts a given unicode text to utf-8 and normalizes line-breaks to \r\n + * @param {String} aText a unicode encoded string + * @return {String} the converted uft-8 encoded string + */ + encodeUTF8: function(aText) { + return ltn.invitation.convertFromUnicode("UTF-8", aText).replace(/(\r\n)|\n/g, "\r\n"); + }, + + /** + * Converts a given unicode text + * @param {String} aCharset target character set + * @param {String} aSrc unicode text to convert + * @return {String} the converted string + */ + convertFromUnicode: function(aCharset, aSrc) { + let unicodeConverter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"] + .createInstance(Components.interfaces.nsIScriptableUnicodeConverter); + unicodeConverter.charset = aCharset; + return unicodeConverter.ConvertFromUnicode(aSrc); + }, + + /** + * Converts a header to a mime encoded header + * @param {String} aHeader a header to encode + * @param {boolean} aIsEmail if enabled, only the CN but not the email address gets + * converted - default value is false + * @return {String} the encoded string + */ + encodeMimeHeader: function(aHeader, aIsEmail = false) { + let fieldNameLen = aHeader.indexOf(": ") + 2; + return MailServices.mimeConverter + .encodeMimePartIIStr_UTF8(aHeader, + aIsEmail, + "UTF-8", + fieldNameLen, + Components.interfaces + .nsIMimeConverter + .MIME_ENCODED_WORD_SIZE); + }, + + /** + * Parses a counterproposal to extract differences to the existing event + * @param {calIEvent|calITodo} aProposedItem The counterproposal + * @param {calIEvent|calITodo} aExistingItem The item to compare with + * @return {JSObject} Objcet of result and differences of parsing + * @return {String} JsObject.result.type Parsing result: OK|OLDVERSION|ERROR|NODIFF + * @return {String} JsObject.result.descr Parsing result description + * @return {Array} JsObject.differences Array of objects consisting of property, proposed + * and original properties. + * @return {String} JsObject.comment A comment of the attendee, if any + */ + parseCounter: function(aProposedItem, aExistingItem) { + let isEvent = cal.isEvent(aProposedItem); + // atm we only support a subset of properties, for a full list see RfC 5546 section 3.2.7 + let properties = ["SUMMARY", "LOCATION", "DTSTART", "DTEND", "COMMENT"]; + if (!isEvent) { + cal.LOG("Parsing of counterproposals is currently only supported for events."); + properties = []; + } + + let diff = []; + let status = { descr: "", type: "OK" }; + // As required in https://tools.ietf.org/html/rfc5546#section-3.2.7 a valid counterproposal + // is referring to as existing UID and must include the same sequence number and organizer as + // the original request being countered + if (aProposedItem.id == aExistingItem.id && + aProposedItem.organizer && aExistingItem.organizer && + aProposedItem.organizer.id == aExistingItem.organizer.id) { + let proposedSequence = aProposedItem.getProperty("SEQUENCE") || 0; + let existingSequence = aExistingItem.getProperty("SEQUENCE") || 0; + if (existingSequence >= proposedSequence) { + if (existingSequence > proposedSequence) { + // in this case we prompt the organizer with the additional information that the + // received proposal refers to an outdated version of the event + status.descr = "This is a counterproposal to an already rescheduled event."; + status.type = "OUTDATED"; + } else if (aProposedItem.stampTime.compare(aExistingItem.stampTime) == -1) { + // now this is the same sequence but the proposal is not based on the latest + // update of the event - updated events may have minor changes, while for major + // ones there has been a rescheduling + status.descr = "This is a counterproposal not based on the latest event update."; + status.type = "NOTLATESTUPDATE"; + } + for (let prop of properties) { + let newValue = aProposedItem.getProperty(prop) || null; + let oldValue = aExistingItem.getProperty(prop) || null; + if ((["DTSTART", "DTEND"].includes(prop) && newValue.toString() != oldValue.toString()) || + (!["DTSTART", "DTEND"].includes(prop) && newValue != oldValue)) { + diff.push({ + property: prop, + proposed: newValue, + original: oldValue + }); + } + } + } else { + status.descr = "Invalid sequence number in counterproposal."; + status.type = "ERROR"; + } + } else { + status.descr = "Mismatch of uid or organizer in counterproposal."; + status.type = "ERROR"; + } + if (status.type != "ERROR" && !diff.length) { + status.descr = "No difference in counterproposal detected."; + status.type = "NODIFF"; + } + return { result: status, differences: diff }; + } +}; diff --git a/calendar/lightning/modules/ltnUtils.jsm b/calendar/lightning/modules/ltnUtils.jsm new file mode 100644 index 000000000..22444aab9 --- /dev/null +++ b/calendar/lightning/modules/ltnUtils.jsm @@ -0,0 +1,22 @@ +/* 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/. */ + +/* exported ltn */ + +Components.utils.import("resource://calendar/modules/calUtils.jsm"); + +this.EXPORTED_SYMBOLS = ["ltn"]; +var ltn = { + /** + * Gets the value of a string in a .properties file from the lightning bundle + * + * @param {String} aBundleName the name of the properties file. It is assumed that the + * file lives in chrome://lightning/locale/ + * @param {String} aStringName the name of the string within the properties file + * @param {Array} aParams [optional] array of parameters to format the string + */ + getString: function(aBundleName, aStringName, aParams) { + return cal.calGetString(aBundleName, aStringName, aParams, "lightning"); + } +}; diff --git a/calendar/lightning/modules/moz.build b/calendar/lightning/modules/moz.build new file mode 100644 index 000000000..b7a3ae4f2 --- /dev/null +++ b/calendar/lightning/modules/moz.build @@ -0,0 +1,9 @@ +# vim: set filetype=python: +# 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/. + +EXTRA_JS_MODULES += [ + 'ltnInvitationUtils.jsm', + 'ltnUtils.jsm', +] diff --git a/calendar/lightning/moz.build b/calendar/lightning/moz.build new file mode 100644 index 000000000..c6f12d640 --- /dev/null +++ b/calendar/lightning/moz.build @@ -0,0 +1,53 @@ +# vim: set filetype=python: +# 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/. + +DIRS += [ + '../base', + '../components', + '../providers/gdata', + 'components', + 'locales', + 'modules', +] + +TEST_DIRS += ['../test'] + +XPI_NAME = 'lightning' +export('XPI_NAME') + +FINAL_TARGET_PP_FILES += [ + 'app.ini', + 'install.rdf', +] + +JAR_MANIFESTS += ['jar.mn'] + +USE_EXTENSION_MANIFEST = True +export('USE_EXTENSION_MANIFEST') + +if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'windows': + DEFINES['THEME'] = 'windows' +else: + DEFINES['THEME'] = 'linux' + +JS_PREFERENCE_PP_FILES += [ + 'content/lightning.js', +] + +FINAL_TARGET_FILES.timezones += [ + '../timezones/zones.json', +] + +with Files('**'): + BUG_COMPONENT = ('Calendar', 'Lightning Only') + +with Files('content/suite-*'): + BUG_COMPONENT = ('Calendar', 'Lightning: SeaMonkey Integration') + +with Files('build/**'): + BUG_COMPONENT = ('Calendar', 'Build Config') + +with Files('app.ini'): + BUG_COMPONENT = ('Calendar', 'Build Config') diff --git a/calendar/lightning/themes/common/html-item-editing.css b/calendar/lightning/themes/common/html-item-editing.css new file mode 100644 index 000000000..56a27cc45 --- /dev/null +++ b/calendar/lightning/themes/common/html-item-editing.css @@ -0,0 +1,98 @@ +/* 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/. */ + +/* html and body styles are copied from chrome://global/skin/global.css + * as a temporary measure to prevent a transparent background on windows. + * Using the global.css file does not work because of a namespace issue. + * See https://bugzilla.mozilla.org/show_bug.cgi?id=1297392#c4 + * + * #container is the top-level react div + */ +html, +body, +#container { + -moz-appearance: window; + background-color: -moz-Dialog; + color: -moz-DialogText; + font: message-box; +} +a { + color: -moz-NativehyperlinkText; + text-decoration: underline; + cursor: pointer; +} +#topwrapper { + max-width: 1200px; +} +.wrapper { + display: flex; + flex-direction: column; + flex-wrap: wrap; + padding: 10px; + margin-bottom: 10px; +} +#wrapper2 { + flex-direction: row; + flex-wrap: nowrap; +} +.box { + flex: 1 1 auto; + padding: 10px; + border-bottom: 1px solid ButtonShadow; + margin-bottom: 10px; + margin-right: 10px; +} +#box5 { + flex-basis: 80%; +} +#attendeesDiv { + page-break-after: always; +} +#description { + margin: 10px; +} +#descriptionTextArea { + width: 100%; + box-sizing: border-box; +} +#tabstrip { + display: flex; + padding: 0; +} +.tab { + list-style: none; + border: 1px solid ButtonShadow; + padding: 5px 10px; + cursor: pointer; +} +.activeTab { + border-bottom: 0px; +} +.hidden { + display:none; +} +.tabpanel { + min-height: 250px; +} +.capsule { + margin-right: 5px; + padding: 3px; + border-radius: 3px; + color: ButtonText; +} +.deleteCapsule { + cursor: pointer; + border-left: 1px solid ButtonText; + padding-left: 4px; + padding-right: 4px; + margin-left: 6px; + font-family: sans; +} + +/* Small screens */ +@media all and (max-width: 750px) { + #attendeesDiv { + page-break-after: auto; + } +} diff --git a/calendar/lightning/themes/common/images/mode-switch-icons.png b/calendar/lightning/themes/common/images/mode-switch-icons.png Binary files differnew file mode 100644 index 000000000..ee311b74b --- /dev/null +++ b/calendar/lightning/themes/common/images/mode-switch-icons.png diff --git a/calendar/lightning/themes/common/imip.css b/calendar/lightning/themes/common/imip.css new file mode 100644 index 000000000..0994a86c1 --- /dev/null +++ b/calendar/lightning/themes/common/imip.css @@ -0,0 +1,117 @@ +/* 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/. */ + +.itip-icon { + --itip-icon-partstat: -16px -16px; /* default: NEEDS-ACTION */ + --itip-icon-role: 0px; /* default: REQ-PARTICIPANT */ + --itip-icon-usertype: -32px; /* default: INDIVIDUAL */ + width: 16px; + height: 16px; + background-image: url(chrome://calendar-common/skin/calendar-itip-icons.svg), + url(chrome://calendar-common/skin/calendar-itip-icons.svg); + background-position: var(--itip-icon-partstat), var(--itip-icon-usertype) var(--itip-icon-role); +} +.itip-icon[partstat="ACCEPTED"] { + --itip-icon-partstat: 0px 0px; +} +.itip-icon[partstat="DECLINED"] { + --itip-icon-partstat: 0px -16px; +} +.itip-icon[partstat="DELEGATED"] { + --itip-icon-partstat: 0px -32px; +} +.itip-icon[partstat="TENTATIVE"] { + --itip-icon-partstat: -16px 0px; +} +.itip-icon[usertype="INDIVIDUAL"] { + --itip-icon-usertype: -32px; +} +.itip-icon[usertype="GROUP"] { + --itip-icon-usertype: -48px; +} +.itip-icon[usertype="RESOURCE"] { + --itip-icon-usertype: -64px; +} +.itip-icon[usertype="ROOM"] { + --itip-icon-usertype: -80px; +} +.itip-icon[usertype="UNKNOWN"] { + --itip-icon-usertype: -96px; +} +.itip-icon[role="REQ-PARTICIPANT"] { + --itip-icon-role: 0px; +} +.itip-icon[role="OPT-PARTICIPANT"] { + --itip-icon-role: -16px; +} +.itip-icon[role="NON-PARTICIPANT"] { + --itip-icon-role: -32px; +} +.itip-icon[role="CHAIR"] { + --itip-icon-role: -32px; + --itip-icon-usertype: -16px; +} + +#imipHtml-attendees-row > .content, +#imipHtml-organizer-row > .content, +#attendee-table > tbody > tr > td, +#organizer-table > tbody > tr > td { + padding: 0 +} + +#invitation-table { + border: 3px solid -moz-default-color; + border-collapse: collapse; + width: 40em; + margin-inline-start: auto; + margin-inline-end: auto; +} +#invitation-table > tbody > tr > td { + padding: 3px; + vertical-align: top; + width: 2em; + text-align: left; +} + +.header { + color: HighlightText; + font-size: 1em; + font-weight: bold; + background-color: Highlight; +} +.description { + width: 9em; + text-align: right; + border-inline-end: 1px solid rgba(255, 255, 255, 0.2); + background-color: rgba(0, 0, 0, 0.2); + vertical-align: top; +} +.content { + width: 29em; + background-color: -moz-default-background-color; +} +.content p { + white-space: pre-wrap; +} +.added { + color: rgb(255, 0, 0); +} +.added[systemcolors] { + color: -moz-DialogText; + font-weight: bold; +} +.modified { + color: rgb(255, 0, 0); + font-style: italic; +} +.modified[systemcolors] { + color: -moz-DialogText; +} +.removed { + color: rgb(125, 125, 125); + text-decoration: line-through; +} +.removed[systemcolors] { + color: -moz-DialogText; +} diff --git a/calendar/lightning/themes/common/lightning.css b/calendar/lightning/themes/common/lightning.css new file mode 100644 index 000000000..b1b0d05ae --- /dev/null +++ b/calendar/lightning/themes/common/lightning.css @@ -0,0 +1,47 @@ +/* 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/. */ + +/* avoids contributing to the min width when Lightning is not selected */ +#calendarTabPanel:not([selected]) { + visibility: collapse; +} + +#calendar-status-todaypane-button > stack > .toolbarbutton-day-text { + text-align: center; + margin-inline-start: 0; + margin-bottom: -4px; + font-size: 7pt; + font-family: Arial, Helvetica, sans-serif; + font-weight: bold; + text-shadow: none; + background-color: transparent; +} + +#calendar-status-todaypane-button[hideLabel] > .toolbarbutton-text, +#calendar-status-todaypane-button[hideLabel] > .toolbarbutton-icon-end { + display: none; +} + +.imipMoreButton > .toolbarbutton-icon { + display: none; +} + +@media not all and (-moz-os-version: windows-xp) { + #task-tab-button .toolbarbutton-icon, + #calendar-tab-button .toolbarbutton-icon, + .calbar-toolbarbutton-1 .toolbarbutton-icon, + toolbarpaletteitem > .msgHeaderView-button .toolbarbutton-icon, + #task-actions-toolbar > .msgHeaderView-button .toolbarbutton-icon { + width: 18px; + height: 18px; + } +} + +#calendar-toolbox, +#task-toolbox { + -moz-appearance: none; + background-color: rgb(248, 248, 248); + border-bottom: 1px solid threedshadow; + border-top: 0px; +}
\ No newline at end of file diff --git a/calendar/lightning/themes/common/suite-accountCentral.css b/calendar/lightning/themes/common/suite-accountCentral.css new file mode 100644 index 000000000..2da68effd --- /dev/null +++ b/calendar/lightning/themes/common/suite-accountCentral.css @@ -0,0 +1,9 @@ +/* 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/. */ + +@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); + +#lightning-newCalendar-row { + list-style-image: url("chrome://calendar/skin/cal-icon24.png"); +} diff --git a/calendar/lightning/themes/linux/accountCentral.css b/calendar/lightning/themes/linux/accountCentral.css new file mode 100644 index 000000000..6a628dd0b --- /dev/null +++ b/calendar/lightning/themes/linux/accountCentral.css @@ -0,0 +1,11 @@ +/* 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/. */ + +#lightning-newCalendar-row > hbox > label:-moz-locale-dir(rtl) { + background-position: right !important; +} + +#lightning-newCalendar-row > hbox > label { + background: url(chrome://calendar/skin/cal-icon24.png) no-repeat !important; +} diff --git a/calendar/lightning/themes/linux/imip.css b/calendar/lightning/themes/linux/imip.css new file mode 100644 index 000000000..2d96dd4fb --- /dev/null +++ b/calendar/lightning/themes/linux/imip.css @@ -0,0 +1,5 @@ +/* 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/. */ + +@import url(chrome://lightning-common/skin/imip.css); diff --git a/calendar/lightning/themes/linux/lightning-toolbar.css b/calendar/lightning/themes/linux/lightning-toolbar.css new file mode 100644 index 000000000..a3c39a544 --- /dev/null +++ b/calendar/lightning/themes/linux/lightning-toolbar.css @@ -0,0 +1,130 @@ +/* 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/. */ + +/* Lightning "Calendar" Toolbarbutton */ + +#lightning-button-calendar { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#calendar-tab); +} + +#lightning-button-tasks { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#task-tab); +} + +#calendar-synchronize-button, +#task-synchronize-button { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#synchronize); +} + +#extractEventButton, +#task-newevent-button, +#hdrExtractEventButton, +#calendar-newevent-button { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#newevent); +} + +#extractTaskButton, +#task-newtask-button, +#hdrExtractTaskButton, +#calendar-newtask-button { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#newtask); +} + +#calendar-edit-button, +#task-edit-button { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#edit); +} + +#calendar-delete-button, +#task-delete-button { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#delete); +} + +#calendar-goto-today-button { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#today); +} + +#calendar-print-button, +#task-print-button { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#print); +} + +#calendar-unifinder-button { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#find); +} + +toolbar[brighttext] #lightning-button-calendar { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#calendar-tab-inverted); +} + +toolbar[brighttext] #lightning-button-tasks { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#task-tab-inverted); +} + +toolbar[brighttext] #calendar-synchronize-button, +toolbar[brighttext] #task-synchronize-button { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#synchronize-inverted); +} + +toolbar[brighttext] #extractEventButton, +toolbar[brighttext] #task-newevent-button, +toolbar[brighttext] #hdrExtractEventButton, +toolbar[brighttext] #calendar-newevent-button { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#newevent-inverted); +} + +toolbar[brighttext] #extractTaskButton, +toolbar[brighttext] #task-newtask-button, +toolbar[brighttext] #hdrExtractTaskButton, +toolbar[brighttext] #calendar-newtask-button { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#newtask-inverted); +} + +toolbar[brighttext] #calendar-edit-button, +toolbar[brighttext] #task-edit-button { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#edit-inverted); +} + +toolbar[brighttext] #calendar-delete-button, +toolbar[brighttext] #task-delete-button { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#delete-inverted); +} + +toolbar[brighttext] #calendar-goto-today-button { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#today-inverted); +} + +toolbar[brighttext] #calendar-print-button, +toolbar[brighttext] #task-print-button { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#print-inverted); +} + +toolbar[brighttext] #calendar-unifinder-button { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#find-inverted); +} + +#lightning-button-calendar > .toolbarbutton-icon, +#lightning-button-tasks > .toolbarbutton-icon, +#calendar-synchronize-button > .toolbarbutton-icon, +#task-synchronize-button > .toolbarbutton-icon, +#extractEventButton > .toolbarbutton-icon, +#task-newevent-button > .toolbarbutton-icon, +#hdrExtractEventButton > .toolbarbutton-icon, +#calendar-newevent-button > .toolbarbutton-icon, +#extractTaskButton > .toolbarbutton-icon, +#task-newtask-button > .toolbarbutton-icon, +#hdrExtractTaskButton > .toolbarbutton-icon, +#calendar-newtask-button > .toolbarbutton-icon, +#calendar-edit-button > .toolbarbutton-icon, +#task-edit-button > .toolbarbutton-icon, +#calendar-delete-button > .toolbarbutton-icon, +#task-delete-button > .toolbarbutton-icon, +#calendar-goto-today-button > .toolbarbutton-icon, +#calendar-print-button > .toolbarbutton-icon, +#task-print-button > .toolbarbutton-icon, +#calendar-unifinder-button > .toolbarbutton-icon { + width: 18px; + height: 18px; + padding: 0; +} diff --git a/calendar/lightning/themes/linux/lightning-widgets.css b/calendar/lightning/themes/linux/lightning-widgets.css new file mode 100644 index 000000000..0c3e1aa5e --- /dev/null +++ b/calendar/lightning/themes/linux/lightning-widgets.css @@ -0,0 +1,10 @@ +/* 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/. */ + +lightning-notification-bar { + background-color: #baeeff; + color: -moz-dialogtext; + border-bottom: 1px solid ThreeDDarkShadow; + padding: 3px; +} diff --git a/calendar/lightning/themes/linux/lightning.css b/calendar/lightning/themes/linux/lightning.css new file mode 100644 index 000000000..c5b02f467 --- /dev/null +++ b/calendar/lightning/themes/linux/lightning.css @@ -0,0 +1,248 @@ +/* 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/. */ + +@import url(chrome://lightning-common/skin/lightning.css); + +#calendarContent { + color: -moz-DialogText; + background-color: -moz-Dialog; +} + +#calsidebar_splitter, +#today-splitter { + -moz-appearance: none; + /* splitter grip area */ + width: 5px; + margin-top: 0; + /* because of the negative margin needed to make the splitter visible */ + position: relative; + z-index: 10; + transition: border-width .3s ease-in; +} + +#calsidebar_splitter { + border-inline-start: 1px solid ThreeDShadow; + /* make only the splitter border visible */ + margin-inline-end: -5px; +} + +#today-splitter { + border-inline-start: 1px solid ThreeDShadow; + /* make only the splitter border visible */ + margin-inline-end: -5px; +} + +#calsidebar_splitter[state="collapsed"] { + border-inline-start: 1px solid transparent; +} + +#calsidebar_splitter[state="collapsed"]:hover { + border-inline-start: 4px solid highlight; +} + +#today-splitter > grippy { + display: none; +} + +/* Calendar list rules */ +#calendar-panel { + padding-bottom: 5px; +} + +/* Lightning preferences icon */ +radio[pane=paneLightning] { + list-style-image: url("chrome://calendar/skin/cal-icon32.png"); +} + +/* iMIP notification bar */ +#imip-bar > image { + list-style-image: url("chrome://calendar/skin/cal-icon32.png"); + -moz-image-region: rect(0px, 32px, 32px, 0px); +} + +/* ::::: tabs ::::: */ + +/* ::: new tab buttons ::: */ +#calendar-tab-button, +#newMsgButton-calendar-menuitem { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#calendar-tab); +} + +#task-tab-button, +#newMsgButton-task-menuitem { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#task-tab); +} + +#tabs-toolbar[brighttext] #calendar-tab-button { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#calendar-tab-inverted); +} + +#tabs-toolbar[brighttext] #task-tab-button { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#task-tab-inverted); +} + +/* ::: tab icons ::: */ +.icon-holder[type="calendar"], +.tabmail-tab[type="calendar"] { + list-style-image: url(chrome://lightning-common/skin/mode-switch-icons.png); + -moz-image-region: rect(0px 64px 16px 48px); +} + +.icon-holder[type="calendar"][selected="true"], +.tabmail-tab[type="calendar"][selected="true"] { + -moz-image-region: rect(16px 64px 32px 48px); +} + +.icon-holder[type="tasks"], +.tabmail-tab[type="tasks"] { + list-style-image: url(chrome://lightning-common/skin/mode-switch-icons.png); + -moz-image-region: rect(0px 80px 16px 64px); +} + +.icon-holder[type="tasks"][selected="true"], +.tabmail-tab[type="tasks"][selected="true"] { + -moz-image-region: rect(16px 80px 32px 64px); +} + +/* Lightning sidebar in calendar and task mode */ +#ltnSidebar { + background-color: -moz-field; + border-bottom: 1px solid ThreeDShadow; +} + +/* Write button */ +#newMsgButton-mail-menuitem { + list-style-image: url(chrome://messenger/skin/icons/mail-toolbar.svg#newmsg); +} + +#newMsgButton-mail-menuitem > .menu-iconic-left > .menu-iconic-icon, +#newMsgButton-calendar-menuitem > .menu-iconic-left > .menu-iconic-icon, +#newMsgButton-task-menuitem > .menu-iconic-left > .menu-iconic-icon { + width: 18px; + height: 18px; + margin: -1px; +} + +/* Today pane button in status bar */ +#calendar-status-todaypane-button, +#calendar-status-todaypane-button[checked="true"] { + min-width: 0; + min-height: 0; + margin: 1px 0 0; + -moz-appearance: none; + border-radius: 3px; + padding: 1px 2px 0 !important; + border: 1px solid transparent; +} + +#calendar-status-todaypane-button:hover { + border: 1px solid ThreeDShadow; + background-color: transparent !important; + background-image: none; + -moz-appearance: none; +} + +#calendar-status-todaypane-button[hideLabel] > stack { + margin-inline-start: 5px; +} + +#calendar-status-todaypane-button > stack > .toolbarbutton-icon-begin { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#pane); + -moz-image-region: rect(0 18px 18px 0); +} + +#calendar-status-todaypane-button:-moz-lwtheme-brighttext > stack > + .toolbarbutton-icon-begin { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#pane-inverted); +} + +/* compensate the 18px icon height */ +#calendar-status-todaypane-button > stack > .toolbarbutton-icon-begin { + margin-top: -1px; + margin-bottom: -1px; +} + +#calendar-status-todaypane-button > stack > .toolbarbutton-day-text { + margin-top: 4px; +} + +/* shift the today pane button label up by one pixel to center it */ +#calendar-status-todaypane-button > .toolbarbutton-text { + margin: 0 0 1px !important; +} + +#calendar-status-todaypane-button > .toolbarbutton-icon-end { + list-style-image: url(chrome://global/skin/icons/collapse.png); +} + +#calendar-status-todaypane-button[checked="true"] > .toolbarbutton-icon-end { + list-style-image: url(chrome://global/skin/icons/expand.png); +} + +#calMinimonthBox { + margin-top: 3px; +} + +/* ::: imip button icons ::: */ +#imip-view-toolbar > .toolbarbutton-1.msgHeaderView-button { + background-color: -moz-dialog; +} + +#imip-view-toolbar > .toolbarbutton-1.msgHeaderView-button > .toolbarbutton-menubutton-button, +#imip-view-toolbar > .toolbarbutton-1.msgHeaderView-button > .toolbarbutton-menubutton-dropmarker { + background-color: transparent; +} + +#imip-view-toolbar > .toolbarbutton-1.msgHeaderView-button:not(:active):hover, +#imip-view-toolbar > .toolbarbutton-1.msgHeaderView-button:-moz-any(:hover,[open="true"]) > + .toolbarbutton-menubutton-button, +#imip-view-toolbar > .toolbarbutton-1.msgHeaderView-button:hover > + .toolbarbutton-menubutton-dropmarker { + background: -moz-dialog linear-gradient(rgba(255, 255, 255, .5), transparent); +} + +#imip-view-toolbar > .toolbarbutton-1.msgHeaderView-button:not([type="menu-button"]):hover:active, +#imip-view-toolbar > .toolbarbutton-1.msgHeaderView-button[type="menu-button"] > + .toolbarbutton-menubutton-button:hover:active, +#imip-view-toolbar > .toolbarbutton-1.msgHeaderView-button[open="true"] > + .toolbarbutton-menubutton-dropmarker { + background: rgb(154, 154, 154) linear-gradient(rgba(255, 255, 255, .7), rgba(255, 255, 255, .4)); +} + +.imipAcceptRecurrencesButton, +.imipAcceptButton { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#complete); +} + +.imipDeclineCounterButton, +.imipDeclineRecurrencesButton, +.imipDeclineButton { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#decline); +} + +.imipTentativeRecurrencesButton, +.imipTentativeButton { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#tentative); +} + +.imipDetailsButton { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#find); +} + +.imipAddButton { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#newevent); +} + +.imipRescheduleButton, +.imipUpdateButton { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#synchronize); +} + +.imipDeleteButton { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#delete); +} + +.imipReconfirmButton { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#priority); +} diff --git a/calendar/lightning/themes/windows/accountCentral.css b/calendar/lightning/themes/windows/accountCentral.css new file mode 100644 index 000000000..6a628dd0b --- /dev/null +++ b/calendar/lightning/themes/windows/accountCentral.css @@ -0,0 +1,11 @@ +/* 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/. */ + +#lightning-newCalendar-row > hbox > label:-moz-locale-dir(rtl) { + background-position: right !important; +} + +#lightning-newCalendar-row > hbox > label { + background: url(chrome://calendar/skin/cal-icon24.png) no-repeat !important; +} diff --git a/calendar/lightning/themes/windows/images/imip-aero.png b/calendar/lightning/themes/windows/images/imip-aero.png Binary files differnew file mode 100644 index 000000000..f5c303aa1 --- /dev/null +++ b/calendar/lightning/themes/windows/images/imip-aero.png diff --git a/calendar/lightning/themes/windows/images/mode-switch-icons-aero.png b/calendar/lightning/themes/windows/images/mode-switch-icons-aero.png Binary files differnew file mode 100644 index 000000000..e1a7f1b52 --- /dev/null +++ b/calendar/lightning/themes/windows/images/mode-switch-icons-aero.png diff --git a/calendar/lightning/themes/windows/images/mode-switch-icons-inverted.png b/calendar/lightning/themes/windows/images/mode-switch-icons-inverted.png Binary files differnew file mode 100644 index 000000000..c61b5a03b --- /dev/null +++ b/calendar/lightning/themes/windows/images/mode-switch-icons-inverted.png diff --git a/calendar/lightning/themes/windows/imip.css b/calendar/lightning/themes/windows/imip.css new file mode 100644 index 000000000..2d96dd4fb --- /dev/null +++ b/calendar/lightning/themes/windows/imip.css @@ -0,0 +1,5 @@ +/* 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/. */ + +@import url(chrome://lightning-common/skin/imip.css); diff --git a/calendar/lightning/themes/windows/imip.png b/calendar/lightning/themes/windows/imip.png Binary files differnew file mode 100644 index 000000000..0690f8737 --- /dev/null +++ b/calendar/lightning/themes/windows/imip.png diff --git a/calendar/lightning/themes/windows/lightning-toolbar.css b/calendar/lightning/themes/windows/lightning-toolbar.css new file mode 100644 index 000000000..fcc74f1b3 --- /dev/null +++ b/calendar/lightning/themes/windows/lightning-toolbar.css @@ -0,0 +1,432 @@ +/* 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/. */ + +#calendar-toolbar2, +#task-toolbar2 { + border-top-width: 0; +} + +@media (-moz-windows-glass) { + #calendar-toolbox:not(:-moz-lwtheme) { + border-bottom-color: #AABCCF; + } +} + +@media (-moz-os-version: windows-xp) { + #lightning-button-calendar { + list-style-image: url(chrome://lightning-common/skin/mode-switch-icons.png); + -moz-image-region: rect(0px 24px 24px 0px); + } + + #lightning-button-calendar[disabled] { + -moz-image-region: rect(48px 24px 72px 0px); + } + toolbar[iconsize="small"] #lightning-button-calendar { + -moz-image-region: rect(0px 64px 16px 48px); + } + + toolbar[iconsize="small"] #lightning-button-calendar[disabled] { + -moz-image-region: rect(32px 64px 48px 48px); + } + + /* Lightning "Tasks" Toolbarbutton */ + #lightning-button-tasks { + list-style-image: url(chrome://lightning-common/skin/mode-switch-icons.png); + -moz-image-region: rect(0px 48px 24px 24px); + } + + #lightning-button-tasks[disabled] { + -moz-image-region: rect(48px 48px 72px 24px); + } + + toolbar[iconsize="small"] #lightning-button-tasks { + -moz-image-region: rect(0px 80px 16px 64px); + } + toolbar[iconsize="small"] #lightning-button-tasks[disabled] { + -moz-image-region: rect(32px 80px 48px 64px); + } + + /* Toolbar buttons */ + + .calbar-toolbarbutton-1 { + list-style-image: url(chrome://calendar/skin/toolbar-large.png); + } + + toolbar[iconsize="small"] .calbar-toolbarbutton-1 { + list-style-image: url(chrome://calendar/skin/toolbar-small.png); + } + + #calendar-synchronize-button, + #task-synchronize-button { + -moz-image-region: rect(0px 648px 24px 624px); + } + + #calendar-synchronize-button[disabled], + #task-synchronize-button[disabled] { + -moz-image-region: rect(48px 648px 72px 624px); + } + + toolbar[iconsize="small"] #calendar-synchronize-button, + toolbar[iconsize="small"] #task-synchronize-button { + -moz-image-region: rect(0px 432px 16px 416px); + } + + toolbar[iconsize="small"] #calendar-synchronize-button[disabled], + toolbar[iconsize="small"] #task-synchronize-button[disabled] { + -moz-image-region: rect(32px 432px 48px 416px); + } + + #calendar-newevent-button, + #task-newevent-button { + -moz-image-region: rect(0px 24px 24px 0px); + } + + #calendar-newevent-button[disabled], + #task-newevent-button[disabled] { + -moz-image-region: rect(48px 24px 72px 0px); + } + + toolbar[iconsize="small"] #calendar-newevent-button, + toolbar[iconsize="small"] #task-newevent-button { + -moz-image-region: rect(0px 16px 16px 0px); + } + + toolbar[iconsize="small"] #calendar-newevent-button[disabled], + toolbar[iconsize="small"] #task-newevent-button[disabled] { + -moz-image-region: rect(32px 16px 48px 0px); + } + + #calendar-newtask-button, + #task-newtask-button { + -moz-image-region: rect(0px 384px 24px 360px); + } + + #calendar-newtask-button[disabled], + #task-newtask-button[disabled] { + -moz-image-region: rect(48px 384px 72px 360px); + } + + toolbar[iconsize="small"] #calendar-newtask-button, + toolbar[iconsize="small"] #task-newtask-button { + -moz-image-region: rect(0px 256px 16px 240px); + } + + toolbar[iconsize="small"] #calendar-newtask-button[disabled], + toolbar[iconsize="small"] #task-newtask-button[disabled] { + -moz-image-region: rect(32px 256px 48px 240px); + } + + #calendar-edit-button, + #task-edit-button { + -moz-image-region: rect(0px 48px 24px 24px); + } + + #calendar-edit-button[disabled], + #task-edit-button[disabled] { + -moz-image-region: rect(48px 48px 72px 24px); + } + + toolbar[iconsize="small"] #calendar-edit-button, + toolbar[iconsize="small"] #task-edit-button { + -moz-image-region: rect(0px 32px 16px 16px); + } + + toolbar[iconsize="small"] #calendar-edit-button[disabled], + toolbar[iconsize="small"] #task-edit-button[disabled] { + -moz-image-region: rect(32px 32px 48px 16px); + } + + #calendar-delete-button, + #task-delete-button { + -moz-image-region: rect(0px 72px 24px 48px); + } + + #calendar-delete-button[disabled], + #task-delete-button[disabled] { + -moz-image-region: rect(48px 72px 72px 48px); + } + + toolbar[iconsize="small"] #calendar-delete-button, + toolbar[iconsize="small"] #task-delete-button { + -moz-image-region: rect(0px 48px 16px 32px); + } + + toolbar[iconsize="small"] #calendar-delete-button[disabled], + toolbar[iconsize="small"] #task-delete-button[disabled] { + -moz-image-region: rect(32px 48px 48px 32px); + } + + #calendar-goto-today-button { + -moz-image-region: rect(0px 408px 24px 384px); + } + + #calendar-goto-today-button[disabled] { + -moz-image-region: rect(48px 408px 72px 384px); + } + + toolbar[iconsize="small"] #calendar-goto-today-button { + -moz-image-region: rect(0px 272px 16px 256px); + } + + toolbar[iconsize="small"] #calendar-goto-today-button[disabled] { + -moz-image-region: rect(32px 272px 48px 256px); + } + + #calendar-print-button, + #task-print-button { + -moz-image-region: rect(0px 360px 24px 336px); + } + + #calendar-print-button[disabled], + #task-print-button[disabled] { + -moz-image-region: rect(48px 360px 72px 336px); + } + + toolbar[iconsize="small"] #calendar-print-button, + toolbar[iconsize="small"] #task-print-button { + -moz-image-region: rect(0px 240px 16px 224px); + } + + toolbar[iconsize="small"] #calendar-print-button[disabled], + toolbar[iconsize="small"] #task-print-button[disabled] { + -moz-image-region: rect(32px 240px 48px 224px); + } + + #calendar-unifinder-button { + -moz-image-region: rect(0px 528px 24px 504px); + } + + #calendar-unifinder-button[disabled] { + -moz-image-region: rect(48px 528px 72px 504px); + } + + toolbar[iconsize="small"] #calendar-unifinder-button { + -moz-image-region: rect(0px 352px 16px 336px); + } + + toolbar[iconsize="small"] #calendar-unifinder-button[disabled] { + -moz-image-region: rect(32px 352px 48px 336px); + } + + #hdrExtractEventButton { + list-style-image: url("chrome://calendar/skin/toolbar-small.png"); + -moz-image-region: rect(0px, 16px, 16px, 0px); + } + + #hdrExtractTaskButton { + list-style-image: url("chrome://calendar/skin/toolbar-small.png"); + -moz-image-region: rect(0px, 256px, 16px, 240px); + } + + #extractEventButton { + list-style-image: url("chrome://calendar/skin/toolbar-large.png"); + -moz-image-region: rect(0px, 24px, 24px, 0px); + } + + #extractTaskButton { + list-style-image: url("chrome://calendar/skin/toolbar-large.png"); + -moz-image-region: rect(0px, 384px, 24px, 360px); + } + + #extractEventButton[disabled] { + -moz-image-region: rect(48px, 24px, 72px, 0px); + } + + #extractTaskButton[disabled] { + -moz-image-region: rect(48px, 384px, 72px, 360px); + } + + toolbar[iconsize="small"] #extractEventButton { + list-style-image: url("chrome://calendar/skin/toolbar-small.png"); + -moz-image-region: rect(0px, 16px, 16px, 0px); + } + + toolbar[iconsize="small"] #extractTaskButton { + list-style-image: url("chrome://calendar/skin/toolbar-small.png"); + -moz-image-region: rect(0px, 256px, 16px, 240px); + } + + toolbar[iconsize="small"] #extractEventButton[disabled] { + -moz-image-region: rect(32px, 16px, 48px, 0px); + } + + toolbar[iconsize="small"] #extractTaskButton[disabled] { + -moz-image-region: rect(32px, 256px, 48px, 240px); + } +} + +@media not all and (-moz-os-version: windows-xp) { + #lightning-button-calendar { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#calendar-tab); + } + + #lightning-button-tasks { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#task-tab); + } + + #calendar-synchronize-button, + #task-synchronize-button { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#synchronize); + } + + #extractEventButton, + #task-newevent-button, + #hdrExtractEventButton, + #calendar-newevent-button { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#newevent); + } + + #extractTaskButton, + #task-newtask-button, + #hdrExtractTaskButton, + #calendar-newtask-button { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#newtask); + } + + #calendar-edit-button, + #task-edit-button { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#edit); + } + + #calendar-delete-button, + #task-delete-button { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#delete); + } + + #calendar-goto-today-button { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#today); + } + + #calendar-print-button, + #task-print-button { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#print); + } + + #calendar-unifinder-button { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#find); + } + + toolbar[brighttext] #lightning-button-calendar { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#calendar-tab-inverted); + } + + toolbar[brighttext] #lightning-button-tasks { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#task-tab-inverted); + } + + toolbar[brighttext] #calendar-synchronize-button, + toolbar[brighttext] #task-synchronize-button { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#synchronize-inverted); + } + + toolbar[brighttext] #extractEventButton, + toolbar[brighttext] #task-newevent-button, + toolbar[brighttext] #hdrExtractEventButton, + toolbar[brighttext] #calendar-newevent-button { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#newevent-inverted); + } + + toolbar[brighttext] #extractTaskButton, + toolbar[brighttext] #task-newtask-button, + toolbar[brighttext] #hdrExtractTaskButton, + toolbar[brighttext] #calendar-newtask-button { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#newtask-inverted); + } + + toolbar[brighttext] #calendar-edit-button, + toolbar[brighttext] #task-edit-button { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#edit-inverted); + } + + toolbar[brighttext] #calendar-delete-button, + toolbar[brighttext] #task-delete-button { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#delete-inverted); + } + + toolbar[brighttext] #calendar-goto-today-button { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#today-inverted); + } + + toolbar[brighttext] #calendar-print-button, + toolbar[brighttext] #task-print-button { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#print-inverted); + } + + toolbar[brighttext] #calendar-unifinder-button { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#find-inverted); + } +} + +@media (-moz-windows-default-theme) and (-moz-os-version: windows-win8), + (-moz-windows-default-theme) and (-moz-os-version: windows-win10) { + #lightning-button-calendar { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#calendar-tab-flat); + } + + #lightning-button-tasks { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#task-tab-flat); + } + + #calendar-synchronize-button, + #task-synchronize-button { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#synchronize-flat); + } + + #extractEventButton, + #task-newevent-button, + #hdrExtractEventButton, + #calendar-newevent-button { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#newevent-flat); + } + + #extractTaskButton, + #task-newtask-button, + #hdrExtractTaskButton, + #calendar-newtask-button { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#newtask-flat); + } + + #calendar-edit-button, + #task-edit-button { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#edit-flat); + } + + #calendar-goto-today-button { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#today-flat); + } + + #calendar-print-button, + #task-print-button { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#print-flat); + } + + #calendar-unifinder-button { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#find-flat); + } +} + +#lightning-button-calendar > .toolbarbutton-icon, +#lightning-button-tasks > .toolbarbutton-icon, +#calendar-synchronize-button > .toolbarbutton-icon, +#task-synchronize-button > .toolbarbutton-icon, +#extractEventButton > .toolbarbutton-icon, +#task-newevent-button > .toolbarbutton-icon, +#hdrExtractEventButton > .toolbarbutton-icon, +#calendar-newevent-button > .toolbarbutton-icon, +#extractTaskButton > .toolbarbutton-icon, +#task-newtask-button > .toolbarbutton-icon, +#hdrExtractTaskButton > .toolbarbutton-icon, +#calendar-newtask-button > .toolbarbutton-icon, +#calendar-edit-button > .toolbarbutton-icon, +#task-edit-button > .toolbarbutton-icon, +#calendar-delete-button > .toolbarbutton-icon, +#task-delete-button > .toolbarbutton-icon, +#calendar-goto-today-button > .toolbarbutton-icon, +#calendar-print-button > .toolbarbutton-icon, +#task-print-button > .toolbarbutton-icon, +#calendar-unifinder-button > .toolbarbutton-icon { + width: 18px; + height: 18px; + padding: 0; +} diff --git a/calendar/lightning/themes/windows/lightning-widgets.css b/calendar/lightning/themes/windows/lightning-widgets.css new file mode 100644 index 000000000..0c3e1aa5e --- /dev/null +++ b/calendar/lightning/themes/windows/lightning-widgets.css @@ -0,0 +1,10 @@ +/* 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/. */ + +lightning-notification-bar { + background-color: #baeeff; + color: -moz-dialogtext; + border-bottom: 1px solid ThreeDDarkShadow; + padding: 3px; +} diff --git a/calendar/lightning/themes/windows/lightning.css b/calendar/lightning/themes/windows/lightning.css new file mode 100644 index 000000000..f7f32dd1e --- /dev/null +++ b/calendar/lightning/themes/windows/lightning.css @@ -0,0 +1,420 @@ +/* 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/. */ + +@import url(chrome://lightning-common/skin/lightning.css); + +#calendarContent { + color: -moz-DialogText; + background-color: -moz-Dialog; +} + +/* Calendar list rules */ +#calendar-panel { + padding-bottom: 5px; +} + +/* Lightning preferences icon */ +radio[pane=paneLightning] { + list-style-image: url(chrome://calendar/skin/cal-icon32.png); +} + +/* iMIP notification bar */ +#imip-bar > image { + list-style-image: url(chrome://calendar/skin/cal-icon32.png); + -moz-image-region: rect(0 32px 32px 0); +} + +/* Lightning sidebar background in calendar and task mode */ +#ltnSidebar { + background-color: -moz-field; +} + +/* Today pane button in status bar */ +#calendar-status-todaypane-button, +#calendar-status-todaypane-button[checked="true"] { + min-width: 0; + min-height: 0; + margin: 1px 0 0; + -moz-appearance: none; + border-radius: 3px; + padding: 1px 2px 0 !important; + border: 1px solid transparent; +} + +#calendar-status-todaypane-button:hover { + border: 1px solid ThreeDShadow; + background-color: transparent !important; + background-image: none; + -moz-appearance: none; +} + +#calendar-status-todaypane-button[hideLabel] > stack { + margin-inline-start: 5px; +} + +#calendar-status-todaypane-button > stack > .toolbarbutton-day-text { + margin-top: 4px; +} + +#calendar-status-todaypane-button > .toolbarbutton-icon-end { + list-style-image: url(chrome://global/skin/icons/collapse.png); +} + +#calendar-status-todaypane-button[checked="true"] > .toolbarbutton-icon-end { + list-style-image: url(chrome://global/skin/icons/expand.png); +} + +/* shift the today pane button label up by one pixel to center it */ +#calendar-status-todaypane-button > .toolbarbutton-text { + margin: 0 0 1px !important; +} + +#calMinimonthBox { + margin-top: 3px; +} + +@media (-moz-os-version: windows-xp) { + #calsidebar_splitter { + border-right: none; + } + + #today-splitter { + border-right: none; + } + + #today-splitter:-moz-lwtheme { + background-image: linear-gradient(rgba(255, 255, 255, 0.5), + rgba(255, 255, 255, 0) 19px, rgba(255, 255, 255, 0) 25px, + ThreeDDarkShadow 25px, -moz-Dialog 26px); + } + + #newMsgButton-mail-menuitem { + list-style-image: url(chrome://messenger/skin/icons/mail-toolbar-small.png); + -moz-image-region: rect(0 32px 16px 16px); + } + + #newMsgButton-calendar-menuitem { + list-style-image: url(chrome://calendar/skin/toolbar-small.png); + -moz-image-region: rect(0 16px 16px 0); + } + + #newMsgButton-task-menuitem { + list-style-image: url(chrome://calendar/skin/toolbar-small.png); + -moz-image-region: rect(0 256px 16px 240px); + } + + #calendar-status-todaypane-button > stack > .toolbarbutton-icon-begin { + list-style-image: url(chrome://calendar/skin/toolbar-small.png); + -moz-image-region: rect(0 336px 16px 320px); + } + + /* ::: new tab buttons ::: */ + #calendar-tab-button { + list-style-image: url(chrome://lightning-common/skin/mode-switch-icons.png); + -moz-image-region: rect(0 64px 16px 48px); + } + + #calendar-tab-button[disabled] { + -moz-image-region: rect(32px 64px 48px 48px); + } + + #task-tab-button { + list-style-image: url(chrome://lightning-common/skin/mode-switch-icons.png); + -moz-image-region: rect(0 80px 16px 64px); + } + + #task-tab-button[disabled] { + -moz-image-region: rect(32px 80px 48px 64px); + } + + /* ::: tab icons ::: */ + .icon-holder[type="calendar"], + .tabmail-tab[type="calendar"] { + list-style-image: url(chrome://lightning-common/skin/mode-switch-icons.png); + -moz-image-region: rect(0 64px 16px 48px); + } + + .icon-holder[type="tasks"], + .tabmail-tab[type="tasks"] { + list-style-image: url(chrome://lightning-common/skin/mode-switch-icons.png); + -moz-image-region: rect(0 80px 16px 64px); + } + + /* ::: imip button icons ::: */ + .imipAcceptButton, + .imipAcceptRecurrencesButton { + list-style-image: url(chrome://lightning/skin/imip.png); + -moz-image-region: rect(0 16px 16px 0); + } + + .imipDeclineButton, + .imipDeclineRecurrencesButton { + list-style-image: url(chrome://lightning/skin/imip.png); + -moz-image-region: rect(0 32px 16px 16px); + } + + .imipTentativeButton, + .imipTentativeRecurrencesButton { + list-style-image: url(chrome://lightning/skin/imip.png); + -moz-image-region: rect(0 48px 16px 32px); + } + + .imipAddButton { + list-style-image: url(chrome://calendar/skin/toolbar-small.png); + -moz-image-region: rect(0 16px 16px 0); + } + + .imipUpdateButton { + list-style-image: url(chrome://calendar/skin/toolbar-small.png); + -moz-image-region: rect(0 432px 16px 416px); + } + + .imipDetailsButton { + list-style-image: url(chrome://calendar/skin/toolbar-small.png); + -moz-image-region: rect(0 352px 16px 336px); + } + + .imipDeleteButton { + list-style-image: url(chrome://calendar/skin/toolbar-small.png); + -moz-image-region: rect(0 48px 16px 32px); + } + + .imipReconfirmButton { + list-style-image: url(chrome://calendar/skin/tasks-actions.png); + -moz-image-region: rect(0 48px 16px 32px); + } +} + +@media not all and (-moz-os-version: windows-xp) { + #calendar-tab-button, + #newMsgButton-calendar-menuitem { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#calendar-tab); + -moz-image-region: auto; + } + + #task-tab-button, + #newMsgButton-task-menuitem { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#task-tab); + -moz-image-region: auto; + } + + #newMsgButton-mail-menuitem { + list-style-image: url(chrome://messenger/skin/icons/mail-toolbar.svg#newmsg); + } + + #tabs-toolbar[brighttext] #calendar-tab-button { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#calendar-tab-inverted); + } + + #tabs-toolbar[brighttext] #task-tab-button { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#task-tab-inverted); + } + + #calendar-status-todaypane-button > stack > .toolbarbutton-icon-begin { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#pane); + -moz-image-region: rect(0 18px 18px 0); + } + +#calendar-status-todaypane-button:-moz-lwtheme-brighttext > stack > + .toolbarbutton-icon-begin { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#pane-inverted); + } + /* compensate the 18px icon height */ + #calendar-status-todaypane-button > stack > .toolbarbutton-icon-begin { + margin-top: -1px; + margin-bottom: -1px; + } + + .icon-holder[type="calendar"], + .tabmail-tab[type="calendar"] { + list-style-image: url(chrome://lightning/skin/mode-switch-icons-aero.png); + -moz-image-region: rect(16px 16px 32px 0); + } + + .icon-holder[type="tasks"], + .tabmail-tab[type="tasks"] { + list-style-image: url(chrome://lightning/skin/mode-switch-icons-aero.png); + -moz-image-region: rect(16px 32px 32px 16px); + } + + /* ::: imip button icons ::: */ + .imipAcceptButton, + .imipAcceptRecurrencesButton { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#complete); + } + + .imipDeclineCounterButton, + .imipDeclineButton, + .imipDeclineRecurrencesButton { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#decline); + } + + .imipTentativeButton, + .imipTentativeRecurrencesButton { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#tentative); + } + + .imipAddButton { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#newevent); + } + + .imipRescheduleButton, + .imipUpdateButton { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#synchronize); + } + + .imipDetailsButton { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#find); + } + + .imipDeleteButton { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#delete); + } + + .imipReconfirmButton { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#priority); + } + + #newMsgButton-mail-menuitem > .menu-iconic-left > .menu-iconic-icon, + #newMsgButton-calendar-menuitem > .menu-iconic-left > .menu-iconic-icon, + #newMsgButton-task-menuitem > .menu-iconic-left > .menu-iconic-icon { + width: 18px; + height: 18px; + margin: -1px; + } + + #calsidebar_splitter, + #today-splitter { + border: none; + min-width: 0; + width: 5px; + background-color: transparent; + margin-top: 0; + position: relative; + z-index: 10; + transition: border-width .3s ease-in; + } + + #calsidebar_splitter { + border-inline-start: 1px solid #a9b7c9; + margin-inline-end: -5px; + } + + #calsidebar_splitter[state="collapsed"] { + border-inline-start: 1px solid transparent; + } + + #calsidebar_splitter[state="collapsed"]:hover { + border-inline-start: 4px solid highlight; + } + + #today-splitter { + border-inline-end: 1px solid #a9b7c9; + margin-inline-start: -5px; + position: relative; + } + + #today-splitter.calendar-sidebar-splitter:-moz-lwtheme { + background-image: none; + } + + #today-splitter > grippy { + display: none; + } + + #today-pane-splitter { + border: none; + border-bottom: 3px double #a9b7c9; + min-height: 0; + height: 5px; + background-color: transparent; + margin-top: -3px; + position: relative; + z-index: 10; + } +} + +@media (-moz-windows-default-theme) and (-moz-os-version: windows-win8), + (-moz-windows-default-theme) and (-moz-os-version: windows-win10) { + #calendar-tab-button, + #newMsgButton-calendar-menuitem { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#calendar-tab-flat); + -moz-image-region: auto; + } + + #task-tab-button, + #newMsgButton-task-menuitem { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#task-tab-flat); + -moz-image-region: auto; + } + + #newMsgButton-mail-menuitem { + list-style-image: url(chrome://messenger/skin/icons/mail-toolbar.svg#newmsg-flat); + } + + #calendar-status-todaypane-button > stack > .toolbarbutton-icon-begin { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#pane-flat); + } + + /* ::: imip button icons ::: */ + .imipAcceptButton, + .imipAcceptRecurrencesButton { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#complete-flat); + } + + .imipDeclineCounterButton, + .imipDeclineButton, + .imipDeclineRecurrencesButton { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#decline-flat); + } + + .imipTentativeButton, + .imipTentativeRecurrencesButton { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#tentative-flat); + } + + .imipAddButton { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#newevent-flat); + } + + .imipRescheduleButton, + .imipUpdateButton { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#synchronize-flat); + } + + .imipDetailsButton { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#find-flat); + } + + .imipReconfirmButton { + list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#priority-flat); + } +} + +@media (-moz-windows-default-theme) { + #imip-view-toolbar > .toolbarbutton-1.msgHeaderView-button, + #imip-view-toolbar > .toolbarbutton-1.msgHeaderView-button > .toolbarbutton-menubutton-button, + #imip-view-toolbar > .toolbarbutton-1.msgHeaderView-button > .toolbarbutton-menubutton-dropmarker { + border-color: var(--toolbarbutton-active-bordercolor); + background-image: linear-gradient(-moz-dialog, -moz-dialog); + } + + #imip-view-toolbar > .toolbarbutton-1.msgHeaderView-button:not(:active):hover, + #imip-view-toolbar > .toolbarbutton-1.msgHeaderView-button:-moz-any(:hover,[open="true"]) > + .toolbarbutton-menubutton-button, + #imip-view-toolbar > .toolbarbutton-1.msgHeaderView-button:hover > + .toolbarbutton-menubutton-dropmarker { + background-image: linear-gradient(rgba(0, 0, 0, .1), rgba(0, 0, 0, .1)), + linear-gradient(-moz-dialog, -moz-dialog); + } + + #imip-view-toolbar > .toolbarbutton-1.msgHeaderView-button:not([type="menu-button"]):hover:active, + #imip-view-toolbar > .toolbarbutton-1.msgHeaderView-button[type="menu-button"] > + .toolbarbutton-menubutton-button:hover:active, + #imip-view-toolbar > .toolbarbutton-1.msgHeaderView-button[open="true"] > + .toolbarbutton-menubutton-dropmarker { + background-image: linear-gradient(rgba(0, 0, 0, .15), rgba(0, 0, 0, .15)), + linear-gradient(-moz-dialog, -moz-dialog); + } +} diff --git a/calendar/lightning/versions.mk b/calendar/lightning/versions.mk new file mode 100644 index 000000000..1445bdc1f --- /dev/null +++ b/calendar/lightning/versions.mk @@ -0,0 +1,11 @@ +# 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/. + +# Lighting version number +THUNDERBIRD_VERSION := $(MOZ_APP_VERSION) +THUNDERBIRD_MAXVERSION := $(MOZ_APP_VERSION) +SEAMONKEY_VERSION := $(MOZ_APP_MAXVERSION) +SEAMONKEY_MAXVERSION := $(MOZ_APP_MAXVERSION) +LIGHTNING_VERSION := $(shell $(PYTHON) $(topsrcdir)/calendar/lightning/build/makeversion.py $(word 1,$(MOZ_PKG_VERSION) $(THUNDERBIRD_VERSION))) +GDATA_VERSION := $(shell $(PYTHON) $(topsrcdir)/calendar/providers/gdata/makeversion.py $(LIGHTNING_VERSION)) |