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