!import
1 <?xml version="1.0"?>
2 <!--
3 - ***** BEGIN LICENSE BLOCK *****
4 - Version: MPL 1.1/GPL 2.0/LGPL 2.1
5 -
6 - The contents of this file are subject to the Mozilla Public License Version
7 - 1.1 (the "License"); you may not use this file except in compliance with
8 - the License. You may obtain a copy of the License at
9 - http://www.mozilla.org/MPL/
10 -
11 - Software distributed under the License is distributed on an "AS IS" basis,
12 - WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
13 - for the specific language governing rights and limitations under the
14 - License.
15 -
16 - The Original Code is Mail folder code.
17 -
18 - The Initial Developer of the Original Code is
19 - Joey Minta <jminta@gmail.com>
20 - Portions created by the Initial Developer are Copyright (C) 2008
21 - the Initial Developer. All Rights Reserved.
22 -
23 - Contributor(s):
24 -
25 - Alternatively, the contents of this file may be used under the terms of
26 - either the GNU General Public License Version 2 or later (the "GPL"), or
27 - the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
28 - in which case the provisions of the GPL or the LGPL are applicable instead
29 - of those above. If you wish to allow use of your version of this file only
30 - under the terms of either the GPL or the LGPL, and not to allow others to
31 - use your version of this file under the terms of the MPL, indicate your
32 - decision by deleting the provisions above and replace them with the notice
33 - and other provisions required by the GPL or the LGPL. If you do not delete
34 - the provisions above, a recipient may use your version of this file under
35 - the terms of any one of the MPL, the GPL or the LGPL.
36 -
37 - ***** END LICENSE BLOCK *****
38 -->
39
40 <bindings id="mailFolderBindings"
41 xmlns="http://www.mozilla.org/xbl"
42 xmlns:xbl="http://www.mozilla.org/xbl"
43 xmlns:html="http://www.w3.org/1999/xhtml"
44 xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
45
46 <binding id="folder-menupopup"
47 extends="chrome://global/content/bindings/popup.xml#popup">
48 <implementation>
constructor
49 <constructor><![CDATA[
50 // If we are a child of a menulist, we need to build our content right
51 // away, otherwise the menulist won't have proper sizing
52 if (this.parentNode && this.parentNode.localName == "menulist")
53 this._ensureInitialized();
54 ]]></constructor>
55 <!--
56 - Make sure we remove our listener when the window is being destroyed
57 -->
destructor
58 <destructor><![CDATA[
59 if (!this._initialized)
60 return;
61
62 const Cc = Components.classes;
63 const Ci = Components.interfaces;
64 var session = Cc["@mozilla.org/messenger/services/session;1"].
65 getService(Ci.nsIMsgMailSession);
66 session.RemoveFolderListener(this._listener);
67 ]]></destructor>
68
69 <!--
70 - If non-null, the subFolders of this nsIMsgFolder will be used to
71 - populate this menu. If this is null, the menu will be populated
72 - using the root-folders for all accounts
73 -->
field__parentFolder
74 <field name="_parentFolder">null</field>
get_parentFolder
set_parentFolder
75 <property name="parentFolder"
76 onget="return this._parentFolder;"
77 onset="return this._parentFolder = val;"/>
78
79 <!--
80 - Various filtering modes can be used with this menu-binding. To use
81 - one of them, append the mode="foo" attribute to the element. When
82 - building the menu, we will then use this._filters[mode] as a filter
83 - function to eliminate folders that should not be shown.
84 -
85 - Note that extensions should feel free to plug in here!
86 -->
field__filters
87 <field name="_filters"><![CDATA[({
88 // Returns true if messages can be filed in the folder
filter_filing
89 filing: function filter_filing(aFolder) {
90 if (!aFolder.server.canFileMessagesOnServer)
91 return false;
92
93 return (aFolder.canFileMessages || aFolder.hasSubFolders);
94 },
95
96 // Returns true if we can get mail for this folder. (usually this just
97 // means the "root" fake folder)
filter_getMail
98 getMail: function filter_getMail(aFolder) {
99 if (aFolder.isServer && aFolder.server.type != "none")
100 return true;
101 if (aFolder.server.type == "nntp" || aFolder.server.type == "rss")
102 return true;
103 return false;
104 },
105
106 // Returns true if we can add filters to this folder/account
filter_filter
107 filters: function filter_filter(aFolder) {
108 // We can always filter news
109 if (aFolder.server.type == "nntp")
110 return true;
111
112 return aFolder.server.canHaveFilters;
113 },
114
filter_subscribe
115 subscribe: function filter_subscribe(aFolder) {
116 return aFolder.canSubscribe;
117 }
118 })]]></field>
119
120 <!--
121 - The maximum number of entries in the "Recent" menu
122 -->
field__MAXRECENT
123 <field name="_MAXRECENT">15</field>
124
125 <!--
126 - Our listener to let us know when folders change/appear/disappear so
127 - we can know to rebuild ourselves.
128 -->
field__listener
129 <field name="_listener"><![CDATA[({
130 _menu: this,
act_add
131 OnItemAdded: function act_add(aRDFParentItem, aItem) {
132 aItem.QueryInterface(Components.interfaces.nsIMsgFolder);
133 if (this._filterFunction && !this._filterFunction(aItem)) {
134 return;
135 }
136 //xxx we can optimize this later
137 //xxx I'm not quite sure why this isn't always a function
138 if (this._menu._teardown)
139 this._menu._teardown();
140 },
141
act_remove
142 OnItemRemoved: function act_remove(aRDFParentItem, aItem) {
143 aItem.QueryInterface(Components.interfaces.nsIMsgFolder);
144 if (this._filterFunction && !this._filterFunction(aItem)) {
145 return;
146 }
147 //xxx we can optimize this later
148 if (this._menu._teardown)
149 this._menu._teardown();
150 },
151
152 //xxx I stole this listener list from nsMsgFolderDatasource.cpp, but
153 // someone should really document what events are fired when, so that
154 // we make sure we're updating at the right times.
OnItemPropertyChanged
155 OnItemPropertyChanged: function(aItem, aProperty, aOld, aNew) {},
OnItemIntPropertyChanged
156 OnItemIntPropertyChanged: function(aItem, aProperty, aOld, aNew) {
157 var child = this._getChildForItem(aItem);
158 if (child) {
159 child._folder = aItem;
160 this._menu._setCssSelectors(child, child._folder);
161 }
162 },
OnItemBoolPropertyChanged
163 OnItemBoolPropertyChanged: function(aItem, aProperty, aOld, aNew) {
164 var child = this._getChildForItem(aItem);
165 if (child) {
166 child._folder = aItem;
167 this._menu._setCssSelectors(child, child._folder);
168 }
169 },
OnItemUnicharPropertyChanged
170 OnItemUnicharPropertyChanged: function(aItem, aProperty, aOld, aNew) {
171 var child = this._getChildForItem(aItem);
172 if (child) {
173 child._folder = aItem;
174 this._menu._setCssSelectors(child, child._folder);
175 }
176 },
OnItemPropertyFlagChanged
177 OnItemPropertyFlagChanged: function(aItem, aProperty, aOld, aNew) {},
OnItemEvent
178 OnItemEvent: function(aFolder, aEvent) {
179 var child = this._getChildForItem(aFolder);
180 if (child) {
181 // Special casing folder renames here, since they require more work
182 // since sort-order may have changed
183 if (aEvent.toString() == "RenameCompleted") {
184 this._menu._teardown();
185 this._menu._ensureInitialized();
186 return;
187 }
188 }
189 },
190
191 /**
192 * Helper function to check and see whether we have a menuitem for this
193 * particular nsIMsgFolder
194 *
195 * @param aItem the nsIMsgFolder to check
196 * @returns null if no child for that folder exists, otherwise the
197 * menuitem for that child
198 */
act__itemIsChild
Called: Object:QueryInterface (15 calls, 189 v-uS)
199 _getChildForItem: function act__itemIsChild(aItem) {
200 if (!this._menu || !this._menu._initialized)
201 return null;
202
203 var item = aItem.QueryInterface(Components.interfaces.nsIMsgFolder);
204 for (var i = 0; i < this._menu.childNodes; i++) {
205 var folder = this._menu.childNodes[i]._folder;
206 if (folder && folder.URI == item.URI)
207 return item;
208 }
209 return null;
210 }
211 })]]></field>
212
213 <!--
214 - True if we have already built our menu-items and are now just
215 - listening for changes
216 -->
field__initialized
217 <field name="_initialized">false</field>
218
219 <!--
220 - Call this if you are unsure whether the menu-items have been built,
221 - but know that they need to be built now if they haven't.
222 -->
223 <method name="_ensureInitialized">
_ensureInitialized
224 <body><![CDATA[
225 if (this._initialized)
226 return;
227
228 // I really wish they'd just make this global...
229 const Cc = Components.classes;
230 const Ci = Components.interfaces;
231
232 var folders = new Array();
233
234 // Figure out which folders to build. If we don't have a parent, then
235 // we assume we should build the top-level accounts. (Actually we
236 // build the fake root folders for those accounts.)
237 if (!this._parentFolder) {
238 var acctMgr = Cc["@mozilla.org/messenger/account-manager;1"].
239 getService(Ci.nsIMsgAccountManager);
240 var count = acctMgr.accounts.Count();
241 for (var i = 0; i < count; i++) {
242 var acct = acctMgr.accounts.GetElementAt(i).QueryInterface(Ci.nsIMsgAccount);
243 folders.push(acct.incomingServer.rootFolder);
244 }
245 } else {
246 // If we do have a parent folder, then we just build based on those
247 // subFolders for that parent
248 var myenum = this._parentFolder.subFolders;
249 while (myenum.hasMoreElements()) {
250 folders.push(myenum.getNext().QueryInterface(Ci.nsIMsgFolder));
251 }
252 }
253
254 // Lastly, we add a listener to get notified of changes in the folder
255 // structure.
256 var session = Cc["@mozilla.org/messenger/services/session;1"].
257 getService(Ci.nsIMsgMailSession);
258 session.AddFolderListener(this._listener, Ci.nsIFolderListener.all);
259
260 this._build(folders);
261 this._initialized = true;
262 ]]></body>
263 </method>
264
265 <!--
266 - Actually constructs the menu-items based on the folders given
267 -
268 - @param aFolders an array of nsIMsgFolders to use for building
269 -->
270 <method name="_build">
271 <parameter name="aFolders"/>
_build
272 <body><![CDATA[
273 // I'm not entirely happy about having to hard-code this as an attr,
274 // but there doesn't seem to be a better way.
275 if (this.getAttribute("showFileHereLabel") == "true" &&
276 this._parentFolder &&
277 (this._parentFolder.noSelect || this._parentFolder.canFileMessages)) {
278 var menuitem = document.createElement("menuitem");
279 menuitem._folder = this._parentFolder;
280 menuitem.setAttribute("label", this.getAttribute("fileHereLabel"));
281 menuitem.setAttribute("accessKey", this.getAttribute("fileHereAccessKey"));
282 // Eww. have to support some legacy code here...
283 menuitem.setAttribute("id", this._parentFolder.URI);
284 this.appendChild(menuitem);
285
286 if (this._parentFolder.noSelect)
287 menuitem.setAttribute("disabled", "true");
288
289 this.appendChild(document.createElement("menuseparator"));
290 }
291
292 // Some menus want a "Recent" option, but that should only be on our
293 // top-level menu
294 if (!this._parentFolder && this.getAttribute("showRecent") == "true") {
295 this._buildRecentMenu();
296 }
297
298 var folders;
299 // Extensions and other consumers can add to these modes too, see the
300 // above note on the _filters field.
301 var mode = this.getAttribute("mode");
302 if (mode && mode != "") {
303 var filterFunction = this._filters[mode];
304 folders = aFolders.filter(filterFunction);
305 this._listener._filterFunction = filterFunction;
306 } else {
307 folders = aFolders;
308 }
309
310 /**
311 * Sorts the list of folders. We give first priority to the sortKey
312 * property, and then via a case-insensitive comparison of names
313 */
nameCompare
314 function nameCompare(a, b) {
315 var sortKey = a.compareSortKeys(b);
316 if (sortKey)
317 return sortKey;
318 return a.prettyName.toLowerCase() > b.prettyName.toLowerCase();
319 }
320 folders = folders.sort(nameCompare);
321
322 for each (var folder in folders) {
323 var node;
324 // If we're going to add subFolders, we need to make menus, not
325 // menuitems.
326 if (!folder.hasSubFolders || this.getAttribute("expandFolders") == "false") {
327 node = document.createElement("menuitem");
328 // Grumble, grumble, legacy code support
329 node.setAttribute("id", folder.URI);
330 node.setAttribute("class", "folderMenuItem menuitem-iconic");
331 this.appendChild(node);
332 } else {
333 //xxx this is slightly problematic in that we haven't confirmed
334 // whether any of the subfolders will pass the filter
335 node = document.createElement("menu");
336 node.setAttribute("class", "folderMenuItem menu-iconic");
337 this.appendChild(node);
338 var popup = this.cloneNode(true);
339 popup._teardown();
340 node.appendChild(popup);
341 popup.parentFolder = folder;
342 }
343 node._folder = folder;
344 node.setAttribute("label", folder.prettyName);
345
346 this._setCssSelectors(folder, node);
347
348 //xxx for later optimization
349 //builtFolders.push(folder);
350 }
351 ]]></body>
352 </method>
353
354 <!--
355 - Builds a submenu with all of the recently used folders in it, to
356 - allow for easy access.
357 -->
358 <method name="_buildRecentMenu">
359 <body><![CDATA[
360 const Cc = Components.classes;
361 const Ci = Components.interfaces;
362 // Iterate through all folders in all accounts, and check MRU_Time,
363 // then take the most current 15.
364
365 /**
366 * This function will iterate through any existing sub-folders and
367 * (1) check if they're recent and (2) recursively call this function
368 * to iterate through any sub-sub-folders.
369 *
370 * @param aFolder the folder to check
371 */
checkSubFolders
372 function checkSubFolders(aFolder) {
373 if (!aFolder.hasSubFolders)
374 return;
375
376 var myenum = aFolder.subFolders;
377 while (myenum.hasMoreElements()) {
378 var folder = myenum.getNext().QueryInterface(Ci.nsIMsgFolder);
379 addIfRecent(folder);
380 checkSubFolders(folder);
381 }
382 }
383
384 var recentFolders = [];
385 var oldestTime = 0;
386
387 /**
388 * This function will add a folder to the recentFolders array if it
389 * is among the 15 most recent. If we exceed 15 folders, it will pop
390 * the oldest folder, ensuring that we end up with the right number
391 *
392 * @param aFolder the folder to check
393 */
394 var menu = this;
addIfRecent
395 function addIfRecent(aFolder) {
396 if (!aFolder.canFileMessages)
397 return;
398
399 var time = aFolder.getStringProperty("MRUTime") || 0;
400 if (time <= oldestTime)
401 return;
402
403 if (recentFolders.length == menu._MAXRECENT) {
404 recentFolders.sort(sorter);
405 recentFolders.pop();
406 oldestTime = recentFolders[recentFolders.length-1].getStringProperty("MRUTime");
407 }
408 recentFolders.push(aFolder);
409 }
410
411 // Start iterating at the top of the hierarchy, that is, with the root
412 // folders for each account.
413 var acctMgr = Cc["@mozilla.org/messenger/account-manager;1"].
414 getService(Ci.nsIMsgAccountManager);
415 var count = acctMgr.accounts.Count();
416 for (var i = 0; i < count; i++) {
417 var acct = acctMgr.accounts.GetElementAt(i).QueryInterface(Ci.nsIMsgAccount);
418 addIfRecent(acct.incomingServer.rootFolder);
419 checkSubFolders(acct.incomingServer.rootFolder);
420 }
421
sorter
422 function sorter(a, b) {
423 return a.getStringProperty("MRUTime") < b.getStringProperty("MRUTime");
424 }
425 recentFolders.sort(sorter);
426
427 // Because we're scanning across multiple accounts, we can end up with
428 // two folders with the same name. In this case, we append the name
429 // of the account as well, to distingiush.
430 var dupeNames = [];
431 for (var i = 0; i < recentFolders.length; i++) {
432 for (var j = i + 1; j < recentFolders.length; j++) {
433 // We can end up with the same name in dupeNames more than once,
434 // but that's ok.
435 if (recentFolders[i].prettyName == recentFolders[j].prettyName)
436 dupeNames.push(recentFolders[i].prettyName);
437 }
438 }
439
440 // Now create the Recent folder and its children
441 var menu = document.createElement("menu");
442 menu.setAttribute("label", this.getAttribute("recentLabel"));
443 menu.setAttribute("accessKey", this.getAttribute("recentAccessKey"));
444 var popup = document.createElement("menupopup");
445 menu.appendChild(popup);
446
447 // Create entries for each of the recent folders.
448 for each (var folder in recentFolders) {
449 var node = document.createElement("menuitem");
450
451 var label = folder.prettyName;
452 if (dupeNames.indexOf(label) != -1)
453 label += " - " + folder.server.prettyName;
454 node.setAttribute("label", label);
455
456 node.setAttribute("class", "folderMenuItem menuitem-iconic");
457 this._setCssSelectors(folder, node);
458 popup.appendChild(node);
459 }
460 this.appendChild(menu);
461 var sep = document.createElement("menuseparator");
462 this.appendChild(sep);
463 ]]></body>
464 </method>
465
466 <!--
467 - This function adds attributes on menu/menuitems to make it easier for
468 - css to style them.
469 -
470 - @param aFolder the folder that corresponds to the menu/menuitem
471 - @param aMenuNode the actual DOM node to set attributes on
472 -->
473 <method name="_setCssSelectors">
474 <parameter name="aFolder"/>
475 <parameter name="aMenuNode"/>
_setCssSelectors
476 <body><![CDATA[
477 const Ci = Components.interfaces;
478 // First set the SpecialFolder attribute
479 // Sigh... why aren't these in an IDL somewhere public?
480 if (aFolder.flags & 0x1000) // MSG_FOLDER_FLAG_INBOX
481 aMenuNode.setAttribute("SpecialFolder", "Inbox");
482 else if (aFolder.flags & 0x0100) // MSG_FOLDER_FLAG_TRASH
483 aMenuNode.setAttribute("SpecialFolder", "Trash");
484 else if (aFolder.flags & 0x0800) // MSG_FOLDER_FLAG_QUEUE
485 aMenuNode.setAttribute("SpecialFolder", "Unsent Messages");
486 else if (aFolder.flags & 0x0200) // MSG_FOLDER_FLAG_SENTMAIL
487 aMenuNode.setAttribute("SpecialFolder", "Sent");
488 else if (aFolder.flags & 0x0400) // MSG_FOLDER_FLAG_DRAFTS
489 aMenuNode.setAttribute("SpecialFolder", "Drafts");
490 else if (aFolder.flags & 0x400000) // MSG_FOLDER_FLAG_TEMPLATES
491 aMenuNode.setAttribute("SpecialFolder", "Templates");
492 else if (aFolder.flags & 0x40000000) // MSG_FOLDER_FLAG_JUNK
493 aMenuNode.setAttribute("SpecialFolder", "Junk");
494 else if (aFolder.flags & 0x0020) // MSG_FOLDER_FLAG_VIRTUAL
495 aMenuNode.setAttribute("SpecialFolder", "Virtual");
496 else
497 aMenuNode.setAttribute("SpecialFolder", "none");
498
499 // Now set the biffState
500 var biffStates = ["NewMail", "NoMail", "UnknownMail"];
501 for each (var state in biffStates) {
502 if (aFolder.biffState == Ci.nsIMsgFolder["nsMsgBiffState_" + state]) {
503 aMenuNode.setAttribute("BiffState", state);
504 break;
505 }
506 }
507
508 // IsServer is simple
509 aMenuNode.setAttribute("IsServer", aFolder.isServer);
510
511 // We have to work a bit for IsSecure. This sucks
512 var server = aFolder.server;
513 if (server instanceof Ci.nsINntpIncomingServer) {
514 aMenuNode.setAttribute("IsSecure", server.isSecure);
515 } else {
516 // If it's not a news-server, apparently we look at the socket type
517 var sock = server.socketType;
518 var isSecure = (sock == Ci.nsIMsgIncomingServer.alwaysUseTLS ||
519 sock == Ci.nsIMsgIncomingServer.useSSL);
520 aMenuNode.setAttribute("IsSecure", isSecure);
521 }
522
523 // the ServerType attribute
524 aMenuNode.setAttribute("ServerType", aFolder.server.type);
525 ]]></body>
526 </method>
527
528 <!--
529 - Makes a given folder selected.
530 -
531 - @param aFolder the folder to select
532 - @note If aFolder is not in this popup, but is instead a descendant of
533 - a member of the popup, that ancestor will be selected.
534 -->
535 <method name="selectFolder">
536 <parameter name="aFolder"/>
selectFolder
537 <body><![CDATA[
538 for (var i in this.childNodes) {
539 var child = this.childNodes[i];
540 if (!child || !child._folder)
541 continue;
542 if (child._folder.URI == aFolder.URI || (this._parentFolder &&
543 this._parentFolder.isAncestorOf(aFolder))) {
544 // Making an assumption about our DOM positioning here.
545 this.parentNode.selectedIndex = i;
546 return;
547 }
548 }
549 Components.utils.reportError("unable to find folder to select!");
550 ]]></body>
551 </method>
552
553 <!--
554 - Removes all menu-items for this popup and resets all fields
555 -->
556 <method name="_teardown">
_teardown
557 <body><![CDATA[
558 while(this.hasChildNodes())
559 this.removeChild(this.lastChild);
560 this._folders = null;
561 this._initialized = false;
562 ]]></body>
563 </method>
564 </implementation>
565
566 <handlers>
567 <!--
568 - In order to improve performance, we're not going to build any of the
569 - menu until we're shown (unless we're the child of a menulist, see
570 - note in the constructor).
571 -
572 - @note _ensureInitialized can be called repeatedly without issue, so
573 - don't worry about it here.
574 -->
575 <handler event="popupshowing" phase="capturing">
576 this._ensureInitialized();
577 </handler>
578 </handlers>
579 </binding>
580 </bindings>