/* copyright 2002 Alexander Malmberg */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "MsgDB.h" #include "FolderWindowController.h" #include "FolderThreader.h" #include "KeyWindow.h" #include "MessageViewController.h" #include "main.h" #include "Pref_ReadAhead.h" #define SORT_THREAD 0 #define SORT_R_THREAD 1 #define SORT_SUBJECT 2 #define SORT_FROM 3 #define SORT_DATE 4 #define SORT_ORDER 5 #define SORT_R_SUBJECT 6 #define SORT_R_FROM 7 #define SORT_R_DATE 8 #define SORT_R_ORDER 9 #define SORT_MAX 9 static int sort_subject(NSNumber *n1,NSNumber *n2,FolderThreader *ft) { NSString *s1,*s2; s1=[ft subjectForMessage: [n1 unsignedIntValue]]; s2=[ft subjectForMessage: [n2 unsignedIntValue]]; return [s1 localizedCaseInsensitiveCompare: s2]; } static int sort_from(NSNumber *n1,NSNumber *n2,FolderThreader *ft) { NSString *s1,*s2; s1=[ft fromForMessage: [n1 unsignedIntValue]]; s2=[ft fromForMessage: [n2 unsignedIntValue]]; return [s1 localizedCaseInsensitiveCompare: s2]; } static int sort_date(NSNumber *n1,NSNumber *n2,FolderThreader *ft) { NSDate *d1,*d2; d1=[ft dateForMessage: [n1 unsignedIntValue]]; d2=[ft dateForMessage: [n2 unsignedIntValue]]; return [d1 compare: d2]; } static int sort_r_subject(NSNumber *n1,NSNumber *n2,FolderThreader *ft) { return -sort_subject(n1,n2,ft); } static int sort_r_from(NSNumber *n1,NSNumber *n2,FolderThreader *ft) { return -sort_from(n1,n2,ft); } static int sort_r_date(NSNumber *n1,NSNumber *n2,FolderThreader *ft) { return -sort_date(n1,n2,ft); } static int sort_order(NSNumber *n1,NSNumber *n2,FolderThreader *ft) { if (ft->msgs[[n1 unsignedIntValue]].midmsgs[[n2 unsignedIntValue]].mid) return NSOrderedAscending; else return NSOrderedDescending; } static int sort_r_order(NSNumber *n1,NSNumber *n2,FolderThreader *ft) { if (ft->msgs[[n1 unsignedIntValue]].mid>ft->msgs[[n2 unsignedIntValue]].mid) return NSOrderedAscending; else return NSOrderedDescending; } static int (*(sort_funcs[SORT_MAX+1]))(NSNumber *,NSNumber *,FolderThreader *ft)= {NULL,NULL, sort_subject,sort_from,sort_date,sort_order, sort_r_subject,sort_r_from,sort_r_date,sort_r_order}; @interface IndentCell : NSCell { int level; } -(void) setLevel: (int)l; @end @implementation IndentCell -(void) setLevel: (int)l { level=l; } -(void) drawWithFrame: (NSRect)f inView: (NSView *)v { if (level>8) { NSString *s=[[NSString alloc] initWithFormat: @"%i",level]; [s drawInRect: f withAttributes: nil]; DESTROY(s); } f.origin.x+=level*16; f.size.width-=level*16; [super drawWithFrame: f inView: v]; } @end #define RA_NEXT_UNREAD 0 #define RA_NEXT 1 #define RA_PREV 2 #define RA_NEXT_THREAD 3 #define RA_MAX 4 #define RA_NONE 4 /* TODO: read-ahead to a greater depth? (ie. use the 'weight'; get the two next unread, then three, etc. */ @interface FolderWindowController (readahead) -(void) readAheadForMessage: (int)num; -(void) cancelCurrentReadAhead; -(void) readAheadWeight: (int)which; @end @implementation FolderWindowController (readahead) static float ra_priority[RA_MAX]; -(void) _readAheadMid: (msg_id_t)mid : (int)limit : (int)pri; { const char *c; int size; int status; c=[mdb msg_getHeader: "Bytes" : mid]; if (!c) return; size=atoi(c); if (!size || size>limit) { return; } status=[mdb msg_dstatus: mid]; if (status==DSTATUS_ERROR) return; [mdb msg_wantData: mid priority: pri]; } /* TODO: more options */ -(void) readAheadForMessage: (int)num { int limit; int i; Class p=[Pref_ReadAhead class]; if (![Pref_ReadAhead readAhead]) return; limit=[Pref_ReadAhead readAheadSizeLimit]; for (i=0;imsgs[s].mid; } if ([p readAheadNextThread]) { /* next thread */ s=[ft findNextMax: 0 from: num+1]; if (s!=ft->num_msgs) { s=[ft findNextUnreadFrom: s]; if (s!=-1) cur_read_ahead[RA_NEXT_THREAD]=ft->msgs[s].mid; } } s=num; if ([p readAheadPrevious]) { /* previous message */ if (s>0) cur_read_ahead[RA_PREV]=ft->msgs[s-1].mid; } if ([p readAheadNext]) { /* next message */ if (snum_msgs-1) cur_read_ahead[RA_NEXT]=ft->msgs[s+1].mid; } } else { if ([p readAheadPrevious] && num>0) { cur_read_ahead[RA_PREV]= ft->msgs[[[sorted_msgs objectAtIndex: num-1] unsignedIntValue]].mid; } if ([p readAheadNext] && num<[sorted_msgs count]-1) { cur_read_ahead[RA_NEXT]= ft->msgs[[[sorted_msgs objectAtIndex: num+1] unsignedIntValue]].mid; } } /* TODO: if eg. next unread and next are the same, the max of their priority should be used */ for (i=0;i3) ra_priority[i]=3; } else { ra_priority[i]-=1.0; if (ra_priority[i]<-5) ra_priority[i]=-5; } } } @end @interface FolderWindowController (private) -(int) indexOf: (msg_id_t) mid; -(void) selectMid: (msg_id_t) mid; -(msg_id_t) selectedMid; -(void) setSortMode: (int)ns; -(void) fixSort; @end @implementation FolderWindowController (private) -(void) selectMessage: (int)num { if (num<0 || num>=ft->num_msgs) return; [list selectRow: num byExtendingSelection: NO]; if (num<5) [list scrollRowToVisible: 0]; else [list scrollRowToVisible: num-5]; if (num+5num_msgs) [list scrollRowToVisible: num+5]; else [list scrollRowToVisible: ft->num_msgs-1]; [list scrollRowToVisible: num]; } -(msg_id_t) selectedMid { int s=[list selectedRow]; if (s==-1) return 0; if (sort_mode!=SORT_THREAD) { [self fixSort]; s=[[sorted_msgs objectAtIndex: s] unsignedIntValue]; } return ft->msgs[s].mid; } -(int) indexOf: (msg_id_t)mid { int r; int i,c; r=[ft indexOf: mid]; if (r==-1) return -1; if (sort_mode==SORT_THREAD) return r; [self fixSort]; c=[sorted_msgs count]; for (i=0;inum_msgs; sorted_msgs=[[NSMutableArray alloc] initWithCapacity: n]; p=n; while (p>0) { np=p-1; for (p=np;p>=0;p--) if (!ft->msgs[p].level) break; if (p<0) p=0; for (i=p;i<=np;i++) { idx=[[NSNumber alloc] initWithUnsignedInt: i]; [sorted_msgs addObject: idx]; [idx release]; } } } else { if (!sorted_msgs) { int i,n; msg_id_t *msgs; NSNumber *idx; n=[folder numMessages]; msgs=[folder getMessages]; sorted_msgs=[[NSMutableArray alloc] initWithCapacity: n]; for (i=0;imsgs[idx].rstatus) { ft->msgs[idx].rstatus=nrstatus; [list reloadData]; } return; } if ([[n name] isEqual: MsgDB_MsgMetaChangeNotification] && (idx=[ft indexOf: [n mid]])!=-1) { [list reloadData]; /* TODO: only need reload if the message is visible */ return; } } -(int) numberOfRowsInTableView: (NSTableView *)tv { if (sort_mode==SORT_THREAD) return ft->num_msgs; else { if (sorted_msgs) return [sorted_msgs count]; if (sort_mode==SORT_R_THREAD) return ft->num_msgs; else return [folder numMessages]; } } -(id) tableView: (NSTableView *)tv objectValueForTableColumn: (NSTableColumn *)tc row: (int)row { if (sort_mode!=SORT_THREAD) { if (!sorted_msgs) [self fixSort]; row=[[sorted_msgs objectAtIndex: row] unsignedIntValue]; } if (tc==c_subject) return [ft subjectForMessage: row]; else if (tc==c_from) return [ft fromForMessage: row]; else if (tc==c_date) { NSDate *d=[ft dateForMessage: row]; NSTimeInterval ti=[d timeIntervalSinceNow]; NSString *frmt; if (ti>0) /* TODO: make this customizable? */ return d; else if (ti>-18*6*60) frmt=@"%H:%M:%S"; else if (ti>-(6*24+18)*60*60) frmt=@"%a %H:%M:%S"; else return d; return [d descriptionWithCalendarFormat: frmt timeZone: nil locale: nil]; } else return @"Unknown column"; } -(void) tableView: (NSTableView *)tv willDisplayCell: (NSCell *)c forTableColumn: (NSTableColumn *)tc row: (int)row { if (sort_mode!=SORT_THREAD) { if (!sorted_msgs) [self fixSort]; row=[[sorted_msgs objectAtIndex: row] unsignedIntValue]; } if (ft->msgs[row].rstatus) [c setFont: [NSFont systemFontOfSize: 0]]; else [c setFont: [NSFont boldSystemFontOfSize: 0]]; if (tc==c_subject) { if (sort_mode==SORT_THREAD || sort_mode==SORT_R_THREAD) [(IndentCell *)c setLevel: ft->msgs[row].level]; else [(IndentCell *)c setLevel: 0]; } } -(void) folderSortThread { [self setSortMode: SORT_THREAD]; } -(void) folderSortReverseThread { [self setSortMode: SORT_R_THREAD]; } -(void) folderSortSubject { [self setSortMode: SORT_SUBJECT]; } -(void) folderSortReverseSubject { [self setSortMode: SORT_R_SUBJECT]; } -(void) folderSortFrom { [self setSortMode: SORT_FROM]; } -(void) folderSortReverseFrom { [self setSortMode: SORT_R_FROM]; } -(void) folderSortDate { [self setSortMode: SORT_DATE]; } -(void) folderSortReverseDate { [self setSortMode: SORT_R_DATE]; } -(void) folderSortOrder { [self setSortMode: SORT_ORDER]; } -(void) folderSortReverseOrder { [self setSortMode: SORT_R_ORDER]; } - initWithMsgDB: (MsgDB *)m folder: (NSString *)fname { NSWindow *win; win=[[KeyWindow alloc] initWithContentRect: NSMakeRect(100,100,550,500) styleMask: NSClosableWindowMask|NSTitledWindowMask|NSResizableWindowMask|NSMiniaturizableWindowMask backing: NSBackingStoreRetained defer: YES]; if (!(self=[super initWithWindow: win])) return nil; folder=[[m folders] objectForKey: fname]; if (!folder) { RELEASE(self); return nil; } [folder retain]; ASSIGN(mdb,m); folder_name=[fname copy]; ft=[[FolderThreader alloc] initWithMsgDB: mdb target: self]; { NSSplitView *spv; spv=[[NSSplitView alloc] initWithFrame: NSMakeRect(0,0,550,500)]; [spv setAutoresizingMask: NSViewWidthSizable|NSViewHeightSizable]; { [win setTitle: folder_name]; c_subject=[[NSTableColumn alloc] initWithIdentifier: @"Subject"]; [[c_subject headerCell] setStringValue: _(@"Subject")]; [c_subject setEditable: NO]; [c_subject setResizable: YES]; [c_subject setWidth: 300]; { IndentCell *c=[[IndentCell alloc] init]; [c_subject setDataCell: c]; [c release]; } c_from=[[NSTableColumn alloc] initWithIdentifier: @"From"]; [[c_from headerCell] setStringValue: _(@"From")]; [c_from setEditable: NO]; [c_from setResizable: YES]; [c_from setWidth: 100]; c_date=[[NSTableColumn alloc] initWithIdentifier: @"Date"]; [[c_date headerCell] setStringValue: _(@"Date")]; [c_date setEditable: NO]; [c_date setResizable: YES]; [c_date setWidth: 100]; listsv=[[NSScrollView alloc] initWithFrame: NSMakeRect(0,0,250,250)]; /* TODO */ [listsv setAutoresizingMask: NSViewWidthSizable|NSViewHeightSizable]; [listsv setAutoresizesSubviews: YES]; [listsv setHasVerticalScroller: YES]; [listsv setHasHorizontalScroller: YES]; [listsv setBorderType: NSBezelBorder]; list=[[NSTableView alloc] initWithFrame: [[listsv contentView] frame]]; [list setAllowsColumnReordering: YES]; [list setAllowsColumnResizing: YES]; [list setAllowsMultipleSelection: NO]; [list setAllowsColumnSelection: NO]; [list addTableColumn: c_subject]; [list addTableColumn: c_from]; [list addTableColumn: c_date]; [list setDataSource: self]; [list setDelegate: self]; [list setDoubleAction: @selector(openMessage:)]; [list setAutosaveName: [NSString stringWithFormat: @"Folder_%@",fname]]; [list setAutosaveTableColumns: YES]; [listsv setDocumentView: list]; [spv addSubview: listsv]; } { /* TODO: fix properly */ NSScrollView *sv; NSTextView *tv; sv=[[NSScrollView alloc] initWithFrame: NSMakeRect(0,0,250,250)]; [sv setHasVerticalScroller: YES]; [sv setHasHorizontalScroller: YES]; [sv setBorderType: NSBezelBorder]; tv=[[NSTextView alloc] initWithFrame: [[sv contentView] frame]]; [tv setHorizontallyResizable: NO]; [tv setVerticallyResizable: YES]; [tv setEditable: NO]; [[tv textContainer] setWidthTracksTextView: YES]; [[tv textContainer] setHeightTracksTextView: NO]; [[tv textContainer] setContainerSize: NSMakeSize(1e6,1e6)]; [tv setAutoresizingMask: NSViewWidthSizable|NSViewHeightSizable]; [sv setDocumentView: tv]; [spv addSubview: sv]; mv=[[MessageViewController alloc] initWithMsgDB: mdb textView: tv scrollView: sv]; [sv release]; [tv release]; } [win setContentView: spv]; [spv release]; { NSString *frame_name=[NSString stringWithFormat: @"Folder_%@",fname]; [win setFrameUsingName: frame_name]; [win setFrameAutosaveName: frame_name]; } } [win setDelegate: self]; [win release]; [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(updateFolder:) name: MsgDB_FolderAddMsgNotification object: mdb]; [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(updateFolder:) name: MsgDB_MsgMetaChangeNotification object: mdb]; { int n,i; msg_id_t *m; n=[folder numMessages]; m=[folder getMessages]; for (i=0;iSORT_MAX) ns=SORT_THREAD; [self setSortMode: ns]; } return self; } -(void) dealloc { if (folder_name) { [[NSUserDefaults standardUserDefaults] setInteger: sort_mode forKey: [NSString stringWithFormat: @"FolderSortMode_%@",folder_name]]; } [[NSNotificationCenter defaultCenter] removeObserver: self]; DESTROY(listsv); DESTROY(list); DESTROY(c_subject); DESTROY(c_date); DESTROY(c_from); DESTROY(mv); DESTROY(ft); DESTROY(sorted_msgs); DESTROY(folder); DESTROY(folder_name); DESTROY(mdb); [super dealloc]; } -(void) tableViewSelectionDidChange: (NSNotification *)n { [self cancelCurrentReadAhead]; [mv setMid: [self selectedMid]]; [self readAheadForMessage: [self indexOf: [self selectedMid]]]; /* TODO */ } -(void) openMessage: (id)sender { if ([self selectedMid]) [app_delegate openMessageWindow: [self selectedMid]]; } -(void) messageToggleRead: (id)sender { int selected=[list selectedRow]; if (selected==-1) return; if (sort_mode!=SORT_THREAD) { [self fixSort]; selected=[[sorted_msgs objectAtIndex: selected] unsignedIntValue]; } if (ft->msgs[selected].rstatus==1) [mdb msg_setMetaHeader: "RStatus" value: "0" : ft->msgs[selected].mid]; else [mdb msg_setMetaHeader: "RStatus" value: "1" : ft->msgs[selected].mid]; } -(void) moveToNextUnread: (id)sender { int s=[list selectedRow]; [self readAheadWeight: RA_NEXT_UNREAD]; if (sort_mode==SORT_THREAD) [ft selectFirstUnreadFrom: s+1]; /* this is valid even if s==-1 */ else { int i,c; [self fixSort]; c=[sorted_msgs count]; for (i=s+1;imsgs[ [[sorted_msgs objectAtIndex: i] unsignedIntValue] ].rstatus) { [self selectMessage: i]; break; } } } -(void) scrollNextUnread: (id)sender { if ([mv scrollDown]) [self moveToNextUnread: nil]; } -(void) skipBranch: (id)sender { if (sort_mode==SORT_THREAD) { int s=[list selectedRow]; if (s==-1) return; s=[ft findNextMax: ft->msgs[s].level from: s+1]; [ft selectFirstUnreadFrom: s]; } } -(void) skipBranchMark: (id)sender { if (sort_mode==SORT_THREAD) { int s=[list selectedRow],n; if (s==-1) return; n=[ft findNextMax: ft->msgs[s].level from: s+1]; [ft markAsRead: s:n-1]; [ft selectFirstUnreadFrom: n]; } } -(void) skipThread: (id)sender { [self readAheadWeight: RA_NEXT_THREAD]; if (sort_mode==SORT_THREAD) { int s=[list selectedRow]; if (s==-1) return; s=[ft findNextMax: 0 from: s+1]; [ft selectFirstUnreadFrom: s]; } } -(void) skipThreadMark: (id)sender { [self readAheadWeight: RA_NEXT_THREAD]; if (sort_mode==SORT_THREAD) { int s=[list selectedRow],n,p; if (s==-1) return; n=[ft findNextMax: 0 from: s+1]; p=[ft findPrevMax: 0 from: s]; [ft markAsRead: p:n-1]; [ft selectFirstUnreadFrom: n]; } } -(void) moveToParent: (id)sender { if (sort_mode==SORT_THREAD) { int s=[list selectedRow]; if (s==-1) return; if (!ft->msgs[s].level) return; [self selectMessage: [ft findPrevMax: ft->msgs[s].level-1 from: s-1]]; } } -(void) markAll: (id)sender { int i; for (i=0;inum_msgs;i++) { if (ft->msgs[i].rstatus) continue; [mdb msg_setMetaHeader: "RStatus" value: "1" : ft->msgs[i].mid]; } } -(int) keyDown: (NSEvent *)ke inWindow: (NSWindow *)w { NSString *str=[ke charactersIgnoringModifiers]; unichar c; if ([str length]!=1) return 0; c=[str characterAtIndex: 0]; if (!([ke modifierFlags]&~NSShiftKeyMask) || [ke modifierFlags]==NSFunctionKeyMask) { int s=[list selectedRow]; switch (c) { case NSUpArrowFunctionKey: case 'p': if (s>0) { [self readAheadWeight: RA_PREV]; [self selectMessage: s-1]; } return 1; case NSDownArrowFunctionKey: if (snum_msgs-1) { [self readAheadWeight: RA_NEXT]; [self selectMessage: s+1]; } return 1; case NSHomeFunctionKey: if (ft->num_msgs) [self selectMessage: 0]; return 1; case NSEndFunctionKey: if (ft->num_msgs) [self selectMessage: ft->num_msgs-1]; return 1; case NSPageDownFunctionKey: if (s==-1) [self selectMessage: 0]; else { s=s+floor(([[listsv contentView] frame].size.height/[list rowHeight])); s--; if (s>=ft->num_msgs) s=ft->num_msgs-1; /* might happen if the window is really small (less than one row visible */ if (s<0) s=0; [self selectMessage: s]; } return 1; case NSPageUpFunctionKey: if (s==-1) [self selectMessage: 0]; else { s=s-floor(([[listsv contentView] frame].size.height/[list rowHeight])); s++; if (s<0) s=0; if (s>=ft->num_msgs) s=ft->num_msgs-1; [self selectMessage: s]; } return 1; case 's': [mv lineUp]; return 1; case 'x': [mv lineDown]; return 1; case ' ': [self scrollNextUnread: nil]; return 1; case 'n': [self moveToNextUnread: nil]; return 1; case 'b': if (sort_mode!=SORT_THREAD) return 0; [self skipBranch: nil]; return 1; case 't': if (sort_mode!=SORT_THREAD) return 0; [self skipThread: nil]; return 1; case 'B': if (sort_mode!=SORT_THREAD) return 0; [self skipBranchMark: nil]; return 1; case 'T': if (sort_mode!=SORT_THREAD) return 0; [self skipThreadMark: nil]; return 1; case 'P': if (sort_mode!=SORT_THREAD) return 0; [self moveToParent: nil]; return 1; case 'm': [self messageToggleRead: nil]; return 1; case 'A': [self markAll: nil]; return 1; } } return 0; } -(void) composeNewArticle: (id)sender { [app_delegate composeArticle: [NSDictionary dictionaryWithObject: folder_name forKey: @"Newsgroups"]]; } /* Same as in PreferencesWindowController. Let the MessageViewController act as a delegate for messages that it's interested in. */ -(BOOL) respondsToSelector: (SEL)s { if ([super respondsToSelector: s]) return YES; return [mv respondsToSelector: s]; } -(void) forwardInvocation: (NSInvocation *)i { if ([mv respondsToSelector: [i selector]]) { [i invokeWithTarget: mv]; return; } [super forwardInvocation: i]; } -(NSMethodSignature *) methodSignatureForSelector: (SEL)sel { NSMethodSignature *ms; ms=[super methodSignatureForSelector: sel]; if (ms) return ms; ms=[mv methodSignatureForSelector: sel]; return ms; } @end