Added `-[NSArray validateAsPropertyList]` and `-[NSDictionary validateAsPropertyList...
[adiumx.git] / Source / ESFileTransferProgressWindowController.m
blobd9511d17cd73fd7ef4c19d09549056823522d46a
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 "ESFileTransfer.h"
21 #import <AIUtilities/AIVariableHeightOutlineView.h>
22 #import <AIUtilities/AIArrayAdditions.h>
23 #import <AIUtilities/AIGenericViewCell.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)sharedTransferProgressWindowController
54         //Create the window
55     if (!sharedTransferProgressInstance) {
56         sharedTransferProgressInstance = [[self alloc] initWithWindowNibName:FILE_TRANSFER_PROGRESS_NIB];
57         }
59         return sharedTransferProgressInstance;
62 + (id)showFileTransferProgressWindow
64         //Configure and show window
65         [[self sharedTransferProgressWindowController] showWindow:nil];
67         return (sharedTransferProgressInstance);
70 + (id)showFileTransferProgressWindowIfNotOpen
72         [[[self sharedTransferProgressWindowController] window] orderFront:nil];
73         
74         return (sharedTransferProgressInstance);
77 //Close the info window
78 + (void)closeTransferProgressWindow
80     if (sharedTransferProgressInstance) {
81         [sharedTransferProgressInstance closeWindow:nil];
82     }
85 + (void)removeFileTransfer:(ESFileTransfer *)inFileTransfer
87     if (sharedTransferProgressInstance) {
88         [sharedTransferProgressInstance _removeFileTransfer:inFileTransfer];
89     }
91 //init
92 #pragma mark Basic window controller functionality
93 - (id)initWithWindowNibName:(NSString *)windowNibName
95     if ((self = [super initWithWindowNibName:windowNibName])) {
96                 progressRows = [[NSMutableArray alloc] init];
97         }
98         return self;
101 - (void)dealloc
103         [[adium notificationCenter] removeObserver:self];
105         [progressRows release]; progressRows = nil;
107     [super dealloc];
111 - (NSString *)adiumFrameAutosaveName
113         return KEY_TRANSFER_PROGRESS_WINDOW_FRAME;
116 //Setup the window before it is displayed
117 - (void)windowDidLoad
119         NSEnumerator    *enumerator;
120         ESFileTransfer  *fileTransfer;
122         //Set the localized title
123         [[self window] setTitle:AILocalizedString(@"File Transfers",nil)];
125         //There's already a menu item in the Window menu; no reason to duplicate it
126         [[self window] setExcludedFromWindowsMenu:YES];
128         //Configure the scroll view
129         [scrollView setHasVerticalScroller:YES];
130         [scrollView setHasHorizontalScroller:NO];
131         [[scrollView contentView] setCopiesOnScroll:NO];
132         if ([scrollView respondsToSelector:@selector(setAutohidesScrollers:)]) {
133                 [scrollView setAutohidesScrollers:YES];
134         }
136         //Configure the outline view
137         [outlineView setDrawsGradientSelection:YES];
138         [[[outlineView tableColumns] objectAtIndex:0] setDataCell:[[[AIGenericViewCell alloc] init] autorelease]];
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         [outlineView accessibilitySetOverrideValue:AILocalizedString(@"File Transfers", nil)
187                                                                   forAttribute:NSAccessibilityDescriptionAttribute];
189         //Call super's implementation
190         [super windowDidLoad];
192         //Observe for new file transfers
193         [[adium notificationCenter] addObserver:self
194                                    selector:@selector(newFileTransfer:)
195                                        name:FileTransfer_NewFileTransfer
196                                                                          object:nil];
197         
198         //Create progress rows for all existing file transfers
199         shouldScrollToNewFileTransfer = NO;
200         enumerator = [[[adium fileTransferController] fileTransferArray] objectEnumerator];
201         while ((fileTransfer = [enumerator nextObject])) {
202                 [self addFileTransfer:fileTransfer];
203         }
204         
205         //Go time
206         [self reloadAllData];
207         
208         shouldScrollToNewFileTransfer = YES;
209         [outlineView scrollRectToVisible:[outlineView rectOfRow:([progressRows count]-1)]];
212 //called as the window closes
213 - (void)windowWillClose:(id)sender
215         [super windowWillClose:sender];
217         //release the window controller (ourself)
218     sharedTransferProgressInstance = nil;
219     [self autorelease];
222 - (void)configureControlDimming
223 {       
224         NSEnumerator                            *enumerator;
225         ESFileTransferProgressRow       *row;
226         BOOL                                            enableClear = NO;
227         
228         enumerator = [progressRows objectEnumerator];
229         while ((row = [enumerator nextObject])) {
230                 if ([[row fileTransfer] isStopped]) {
231                         enableClear = YES;
232                         break;
233                 }
234         }
235         
236         [button_clear setEnabled:enableClear];
239 //Called when a progress row has loaded its view and is ready to be added to our window
240 #pragma mark Progress row addition to the window
241 - (void)progressRowDidAwakeFromNib:(ESFileTransferProgressRow *)progressRow
243         if (![progressRows containsObjectIdenticalTo:progressRow]) {
244                 [progressRows addObject:progressRow];
245         }
247         if (shouldScrollToNewFileTransfer) {
248                 [self reloadAllData];
249                 
250                 [outlineView scrollRectToVisible:[outlineView rectOfRow:[progressRows indexOfObject:progressRow]]];
251         }
254 #pragma mark Progress row details twiddle
255 //Called when the file transfer view's twiddle is clicked.
256 - (void)fileTransferProgressRow:(ESFileTransferProgressRow *)progressRow
257                           heightChangedFrom:(float)oldHeight
258                                                          to:(float)newHeight
260         if (shouldScrollToNewFileTransfer) {
261                 [self reloadAllData];
262                 
263                 [outlineView scrollRectToVisible:[outlineView rectOfRow:[progressRows indexOfObject:progressRow]]];
264         }
267 #pragma mark Adding file transfers
268 //Notification of a new file transfer; add it to the window
269 - (void)newFileTransfer:(NSNotification *)notification
271         ESFileTransfer  *fileTransfer;
273         if ((fileTransfer = [notification object])) {
274                 [self addFileTransfer:fileTransfer];
275         }
278 //Add a file transfer's progress row if we don't already have one for the fileTransfer.
279 //This will call back on progressRowDidAwakeFromNib: if it adds a new row.
280 - (void)addFileTransfer:(ESFileTransfer *)inFileTransfer
282         if (![self existingRowForFileTransfer:inFileTransfer]) {
283                 [ESFileTransferProgressRow rowForFileTransfer:inFileTransfer withOwner:self];
284         }
287 - (void)_removeFileTransfer:(ESFileTransfer *)inFileTransfer
289         ESFileTransferProgressRow       *row;
291         if ((row = [self existingRowForFileTransfer:inFileTransfer])) [self _removeFileTransferRow:row];
294 - (ESFileTransferProgressRow *)existingRowForFileTransfer:(ESFileTransfer *)inFileTransfer
296         NSEnumerator                            *enumerator;
297         ESFileTransferProgressRow       *row;
299         enumerator = [progressRows objectEnumerator];
300         while ((row = [enumerator nextObject])) {
301                 if ([row fileTransfer] == inFileTransfer) break;
302         }
304         return row;
307 //Remove a file transfer row from the window. This is coupled to the file transfer controller; care must be taken
308 //that we don't remove a row which is in progress, as this will remove the file transfer controller's tracking of it.
309 //This must be done so we don't see the file transfer again if the progress window is closed and then reopened.
310 - (void)_removeFileTransferRow:(ESFileTransferProgressRow *)progressRow
312         ESFileTransfer  *fileTransfer = [progressRow fileTransfer];
314         if ([fileTransfer isStopped]) {
315                 NSClipView              *clipView = [scrollView contentView];
316                 unsigned                row;
318                 //Protect
319                 [progressRow retain];
321                 //Remove the row from our array, and its file transfer from the fileTransferController
322                 row = [progressRows indexOfObject:progressRow];
323                 [progressRows removeObject:progressRow];
324                 [[adium fileTransferController] _removeFileTransfer:fileTransfer];
325                 
326                 if (shouldScrollToNewFileTransfer) {
327                         //Refresh the outline view
328                         [self reloadAllData];
329                         
330                         //Determine the row to reselect.  If the current row is valid, keep it.  If it isn't, use the last row.
331                         if (row >= [progressRows count]) {
332                                 row = [progressRows count] - 1;
333                         }
334                         [clipView scrollToPoint:[clipView constrainScrollPoint:([outlineView rectOfRow:row].origin)]];
335                         
336                         [self updateStatusBar];
337                 }
338                 
339                 //Clean up
340                 [progressRow release];
341         }
344 #pragma mark Status bar
345 //Called when a progress row changes its type, typically from Unknown to either Incoming or Outgoing
346 - (void)progressRowDidChangeType:(ESFileTransferProgressRow *)progressRow
348         /* We get here as a progress row intializes itself, before it claims to be ready for display and therefore before
349          * we have it in the progressRows array.  Add it now if necessary */
350         if (![progressRows containsObjectIdenticalTo:progressRow]) {
351                 [progressRows addObject:progressRow];
352         }
353         
354         [self updateStatusBar];
357 - (void)progressRowDidChangeStatus:(ESFileTransferProgressRow *)progressRow
359         [self configureControlDimming];
362 //Update the status bar at the bottom of the window
363 - (void)updateStatusBar
365         NSEnumerator                            *enumerator;
366         ESFileTransferProgressRow       *aRow;
367         NSString                                        *statusBarString, *downloadsString = nil, *uploadsString = nil;
368         unsigned                                        downloads = 0, uploads = 0;
369         
370         enumerator = [progressRows objectEnumerator];
371         while ((aRow = [enumerator nextObject])) {
372                 AIFileTransferType type = [aRow type];
373                 if (type == Incoming_FileTransfer) {
374                         downloads++;
375                 } else if (type == Outgoing_FileTransfer) {
376                         uploads++;
377                 }
378         }
380         if (downloads > 0) {
381                 if (downloads == 1)
382                         downloadsString = AILocalizedString(@"1 download",nil);
383                 else
384                         downloadsString = [NSString stringWithFormat:AILocalizedString(@"%i downloads","(number) downloads"), downloads];
385         }
387         if (uploads > 0) {
388                 if (uploads == 1)
389                         uploadsString = AILocalizedString(@"1 upload",nil);
390                 else
391                         uploadsString = [NSString stringWithFormat:AILocalizedString(@"%i uploads","(number) uploads"), uploads];
392         }
394         if (downloadsString && uploadsString) {
395                 statusBarString = [NSString stringWithFormat:@"%@; %@",downloadsString,uploadsString];
396         } else if (downloadsString) {
397                 statusBarString = downloadsString;
398         } else if (uploadsString) {
399                 statusBarString = uploadsString;
400         } else {
401                 statusBarString = @"";
402         }
404         [textField_statusBar setStringValue:statusBarString];
407 - (IBAction)clearAllCompleteTransfers:(id)sender
409         NSEnumerator                            *enumerator;
410         ESFileTransferProgressRow       *row;
411         
412         shouldScrollToNewFileTransfer = NO;
413         enumerator = [[[progressRows copy] autorelease] objectEnumerator];
414         while ((row = [enumerator nextObject])) {
415                 if ([[row fileTransfer] isStopped]) [self _removeFileTransferRow:row];
416         }       
417         shouldScrollToNewFileTransfer = YES;
418         
419         [self reloadAllData];
421         [outlineView scrollRectToVisible:[outlineView rectOfRow:0]];    
424 #pragma mark OutlineView dataSource
425 - (id)outlineView:(NSOutlineView *)inOutlineView child:(int)index ofItem:(id)item
427         if (index < [progressRows count]) {
428                 return [progressRows objectAtIndex:index];
429         } else {
430                 return nil;
431         }
434 - (int)outlineView:(NSOutlineView *)inOutlineView numberOfChildrenOfItem:(id)item
436         return [progressRows count];
439 //No items are expandable for the outline view
440 - (BOOL)outlineView:(NSOutlineView *)inOutlineView isItemExpandable:(id)item
442         return NO;
445 //We don't use object values
446 - (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item
448         return @"";
451 //Each row should be the height of its item's view
452 - (float)outlineView:(NSOutlineView *)outlineView heightOfRowByItem:(id)item
454         NSView *view = [(ESFileTransferProgressRow *)item view];
455         
456         return (view ? [view frame].size.height : 0);
459 //Before a cell is display, set its embedded view
460 - (void)outlineView:(NSOutlineView *)inOutlineView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item
462         [cell setEmbeddedView:[(ESFileTransferProgressRow *)item view]];
465 #pragma mark Outline view delegate
466 - (void)outlineViewDeleteSelectedRows:(NSOutlineView *)inOutlineView
468         int             row = [inOutlineView selectedRow];
469         BOOL    didDelete = NO;
470         if (row != -1) {
471                 ESFileTransferProgressRow       *progressRow = [inOutlineView itemAtRow:row];
472                 if ([[progressRow fileTransfer] isStopped]) {
473                         [self _removeFileTransferRow:progressRow];
474                         didDelete = YES;
475                 }
476         }
478         //If they tried to delete a row that isn't finished, or we got here with no valid selection, sound the system beep
479         if (!didDelete)
480                 NSBeep();
483 - (NSMenu *)outlineView:(NSOutlineView *)inOutlineView menuForEvent:(NSEvent *)inEvent
485         NSMenu  *menu = nil;
486     NSPoint     location;
487     int         row;
489     //Get the clicked item
490     location = [inOutlineView convertPoint:[inEvent locationInWindow]
491                                                                   fromView:nil];
492     row = [inOutlineView rowAtPoint:location];
494         if (row != -1) {
495                 ESFileTransferProgressRow       *progressRow = [inOutlineView itemAtRow:row];
496                 menu = [progressRow menuForEvent:inEvent];
497         }
499         return menu;
503  * @brief Reload all data
505  * After removing the subviews of the outline view, reload the data.
506  * Next, ensure the height of the outline view is still correct.
507  * Finally, update our display and associated controls.
508  */
509 - (void)reloadAllData
511         [[[[outlineView subviews] copy] autorelease] makeObjectsPerformSelector:@selector(removeFromSuperview)];
512         [outlineView reloadData];
514         NSRect  outlineFrame = [outlineView frame];
515         int             totalHeight = [outlineView totalHeight];
517         if (outlineFrame.size.height != totalHeight) {
518                 outlineFrame.size.height = totalHeight;
519                 [outlineView setFrame:outlineFrame];
520                 [outlineView setNeedsDisplay:YES];
521         }
523         //Update our status bar
524         [self updateStatusBar];
526         //Enable/disable our controls
527         [self configureControlDimming];
530 #pragma mark Window zoom
531 //Size for window zoom
532 - (NSRect)windowWillUseStandardFrame:(NSWindow *)inWindow defaultFrame:(NSRect)defaultFrame
534         NSRect  oldWindowFrame = [inWindow frame];
535         NSRect  windowFrame = oldWindowFrame;
536         NSSize  minSize = [inWindow minSize];
537         NSSize  maxSize = [inWindow maxSize];
539         //Take the desired height and add the parts of the window which aren't in the scrollView.
540         int desiredHeight = ([outlineView totalHeight] + (windowFrame.size.height - [scrollView frame].size.height));
542         windowFrame.size.height = desiredHeight;
543         windowFrame.size.width = 300;
545         //Respect the min and max sizes
546         if (windowFrame.size.width < minSize.width) windowFrame.size.width = minSize.width;
547         if (windowFrame.size.height < minSize.height) windowFrame.size.height = minSize.height;
548         if (windowFrame.size.width > maxSize.width) windowFrame.size.width = maxSize.width;
549         if (windowFrame.size.height > maxSize.height) windowFrame.size.height = maxSize.height;
551         //Keep the top-left corner the same
552         windowFrame.origin.y = oldWindowFrame.origin.y + oldWindowFrame.size.height - windowFrame.size.height;
554     return windowFrame;
557 @end