AIHyperlinks universal building
[adiumx.git] / Source / ESFileTransferProgressWindowController.m
blob6b3e5698c96b71eeb627ef35b19490e550df01c8
1 /*
2  * Adium is the legal property of its developers, whose names are listed in the copyright file included
3  * with this source distribution.
4  *
5  * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6  * General Public License as published by the Free Software Foundation; either version 2 of the License,
7  * or (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10  * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
11  * Public License for more details.
12  *
13  * You should have received a copy of the GNU General Public License along with this program; if not,
14  * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
15  */
17 #import "ESFileTransferProgressRow.h"
18 #import "ESFileTransferProgressView.h"
19 #import "ESFileTransferProgressWindowController.h"
20 #import <Adium/ESFileTransfer.h>
21 #import <AIUtilities/AIVariableHeightOutlineView.h>
22 #import <AIUtilities/AIArrayAdditions.h>
23 #import <AIUtilities/BZGenericViewCell.h>
25 #define FILE_TRANSFER_PROGRESS_NIB                      @"FileTransferProgressWindow"
26 #define KEY_TRANSFER_PROGRESS_WINDOW_FRAME      @"Transfer Progress Window Frame"
28 @interface ESFileTransferProgressWindowController (PRIVATE)
29 - (void)addFileTransfer:(ESFileTransfer *)fileTransfer;
30 - (ESFileTransferProgressRow *)previousRow;
31 - (ESFileTransferProgressRow *)nextRow;
32 - (void)updateStatusBar;
33 - (void)reloadAllData;
34 - (void)_removeFileTransfer:(ESFileTransfer *)inFileTransfer;
35 - (ESFileTransferProgressRow *)existingRowForFileTransfer:(ESFileTransfer *)inFileTransfer;
36 @end
38 @interface ESFileTransferController (PRIVATE)
39 - (void)_removeFileTransfer:(ESFileTransfer *)fileTransfer;
40 @end
42 #ifndef NSAppKitVersionNumber10_3
43 #       define NSTableViewUniformColumnAutoresizingStyle 1
44 #endif
46 @implementation ESFileTransferProgressWindowController
48 static ESFileTransferProgressWindowController *sharedTransferProgressInstance = nil;
50 //Return the shared contact info window
51 #pragma mark Class Methods
52 + (id)showFileTransferProgressWindow
54     //Create the window
55     if(!sharedTransferProgressInstance){
56         sharedTransferProgressInstance = [[self alloc] initWithWindowNibName:FILE_TRANSFER_PROGRESS_NIB];
57         }
59         //Configure and show window
60         [sharedTransferProgressInstance showWindow:nil];
61         [[sharedTransferProgressInstance window] orderFront:nil];
63         return (sharedTransferProgressInstance);
66 + (id)showFileTransferProgressWindowIfNotOpen
68         //Create the window and show it if it is not already open
69     if(!sharedTransferProgressInstance){
70                 [self showFileTransferProgressWindow];
71         }
72         
73         return (sharedTransferProgressInstance);
76 //Close the info window
77 + (void)closeTransferProgressWindow
79     if(sharedTransferProgressInstance){
80         [sharedTransferProgressInstance closeWindow:nil];
81     }
84 + (void)removeFileTransfer:(ESFileTransfer *)inFileTransfer
86     if(sharedTransferProgressInstance){
87         [sharedTransferProgressInstance _removeFileTransfer:inFileTransfer];
88     }
90 //init
91 #pragma mark Basic window controller functionality
92 - (id)initWithWindowNibName:(NSString *)windowNibName
94     if((self = [super initWithWindowNibName:windowNibName])) {
95                 progressRows = [[NSMutableArray alloc] init];
96         }
97         return self;
100 - (void)dealloc
102         [[adium notificationCenter] removeObserver:self];
104         [progressRows release]; progressRows = nil;
106     [super dealloc];
110 - (NSString *)adiumFrameAutosaveName
112         return(KEY_TRANSFER_PROGRESS_WINDOW_FRAME);
115 //Setup the window before it is displayed
116 - (void)windowDidLoad
118         NSEnumerator    *enumerator;
119         ESFileTransfer  *fileTransfer;
121         //Set the localized title
122         [[self window] setTitle:AILocalizedString(@"File Transfers",nil)];
124         //There's already a menu item in the Window menu; no reason to duplicate it
125         [[self window] setExcludedFromWindowsMenu:YES];
127         //Configure the scroll view
128         [scrollView setHasVerticalScroller:YES];
129         [scrollView setHasHorizontalScroller:NO];
130         [[scrollView contentView] setCopiesOnScroll:NO];
131         if([scrollView respondsToSelector:@selector(setAutohidesScrollers:)]){
132                 [scrollView setAutohidesScrollers:YES];
133         }
135         //Configure the outline view
136         BZGenericViewCell       *cell = [[[BZGenericViewCell alloc] init] autorelease];
137         [cell setDrawsGradientHighlight:YES];
138         [[[outlineView tableColumns] objectAtIndex:0] setDataCell:cell];
140         [outlineView sizeLastColumnToFit];
141         [outlineView setAutoresizesSubviews:YES];
142         if([outlineView respondsToSelector:@selector(setColumnAutoresizingStyle:)]){
143                 [outlineView setColumnAutoresizingStyle:NSTableViewUniformColumnAutoresizingStyle];
144         }else{
145                 [outlineView setAutoresizesAllColumnsToFit:YES];
146         }
147         [outlineView setDrawsAlternatingRows:YES];
148         [outlineView setDataSource:self];
149         [outlineView setDelegate:self];
151         //Set up and size our Clear button
152         {
153                 NSRect  newFrame, oldFrame;
154                 
155                 //Clear
156                 [button_clear setAutoresizingMask:(NSViewMaxXMargin | NSViewMaxYMargin)];
158                 oldFrame = [button_clear frame];
159                 [button_clear setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
160                 [button_clear setTitle:AILocalizedString(@"Clear",nil)];
161                 [button_clear sizeToFit];
162                 newFrame = [button_clear frame];
163                 
164                 //Don't let the button get smaller than it was initially
165                 if(newFrame.size.width < oldFrame.size.width) newFrame.size.width = oldFrame.size.width;
166                 
167                 //Keep the origin and height the same - we just want to size for width
168                 newFrame.origin = oldFrame.origin;
169                 newFrame.size.height = oldFrame.size.height;
170                 [button_clear setFrame:newFrame];
171                 [button_clear setNeedsDisplay:YES];
173                 //Resize the status bar text
174                 int widthChange = oldFrame.size.width - newFrame.size.width;
175                 if(widthChange){
176                         NSRect  statusFrame;
177                         
178                         statusFrame = [textField_statusBar frame];
179                         statusFrame.origin.x += widthChange;
180                         statusFrame.size.width -= widthChange;
181                         [textField_statusBar setFrame:statusFrame];
182                         [textField_statusBar setNeedsDisplay:YES];
183                 }
184         }
185         
186         //Call super's implementation
187         [super windowDidLoad];
189         //Observe for new file transfers
190         [[adium notificationCenter] addObserver:self
191                                    selector:@selector(newFileTransfer:)
192                                        name:FileTransfer_NewFileTransfer
193                                                                          object:nil];
194         
195         //Create progress rows for all existing file transfers
196         shouldScrollToNewFileTransfer = NO;
197         enumerator = [[[adium fileTransferController] fileTransferArray] objectEnumerator];
198         while(fileTransfer = [enumerator nextObject]){
199                 [self addFileTransfer:fileTransfer];
200         }
201         
202         //Go time
203         [self reloadAllData];
204         
205         shouldScrollToNewFileTransfer = YES;
206         [outlineView scrollRectToVisible:[outlineView rectOfRow:([progressRows count]-1)]];
209 //called as the window closes
210 - (void)windowWillClose:(id)sender
212         [super windowWillClose:sender];
214         //release the window controller (ourself)
215     sharedTransferProgressInstance = nil;
216     [self autorelease];
219 - (void)configureControlDimming
220 {       
221         NSEnumerator                            *enumerator;
222         ESFileTransferProgressRow       *row;
223         BOOL                                            enableClear = NO;
224         
225         enumerator = [progressRows objectEnumerator];
226         while(row = [enumerator nextObject]){
227                 if([[row fileTransfer] isStopped]){
228                         enableClear = YES;
229                         break;
230                 }
231         }
232         
233         [button_clear setEnabled:enableClear];
236 //Called when a progress row has loaded its view and is ready to be added to our window
237 #pragma mark Progress row addition to the window
238 - (void)progressRowDidAwakeFromNib:(ESFileTransferProgressRow *)progressRow
240         if(![progressRows containsObjectIdenticalTo:progressRow]){
241                 [progressRows addObject:progressRow];
242         }
244         if(shouldScrollToNewFileTransfer){
245                 [self reloadAllData];
246                 
247                 [outlineView scrollRectToVisible:[outlineView rectOfRow:[progressRows indexOfObject:progressRow]]];
248         }
251 #pragma mark Progress row details twiddle
252 //Called when the file transfer view's twiddle is clicked.
253 - (void)fileTransferProgressRow:(ESFileTransferProgressRow *)progressRow
254                           heightChangedFrom:(float)oldHeight
255                                                          to:(float)newHeight
257         if(shouldScrollToNewFileTransfer){
258                 [self reloadAllData];
259                 
260                 [outlineView scrollRectToVisible:[outlineView rectOfRow:[progressRows indexOfObject:progressRow]]];
261         }
264 #pragma mark Adding file transfers
265 //Notification of a new file transfer; add it to the window
266 - (void)newFileTransfer:(NSNotification *)notification
268         ESFileTransfer  *fileTransfer;
270         if(fileTransfer = [notification object]){
271                 [self addFileTransfer:fileTransfer];
272         }
275 //Add a file transfer's progress row if we don't already have one for the fileTransfer.
276 //This will call back on progressRowDidAwakeFromNib: if it adds a new row.
277 - (void)addFileTransfer:(ESFileTransfer *)inFileTransfer
279         if(![self existingRowForFileTransfer:inFileTransfer]){
280                 [ESFileTransferProgressRow rowForFileTransfer:inFileTransfer withOwner:self];
281         }
284 - (void)_removeFileTransfer:(ESFileTransfer *)inFileTransfer
286         ESFileTransferProgressRow       *row;
288         if(row = [self existingRowForFileTransfer:inFileTransfer]) [self _removeFileTransferRow:row];
291 - (ESFileTransferProgressRow *)existingRowForFileTransfer:(ESFileTransfer *)inFileTransfer
293         NSEnumerator                            *enumerator;
294         ESFileTransferProgressRow       *row;
296         enumerator = [progressRows objectEnumerator];
297         while(row = [enumerator nextObject]){
298                 if([row fileTransfer] == inFileTransfer) break;
299         }
301         return(row);
304 //Remove a file transfer row from the window. This is coupled to the file transfer controller; care must be taken
305 //that we don't remove a row which is in progress, as this will remove the file transfer controller's tracking of it.
306 //This must be done so we don't see the file transfer again if the progress window is closed and then reopened.
307 - (void)_removeFileTransferRow:(ESFileTransferProgressRow *)progressRow
309         ESFileTransfer  *fileTransfer = [progressRow fileTransfer];
311         if([fileTransfer isStopped]){
312                 NSClipView              *clipView = [scrollView contentView];
313                 unsigned                row;
315                 //Protect
316                 [progressRow retain];
318                 //Remove the row from our array, and its file transfer from the fileTransferController
319                 row = [progressRows indexOfObject:progressRow];
320                 [progressRows removeObject:progressRow];
321                 [[adium fileTransferController] _removeFileTransfer:fileTransfer];
322                 
323                 if(shouldScrollToNewFileTransfer){
324                         //Refresh the outline view
325                         [self reloadAllData];
326                         
327                         //Determine the row to reselect.  If the current row is valid, keep it.  If it isn't, use the last row.
328                         if(row >= [progressRows count]){
329                                 row = [progressRows count] - 1;
330                         }
331                         [clipView scrollToPoint:[clipView constrainScrollPoint:([outlineView rectOfRow:row].origin)]];
332                         
333                         [self updateStatusBar];
334                 }
335                 
336                 //Clean up
337                 [progressRow release];
338         }
341 #pragma mark Status bar
342 //Called when a progress row changes its type, typically from Unknown to either Incoming or Outgoing
343 - (void)progressRowDidChangeType:(ESFileTransferProgressRow *)progressRow
345         /* We get here as a progress row intializes itself, before it claims to be ready for display and therefore before
346          * we have it in the progressRows array.  Add it now if necessary */
347         if(![progressRows containsObjectIdenticalTo:progressRow]){
348                 [progressRows addObject:progressRow];
349         }
350         
351         [self updateStatusBar];
354 - (void)progressRowDidChangeStatus:(ESFileTransferProgressRow *)progressRow
356         [self configureControlDimming];
359 //Update the status bar at the bottom of the window
360 - (void)updateStatusBar
362         NSEnumerator                            *enumerator;
363         ESFileTransferProgressRow       *aRow;
364         NSString                                        *statusBarString, *downloadsString = nil, *uploadsString = nil;
365         unsigned                                        downloads = 0, uploads = 0;
366         
367         enumerator = [progressRows objectEnumerator];
368         while(aRow = [enumerator nextObject]){
369                 FileTransferType type = [aRow type];
370                 if(type == Incoming_FileTransfer){
371                         downloads++;
372                 }else if(type == Outgoing_FileTransfer){
373                         uploads++;
374                 }
375         }
377         if(downloads > 0){
378                 if(downloads == 1)
379                         downloadsString = AILocalizedString(@"1 download",nil);
380                 else
381                         downloadsString = [NSString stringWithFormat:AILocalizedString(@"%i downloads","(number) downloads"), downloads];
382         }
384         if(uploads > 0){
385                 if(uploads == 1)
386                         uploadsString = AILocalizedString(@"1 upload",nil);
387                 else
388                         uploadsString = [NSString stringWithFormat:AILocalizedString(@"%i uploads","(number) uploads"), uploads];
389         }
391         if(downloadsString && uploadsString){
392                 statusBarString = [NSString stringWithFormat:@"%@; %@",downloadsString,uploadsString];
393         }else if(downloadsString){
394                 statusBarString = downloadsString;
395         }else if(uploadsString){
396                 statusBarString = uploadsString;
397         }else{
398                 statusBarString = @"";
399         }
401         [textField_statusBar setStringValue:statusBarString];
404 - (IBAction)clearAllCompleteTransfers:(id)sender
406         NSEnumerator                            *enumerator;
407         ESFileTransferProgressRow       *row;
408         
409         shouldScrollToNewFileTransfer = NO;
410         enumerator = [progressRows objectEnumerator];
411         while(row = [enumerator nextObject]){
412                 if([[row fileTransfer] isStopped]) [self _removeFileTransferRow:row];
413         }       
414         shouldScrollToNewFileTransfer = YES;
415         
416         [self reloadAllData];
418         [outlineView scrollRectToVisible:[outlineView rectOfRow:0]];    
421 #pragma mark OutlineView dataSource
422 - (id)outlineView:(NSOutlineView *)inOutlineView child:(int)index ofItem:(id)item
424         if(index < [progressRows count]) {
425                 return([progressRows objectAtIndex:index]);
426         } else {
427                 return nil;
428         }
431 - (int)outlineView:(NSOutlineView *)inOutlineView numberOfChildrenOfItem:(id)item
433         return([progressRows count]);
436 //No items are expandable for the outline view
437 - (BOOL)outlineView:(NSOutlineView *)inOutlineView isItemExpandable:(id)item
439         return(NO);
442 //We don't use object values
443 - (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item
445         return(@"");
448 //Each row should be the height of its item's view
449 - (int)outlineView:(NSOutlineView *)inOutlineView heightForItem:(id)item atRow:(int)row
451         return([[(ESFileTransferProgressRow *)item view] frame].size.height);
454 //Before a cell is display, set its embedded view
455 - (void)outlineView:(NSOutlineView *)inOutlineView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item
457         [cell setEmbeddedView:[(ESFileTransferProgressRow *)item view]];
460 #pragma mark Outline view delegate
461 - (void)outlineViewDeleteSelectedRows:(NSOutlineView *)inOutlineView
463         int             row = [inOutlineView selectedRow];
464         BOOL    didDelete = NO;
465         if(row != -1){
466                 ESFileTransferProgressRow       *progressRow = [inOutlineView itemAtRow:row];
467                 if([[progressRow fileTransfer] isStopped]){
468                         [self _removeFileTransferRow:progressRow];
469                         didDelete = YES;
470                 }
471         }
473         //If they tried to delete a row that isn't finished, or we got here with no valid selection, sound the system beep
474         if(!didDelete)
475                 NSBeep();
478 - (NSMenu *)outlineView:(NSOutlineView *)inOutlineView menuForEvent:(NSEvent *)inEvent
480         NSMenu  *menu = nil;
481     NSPoint     location;
482     int         row;
484     //Get the clicked item
485     location = [inOutlineView convertPoint:[inEvent locationInWindow]
486                                                                   fromView:[[inOutlineView window] contentView]];
487     row = [inOutlineView rowAtPoint:location];
489         if(row != -1){
490                 ESFileTransferProgressRow       *progressRow = [inOutlineView itemAtRow:row];
491                 menu = [progressRow menuForEvent:inEvent];
492         }
494         return(menu);
498  * @brief Reload all data
500  * After removing the subviews of the outline view, reload the data.
501  * Next, ensure the height of the outline view is still correct.
502  * Finally, update our display and associated controls.
503  */
504 - (void)reloadAllData
506         [[[[outlineView subviews] copy] autorelease] makeObjectsPerformSelector:@selector(removeFromSuperview)];
507         [outlineView reloadData];
509         NSRect  outlineFrame = [outlineView frame];
510         int             totalHeight = [outlineView totalHeight];
512         if(outlineFrame.size.height != totalHeight){
513                 outlineFrame.size.height = totalHeight;
514                 [outlineView setFrame:outlineFrame];
515                 [outlineView setNeedsDisplay:YES];
516         }
518         //Update our status bar
519         [self updateStatusBar];
521         //Enable/disable our controls
522         [self configureControlDimming];
525 #pragma mark Window zoom
526 //Size for window zoom
527 - (NSRect)windowWillUseStandardFrame:(NSWindow *)inWindow defaultFrame:(NSRect)defaultFrame
529         NSRect  oldWindowFrame = [inWindow frame];
530         NSRect  windowFrame = oldWindowFrame;
531         NSSize  minSize = [inWindow minSize];
532         NSSize  maxSize = [inWindow maxSize];
534         //Take the desired height and add the parts of the window which aren't in the scrollView.
535         int desiredHeight = ([outlineView totalHeight] + (windowFrame.size.height - [scrollView frame].size.height));
537         windowFrame.size.height = desiredHeight;
538         windowFrame.size.width = 300;
540         //Respect the min and max sizes
541         if(windowFrame.size.width < minSize.width) windowFrame.size.width = minSize.width;
542         if(windowFrame.size.height < minSize.height) windowFrame.size.height = minSize.height;
543         if(windowFrame.size.width > maxSize.width) windowFrame.size.width = maxSize.width;
544         if(windowFrame.size.height > maxSize.height) windowFrame.size.height = maxSize.height;
546         //Keep the top-left corner the same
547         windowFrame.origin.y = oldWindowFrame.origin.y + oldWindowFrame.size.height - windowFrame.size.height;
549     return(windowFrame);
552 @end