]> err.no Git - scalable-opengroupware.org/blob - UI/MailerUI/UIxMailTree.m
git-svn-id: http://svn.opengroupware.org/SOGo/inverse/trunk@1065 d1b88da0-ebda-0310...
[scalable-opengroupware.org] / UI / MailerUI / UIxMailTree.m
1 /*
2   Copyright (C) 2004 SKYRIX Software AG
3
4   This file is part of OpenGroupware.org.
5
6   OGo is free software; you can redistribute it and/or modify it under
7   the terms of the GNU Lesser General Public License as published by the
8   Free Software Foundation; either version 2, or (at your option) any
9   later version.
10
11   OGo is distributed in the hope that it will be useful, but WITHOUT ANY
12   WARRANTY; without even the implied warranty of MERCHANTABILITY or
13   FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
14   License for more details.
15
16   You should have received a copy of the GNU Lesser General Public
17   License along with OGo; see the file COPYING.  If not, write to the
18   Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
19   02111-1307, USA.
20 */
21
22 #import "common.h"
23
24 #import <SoObjects/Mailer/SOGoMailBaseObject.h>
25 #import <SoObjects/Mailer/SOGoMailAccount.h>
26 #import <SoObjects/Mailer/SOGoMailFolder.h>
27 #import <NGObjWeb/SoComponent.h>
28 #import <NGObjWeb/SoObject+SoDAV.h>
29
30 #import "UIxMailTree.h"
31 #import "UIxMailTreeBlock.h"
32
33 /*
34   Support special icons:
35     tbtv_leaf_corner_17x17.gif
36     tbtv_inbox_17x17.gif
37     tbtv_drafts_17x17.gif
38     tbtv_sent_17x17.gif
39     tbtv_trash_17x17.gif
40 */
41
42 @interface NSString(DotCutting)
43
44 - (NSString *)stringByCuttingOffAtDotsWhenExceedingLength:(int)_maxLength;
45
46 - (NSString *)titleForSOGoIMAP4String;
47
48 @end
49
50 @implementation UIxMailTree
51
52 static BOOL debugBlocks = NO;
53
54 + (void)initialize
55 {
56   [UIxMailTreeBlock class]; // ensure that globals are initialized
57 }
58
59 - (id) init
60 {
61   if ((self = [super init]))
62     {
63       flattenedNodes = nil;
64     }
65
66   return self;
67 }
68
69 - (void) dealloc
70 {
71   [treeFolderAction release];
72   [rootClassName release];
73   [rootNodes release];
74   [item release];
75   [flattenedNodes release];
76   [super dealloc];
77 }
78
79 /* icons */
80
81 - (NSString *) defaultIconName
82 {
83   return @"tbtv_leaf_corner_17x17.gif";
84 }
85
86 - (NSString *)iconNameForType:(NSString *)_type {
87   if (![_type isNotNull])
88     return [self defaultIconName];
89   
90   //return @"tbtv_drafts_17x17.gif";
91   
92   return [self defaultIconName];
93 }
94
95 /* accessors */
96
97 - (void)setRootClassName:(id)_rootClassName {
98   ASSIGNCOPY(rootClassName, _rootClassName);
99 }
100 - (id)rootClassName {
101   return rootClassName;
102 }
103
104 - (void)setItem:(id)_item {
105   ASSIGN(item, _item);
106 }
107 - (id)item {
108   return item;
109 }
110
111 - (void)setTreeFolderAction:(NSString *)_action {
112   ASSIGNCOPY(treeFolderAction, _action);
113 }
114 - (NSString *)treeFolderAction {
115   return treeFolderAction;
116 }
117
118 - (NSString *)itemIconName {
119   // TODO: only called once!
120   NSString *ftype;
121   
122   ftype = [[self item] valueForKey:@"outlookFolderClass"];
123   return [self iconNameForType:ftype];
124 }
125
126 /* fetching subfolders */
127
128 - (NSArray *)fetchSubfoldersOfObject:(id)_object {
129   /* Walk over toManyRelationshipKeys and lookup the controllers for them. */
130   NSMutableArray *ma;
131   NSArray  *names;
132   unsigned i, count;
133   
134   if ((names = [_object toManyRelationshipKeys]) == nil) {
135     if (debugBlocks) [self logWithFormat:@"no to-many: %@", _object];
136     return nil;
137   }
138   
139   if (debugBlocks) {
140     [self logWithFormat:@"to-many: %@ %@", _object,
141           [names componentsJoinedByString:@","]];
142   }
143
144   count = [names count];
145   ma    = [NSMutableArray arrayWithCapacity:(count + 1)];
146   for (i = 0; i < count; i++) {
147     id folder;
148     
149     // TODO: use some context or reuse the main context?
150     folder = [_object lookupName:[names objectAtIndex:i] inContext:nil 
151                       acquire:NO];
152     if (folder == nil) {
153       if (debugBlocks) {
154         [self logWithFormat:@"  DID NOT FIND FOLDER %@: %@",
155                 _object,
156                 [names objectAtIndex:i]];
157       }
158       continue;
159     }
160     if ([folder isKindOfClass:[NSException class]]) {
161       if (debugBlocks) {
162         [self logWithFormat:@"  FOLDER LOOKUP EXCEPTION %@: %@",
163                 [names objectAtIndex:i], folder];
164       }
165       continue;
166     }
167     
168     [ma addObject:folder];
169   }
170   if (debugBlocks)
171     [self logWithFormat:@"  returning: %@ %@", _object, ma];
172   return ma;
173 }
174
175 /* navigation nodes */
176
177 - (BOOL)isRootObject:(id)_object {
178   if (![_object isNotNull]) {
179     [self warnWithFormat:@"(%s): got to root by nil lookup ...",
180             __PRETTY_FUNCTION__];
181     return YES;
182   }
183
184   if ([_object isKindOfClass:NSClassFromString(@"SOGoUserFolder")])
185     return YES;
186   
187   return [_object isKindOfClass:NSClassFromString([self rootClassName])];
188 }
189
190 - (NSString *)treeNavigationLinkForObject:(id)_object
191                                   atDepth:(int)_depth
192 {
193   NSMutableString *link;
194   int i;
195   
196   link = [NSMutableString new];
197   [link autorelease];
198
199   for (i = 0; i < _depth; i++)
200     [link appendString: @"../"];
201
202   [link appendFormat: @"%@/%@",
203         [_object nameInContainer],
204         [self treeFolderAction]];
205   
206   return link;
207 }
208
209 - (void) getTitle: (NSString **)_t
210        folderType: (NSString **)_ft
211           andIcon: (NSString **)_icon
212         forObject: (id)_object
213 {
214   // TODO: need to refactor for reuse!
215   NSString *ftype;
216   unsigned len;
217
218 //   if ([_object respondsToSelector: @selector (outlookFolderClass)])
219 //     ftype = [_object outlookFolderClass];
220 //   else
221     ftype = [_object valueForKey:@"outlookFolderClass"];
222   len = [ftype length];
223   
224   *_ft = nil;
225
226   switch (len) {
227   case 8:
228     if ([ftype isEqualToString:@"IPF.Sent"]) {
229       *_t = [self labelForKey:@"SentFolderName"];
230       *_icon = @"tbtv_sent_17x17.gif";
231       *_ft = @"sent";
232       return;
233     }
234     break;
235   case 9:
236     if ([ftype isEqualToString:@"IPF.Inbox"]) {
237       *_t = [self labelForKey:@"InboxFolderName"];
238       *_icon = @"tbtv_inbox_17x17.gif";
239       *_ft = @"inbox";
240       return;
241     }
242     if ([ftype isEqualToString:@"IPF.Trash"]) {
243       *_t = [self labelForKey:@"TrashFolderName"];
244       *_icon = @"tbtv_trash_17x17.gif";
245       *_ft = @"trash";
246       return;
247     }
248     break;
249   case 10:
250     if ([ftype isEqualToString:@"IPF.Drafts"]) {
251       *_t = [self labelForKey:@"DraftsFolderName"];
252       *_icon = @"tbtv_drafts_17x17.gif";
253       *_ft = @"drafts";
254       return;
255     }
256 //     if ([ftype isEqualToString:@"IPF.Filter"]) {
257 //       *_t = [self labelForKey:@"SieveFolderName"];
258 //       *_icon = nil;
259 //       *_ft = @"sieve";
260 //       return;
261 //     }
262     break;
263   }
264
265   *_t    = [_object davDisplayName];
266   *_icon = nil;
267   
268   if ([_object isKindOfClass:NSClassFromString(@"SOGoMailFolder")])
269     *_icon = nil;
270   else if ([_object isKindOfClass:NSClassFromString(@"SOGoMailAccount")]) {
271     *_icon = @"tbtv_account_17x17.gif";
272
273     *_ft = @"account";
274     
275     /* title processing is somehow Agenor specific and should be done in UI */
276     *_t = [[_object nameInContainer] titleForSOGoIMAP4String];
277   }
278   else if ([_object isKindOfClass:NSClassFromString(@"SOGoMailAccounts")])
279     *_icon = @"tbtv_account_17x17.gif";
280   else if ([_object isKindOfClass:NSClassFromString(@"SOGoUserFolder")])
281     *_icon = @"tbtv_inbox_17x17.gif";
282   else {
283     // TODO: use drafts icon for other SOGo folders
284     *_icon = @"tbtv_drafts_17x17.gif";
285   }
286 }
287
288 - (UIxMailTreeBlock *) treeNavigationBlockForLeafNode: (id) _o
289                                               atDepth: (int) _d
290 {
291   UIxMailTreeBlock *md;
292   NSString *n, *i, *ft;
293   id blocks;
294
295   /* 
296      Trigger plus in treeview if it has subfolders. It is an optimization that
297      we do not generate blocks for folders which are not displayed anyway.
298   */
299   blocks = [[_o toManyRelationshipKeys] count] > 0
300     ? UIxMailTreeHasChildrenMarker
301     : nil;
302
303   [self getTitle: &n folderType: &ft andIcon: &i forObject:_o];
304
305   md = [UIxMailTreeBlock blockWithName: nil
306                          title: n
307                          iconName: i
308                          link: [self treeNavigationLinkForObject:_o atDepth:_d]
309                          isPathNode:NO
310                          isActiveNode:NO
311                          childBlocks: blocks];
312   return md;
313 }
314
315 - (UIxMailTreeBlock *)treeNavigationBlockForRootNode:(id)_object {
316   /*
317      This generates the block for the root object (root of the tree, we get
318      there by walking up the chain starting with the client object).
319   */
320   UIxMailTreeBlock *md;
321   NSMutableArray   *blocks;
322   NSArray          *folders;
323   NSString         *title, *icon, *ft;
324   unsigned         i, count;
325
326   if (debugBlocks) {
327     [self logWithFormat:@"block for root node 0x%08X<%@>", 
328             _object, NSStringFromClass([_object class])];
329   }
330   
331   /* process child folders */
332   
333   folders = [self fetchSubfoldersOfObject:_object];
334   count   = [folders count];
335   blocks  = [NSMutableArray arrayWithCapacity:count];
336   for (i = 0; i < count; i++) {
337     id block;
338     
339     block = [self treeNavigationBlockForLeafNode: [folders objectAtIndex:i]
340                   atDepth:0];
341     if ([block isNotNull]) [blocks addObject:block];
342   }
343   if ([blocks count] == 0)
344     blocks = nil;
345   
346   /* build block */
347   
348   [self getTitle:&title folderType: &ft andIcon:&icon forObject:_object];
349
350   md = [UIxMailTreeBlock blockWithName: [_object nameInContainer]
351                          title: title
352                          iconName: icon
353                          link: [@"../" stringByAppendingString:
354                                    [_object nameInContainer]]
355                          isPathNode: YES
356                          isActiveNode: YES
357                          childBlocks: blocks];
358   return md;
359 }
360
361 - (UIxMailTreeBlock *) fullTreeNavigationBlockForNode: (id)_object
362 {
363   UIxMailTreeBlock *md;
364   NSMutableArray   *blocks;
365   NSArray          *folders;
366   NSString         *title, *icon, *ft;
367   unsigned         i, count;
368
369   if (debugBlocks)
370     [self logWithFormat:@"block for root node 0x%08X<%@>", 
371             _object, NSStringFromClass([_object class])];
372   
373   folders = [self fetchSubfoldersOfObject: _object];
374   count   = [folders count];
375   blocks  = [NSMutableArray arrayWithCapacity: count];
376   for (i = 0; i < count; i++)
377     {
378       id block;
379     
380       block = [self fullTreeNavigationBlockForNode: [folders objectAtIndex:i]];
381       if ([block isNotNull]) [blocks addObject:block];
382     }
383
384   if (![blocks count])
385     blocks = nil;
386   
387   [self getTitle: &title folderType: &ft andIcon: &icon forObject: _object];
388 //   NSLog (@"*********** title = '%@'/icon = '%@'", title, icon);
389
390   md = [UIxMailTreeBlock blockWithName: [_object nameInContainer]
391                          title: title
392                          iconName: icon
393                          link: [@"../" stringByAppendingString:
394                                    [_object nameInContainer]]
395                          isPathNode: YES
396                          isActiveNode: YES
397                          childBlocks: blocks];
398   [md setFolderType: ft];
399
400   return md;
401 }
402
403 - (UIxMailTreeBlock *) treeNavigationBlockForActiveNode: (id) _object
404 {
405   /* 
406      This generates the block for the clientObject (the object which has the 
407      focus)
408   */
409   UIxMailTreeBlock *md;
410   NSMutableArray   *blocks;
411   NSArray  *folders;
412   NSString *title, *icon, *ft;
413   unsigned i, count;
414
415   // TODO: maybe we can join the two implementations, this might not be
416   //       necessary
417   if ([self isRootObject:_object]) /* we are at the top */
418     return [self treeNavigationBlockForRootNode:_object];
419   
420   if (debugBlocks) {
421     [self logWithFormat:@"block for active node 0x%08X<%@> - %@", 
422             _object, NSStringFromClass([_object class]),
423             [_object davDisplayName]];
424   }
425   
426   /* process child folders */
427   
428   folders = [self fetchSubfoldersOfObject:_object];
429   count   = [folders count];
430   blocks  = [NSMutableArray arrayWithCapacity:count];
431   for (i = 0; i < count; i++) {
432     UIxMailTreeBlock *block;
433     
434     block = [self treeNavigationBlockForLeafNode: [folders objectAtIndex:i]
435                   atDepth: 0];
436     if ([block isNotNull]) [blocks addObject:block];
437   }
438   if ([blocks count] == 0) blocks = nil;
439
440   /* build block */
441   
442   [self getTitle:&title folderType: &ft andIcon:&icon forObject:_object];
443   md = [UIxMailTreeBlock blockWithName: [_object nameInContainer]
444                          title: title
445                          iconName: icon
446                          link: @"."
447                          isPathNode: YES
448                          isActiveNode: YES
449                          childBlocks: blocks];
450   return md;
451 }
452
453 - (UIxMailTreeBlock *)
454   treeNavigationBlockForObject: (id) _object
455           withActiveChildBlock: (UIxMailTreeBlock *) _activeChildBlock 
456                          depth: (int) _depth
457 {
458   /*
459     Note: 'activeChildBlock' here doesn't mean that the block is the selected
460           folder in the tree. Its just the element which is active in the
461           list of subfolders.
462   */
463   UIxMailTreeBlock *resultBlock;
464   NSMutableArray   *blocks;
465   NSString         *activeName;
466   NSArray          *folders;
467   NSString         *title, *icon, *ft;
468   unsigned         i, count;
469   
470   activeName = [_activeChildBlock valueForKey:@"name"];
471   
472   /* process child folders */
473   
474   folders = [self fetchSubfoldersOfObject:_object];
475   count   = [folders count];
476   blocks  = [NSMutableArray arrayWithCapacity:count == 0 ? 1 : count];
477   for (i = 0; i < count; i++) {
478     UIxMailTreeBlock *block;
479     id folder;
480     
481     folder = [folders objectAtIndex:i];
482     block = [activeName isEqualToString:[folder nameInContainer]]
483       ? _activeChildBlock
484       : [self treeNavigationBlockForLeafNode: folder
485               atDepth:_depth];
486     
487     if ([block isNotNull]) [blocks addObject:block];
488   }
489   if ([blocks count] == 0) {
490     if (_activeChildBlock != nil) // if the parent has no proper fetchmethod!
491       [blocks addObject:_activeChildBlock];
492     else
493       blocks = nil;
494   }
495
496   /* build block */
497   
498   [self getTitle:&title folderType: &ft andIcon:&icon forObject:_object];
499   resultBlock
500     = [UIxMailTreeBlock blockWithName: [_object nameInContainer]
501                         title: title
502                         iconName: icon
503                         link:
504                           [self treeNavigationLinkForObject: _object 
505                                 atDepth: (_depth + 1)]
506                         isPathNode:YES isActiveNode:NO
507                         childBlocks:blocks];
508   
509   /* recurse up unless we are at the root */
510
511   if ([self isRootObject:_object]) /* we are at the top */
512     return resultBlock;
513   
514   return [self treeNavigationBlockForObject:[_object container] 
515                withActiveChildBlock:resultBlock
516                depth:(_depth + 1)];
517 }
518
519 - (UIxMailTreeBlock *)buildNavigationNodesForObject:(id)_object {
520   /*
521     This is the top-level 'flattening' method. The _object is the active
522     object in the tree, that is, usually a "current folder".
523     
524     The tree will show:
525     all subfolders of the current folder,
526     all parent folders of the current folder up to some root,
527     all siblings along the parent chain.
528   */
529   UIxMailTreeBlock *block;
530   
531   /* 
532      This is the cursor, we create nodes below that for direct subfolders
533   */
534   if (debugBlocks) [self logWithFormat:@"ACTIVE block ..."];
535   block = [self treeNavigationBlockForActiveNode:_object];
536   if (debugBlocks) [self logWithFormat:@"  ACTIVE block: %@", block];
537   
538   if ([self isRootObject:_object]) {
539     if (debugBlocks) [self logWithFormat:@"  active block is root."];
540     return block;
541   }
542   
543   /* 
544      The following returns the root block. It calculates the chain up to the
545      root folder starting with the parent of the current object.
546   */
547   if (debugBlocks) [self logWithFormat:@"ACTIVE parent block ..."];
548   block = [self treeNavigationBlockForObject:[_object container] 
549                 withActiveChildBlock:block
550                 depth: 1];
551   if (debugBlocks) [self logWithFormat:@"done: %@", block];
552   return block;
553 }
554
555 /* tree */
556
557 - (NSArray *)rootNodes {
558   UIxMailTreeBlock *navNode;
559   
560   if (rootNodes != nil)
561     return rootNodes;
562   
563   navNode = [self buildNavigationNodesForObject:[self clientObject]];
564   
565   if ([navNode hasChildren] && [navNode areChildrenLoaded])
566     rootNodes = [[navNode children] retain];
567   else if (navNode)
568     rootNodes = [[NSArray alloc] initWithObjects:&navNode count:1];
569   
570   return rootNodes;
571 }
572
573 - (int) addNodes: (NSArray *) nodes
574         atSerial: (int) startSerial
575        forParent: (int) parent
576     withRootName: (NSString *) rootName
577          toArray: (NSMutableArray *) array
578 {
579   unsigned int count, max, currentSerial;
580   UIxMailTreeBlock *curNode;
581   NSString *fullName;
582
583   max = [nodes count];
584   currentSerial = startSerial;
585   for (count = 0; count < max; count++)
586     {
587       curNode = [nodes objectAtIndex: count];
588       fullName = [rootName stringByAppendingFormat: @"/%@", [curNode name]];
589       [curNode setName: fullName];
590       [curNode setSerial: currentSerial];
591       [curNode setParent: parent];
592       [array addObject: curNode];
593       if ([curNode hasChildren])
594         currentSerial = [self addNodes: [curNode children]
595                               atSerial: currentSerial + 1
596                               forParent: currentSerial
597                               withRootName: fullName
598                               toArray: array];
599       else
600         currentSerial++;
601     }
602
603   return currentSerial;
604 }
605
606 - (NSArray *) flattenedNodes
607 {
608   UIxMailTreeBlock *rootNode; // , *curNode;
609   id mailAccounts;
610 //   unsigned int count, max;
611
612   if (!flattenedNodes)
613     {
614       flattenedNodes = [NSMutableArray new];
615
616       if (![[self clientObject] isKindOfClass: NSClassFromString(@"SOGoMailAccounts")])
617         mailAccounts = [[self clientObject] mailAccountsFolder];
618       else
619         mailAccounts = [self clientObject];
620
621       rootNode = [self fullTreeNavigationBlockForNode: mailAccounts];
622       [self addNodes: [rootNode children]
623             atSerial: 1
624             forParent: 0
625             withRootName: @""
626             toArray: flattenedNodes];
627 //       max = [flattenedBlocks count];
628 //       for (count = 0; count < max; count++)
629 //      {
630 //        curNode = [flattenedBlocks objectAtIndex: count];
631 //        NSLog (@"%d: %@/%@", count, [curNode title], [curNode iconName]);
632 //      }
633     }
634
635   return flattenedNodes;
636 }
637
638 /* notifications */
639
640 - (void) sleep
641 {
642   [item release]; item = nil;
643   [rootNodes release]; rootNodes = nil;
644   [super sleep];
645 }
646
647 @end /* UIxMailTree */
648
649
650 @implementation NSString(DotCutting)
651
652 - (NSString *)stringByCuttingOffAtDotsWhenExceedingLength:(int)_maxLength {
653   NSRange  r, r2;
654   NSString *s;
655   int      i;
656   
657   if ([self length] <= _maxLength) /* if length is small, return as is */
658     return self;
659   
660   if ((r = [self rangeOfString:@"."]).length == 0)
661     /* no dots in share, return even if longer than boundary */
662     return self;
663   
664   s = self;
665   i  = r.location + r.length;
666   r2 = [s rangeOfString:@"." options:NSLiteralSearch 
667           range:NSMakeRange(i, [s length] - i)];
668     
669   if (r2.length > 0) {
670     s = [s substringToIndex:r2.location];
671     if ([s length] <= _maxLength) /* if length is small, return as is */
672       return s;
673   }
674   
675   /* no second dot, and the whole was too long => cut off after first */
676   return [s substringToIndex:r.location];
677 }
678
679 - (NSString *)titleForSOGoIMAP4String {
680   /* 
681      eg:
682        guizmo.g.-.baluh.hommes.tests-montee-en-charge-ogo@\
683        amelie-01.ac.melanie2.i2
684   */
685   static int CutOffLength = 16;
686   NSString *s;
687   NSRange  r;
688   
689   s = self;
690   
691   /* check for connect strings without hostnames */
692   
693   r = [s rangeOfString:@"@"];
694   if (r.length == 0) {
695     /* no login provide, just use the hostname (without domain) */
696     r = [s rangeOfString:@"."];
697     return r.length > 0 ? [s substringToIndex:r.location] : s;
698   }
699   
700   s = [s substringToIndex:r.location];
701   
702   /* check for shares */
703   
704   r = [s rangeOfString:@".-."];
705   if (r.length > 0) {
706     /* eg: 'baluh.hommes.tests-montee-en-charge-ogo' */
707     s = [s substringFromIndex:(r.location + r.length)];
708     
709     return [s stringByCuttingOffAtDotsWhenExceedingLength:CutOffLength];
710   }
711   
712   /* just the login name, possibly long (test.et.di.cete-lyon) */
713   return [s stringByCuttingOffAtDotsWhenExceedingLength:CutOffLength];
714 }
715
716 @end /* NSString(DotCutting) */