2 * Adium is the legal property of its developers, whose names are listed in the copyright file included
3 * with this source distribution.
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.
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.
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.
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;
38 @interface ESFileTransferController (PRIVATE)
39 - (void)_removeFileTransfer:(ESFileTransfer *)fileTransfer;
42 #ifndef NSAppKitVersionNumber10_3
43 # define NSTableViewUniformColumnAutoresizingStyle 1
46 @implementation ESFileTransferProgressWindowController
48 static ESFileTransferProgressWindowController *sharedTransferProgressInstance = nil;
50 //Return the shared contact info window
51 #pragma mark Class Methods
52 + (id)sharedTransferProgressWindowController
55 if (!sharedTransferProgressInstance) {
56 sharedTransferProgressInstance = [[self alloc] initWithWindowNibName:FILE_TRANSFER_PROGRESS_NIB];
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];
74 return (sharedTransferProgressInstance);
77 //Close the info window
78 + (void)closeTransferProgressWindow
80 if (sharedTransferProgressInstance) {
81 [sharedTransferProgressInstance closeWindow:nil];
85 + (void)removeFileTransfer:(ESFileTransfer *)inFileTransfer
87 if (sharedTransferProgressInstance) {
88 [sharedTransferProgressInstance _removeFileTransfer:inFileTransfer];
92 #pragma mark Basic window controller functionality
93 - (id)initWithWindowNibName:(NSString *)windowNibName
95 if ((self = [super initWithWindowNibName:windowNibName])) {
96 progressRows = [[NSMutableArray alloc] init];
103 [[adium notificationCenter] removeObserver:self];
105 [progressRows release]; progressRows = nil;
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];
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];
145 [outlineView setAutoresizesAllColumnsToFit:YES];
147 [outlineView setDrawsAlternatingRows:YES];
148 [outlineView setDataSource:self];
149 [outlineView setDelegate:self];
151 //Set up and size our Clear button
153 NSRect newFrame, oldFrame;
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];
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;
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;
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];
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
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];
203 [self reloadAllData];
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;
219 - (void)configureControlDimming
221 NSEnumerator *enumerator;
222 ESFileTransferProgressRow *row;
223 BOOL enableClear = NO;
225 enumerator = [progressRows objectEnumerator];
226 while ((row = [enumerator nextObject])) {
227 if ([[row fileTransfer] isStopped]) {
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];
244 if (shouldScrollToNewFileTransfer) {
245 [self reloadAllData];
247 [outlineView scrollRectToVisible:[outlineView rectOfRow:[progressRows indexOfObject:progressRow]]];
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
257 if (shouldScrollToNewFileTransfer) {
258 [self reloadAllData];
260 [outlineView scrollRectToVisible:[outlineView rectOfRow:[progressRows indexOfObject:progressRow]]];
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];
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];
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;
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];
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];
323 if (shouldScrollToNewFileTransfer) {
324 //Refresh the outline view
325 [self reloadAllData];
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;
331 [clipView scrollToPoint:[clipView constrainScrollPoint:([outlineView rectOfRow:row].origin)]];
333 [self updateStatusBar];
337 [progressRow release];
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];
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;
367 enumerator = [progressRows objectEnumerator];
368 while ((aRow = [enumerator nextObject])) {
369 AIFileTransferType type = [aRow type];
370 if (type == Incoming_FileTransfer) {
372 } else if (type == Outgoing_FileTransfer) {
379 downloadsString = AILocalizedString(@"1 download",nil);
381 downloadsString = [NSString stringWithFormat:AILocalizedString(@"%i downloads","(number) downloads"), downloads];
386 uploadsString = AILocalizedString(@"1 upload",nil);
388 uploadsString = [NSString stringWithFormat:AILocalizedString(@"%i uploads","(number) uploads"), uploads];
391 if (downloadsString && uploadsString) {
392 statusBarString = [NSString stringWithFormat:@"%@; %@",downloadsString,uploadsString];
393 } else if (downloadsString) {
394 statusBarString = downloadsString;
395 } else if (uploadsString) {
396 statusBarString = uploadsString;
398 statusBarString = @"";
401 [textField_statusBar setStringValue:statusBarString];
404 - (IBAction)clearAllCompleteTransfers:(id)sender
406 NSEnumerator *enumerator;
407 ESFileTransferProgressRow *row;
409 shouldScrollToNewFileTransfer = NO;
410 enumerator = [progressRows objectEnumerator];
411 while ((row = [enumerator nextObject])) {
412 if ([[row fileTransfer] isStopped]) [self _removeFileTransferRow:row];
414 shouldScrollToNewFileTransfer = YES;
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];
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
442 //We don't use object values
443 - (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item
448 //Each row should be the height of its item's view
449 - (float)outlineView:(NSOutlineView *)outlineView heightOfRowByItem:(id)item
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];
466 ESFileTransferProgressRow *progressRow = [inOutlineView itemAtRow:row];
467 if ([[progressRow fileTransfer] isStopped]) {
468 [self _removeFileTransferRow:progressRow];
473 //If they tried to delete a row that isn't finished, or we got here with no valid selection, sound the system beep
478 - (NSMenu *)outlineView:(NSOutlineView *)inOutlineView menuForEvent:(NSEvent *)inEvent
484 //Get the clicked item
485 location = [inOutlineView convertPoint:[inEvent locationInWindow]
487 row = [inOutlineView rowAtPoint:location];
490 ESFileTransferProgressRow *progressRow = [inOutlineView itemAtRow:row];
491 menu = [progressRow menuForEvent:inEvent];
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.
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];
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;