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 <Adium/AIListObject.h>
21 #import <Adium/AIUserIcons.h>
22 #import "ESFileTransfer.h"
23 #import <AIUtilities/AIMenuAdditions.h>
24 #import <AIUtilities/AIStringAdditions.h>
26 #define BYTES_RECEIVED [NSString stringWithFormat:AILocalizedString(@"%@ received","%@ will be replaced by a string such as '5 MB' in the file transfer window"),bytesString]
27 #define BYTES_SENT [NSString stringWithFormat:AILocalizedString(@"%@ sent","%@ will be replaced by a string such as '5 MB' in the file transfer window"),bytesString]
28 #define BUFFER_SIZE 25
30 @interface ESFileTransferProgressRow (PRIVATE)
31 - (NSString *)stringForSize:(unsigned long long)size;
32 - (NSString *)stringForSize:(unsigned long long)inSize of:(unsigned long long)totalSize ofString:(NSString *)totalSizeString;
33 - (NSString *)readableTimeForSecs:(NSTimeInterval)secs inLongFormat:(BOOL)longFormat;
34 - (id)initForFileTransfer:(ESFileTransfer *)inFileTransfer withOwner:(id)owner;
35 - (void)updateIconImage;
36 - (void)updateSourceAndDestination;
39 @implementation ESFileTransferProgressRow
41 + (ESFileTransferProgressRow *)rowForFileTransfer:(ESFileTransfer *)inFileTransfer withOwner:(id)inOwner
43 return [[[ESFileTransferProgressRow alloc] initForFileTransfer:inFileTransfer withOwner:inOwner] autorelease];
46 - (id)initForFileTransfer:(ESFileTransfer *)inFileTransfer withOwner:(id)inOwner
48 if ((self = [super init])) {
52 fileTransfer = [inFileTransfer retain];
53 [fileTransfer setDelegate:self];
57 bytesSentQueue = [[NSMutableArray alloc] init];
58 updateTickQueue = [[NSMutableArray alloc] init];
60 [NSBundle loadNibNamed:@"ESFileTransferProgressView" owner:self];
69 [fileTransfer setDelegate:nil];
70 [fileTransfer release];
71 [view release]; view = nil;
72 [sizeString release]; sizeString = nil;
74 [bytesSentQueue release]; bytesSentQueue = nil;
75 [updateTickQueue release]; updateTickQueue = nil;
80 - (ESFileTransfer *)fileTransfer
85 - (ESFileTransferProgressView *)view
92 //If we already know something about this file transfer, update since we missed delegate calls
93 [self fileTransfer:fileTransfer didSetSize:[fileTransfer size]];
94 [self fileTransfer:fileTransfer didSetLocalFilename:[fileTransfer localFilename]];
95 [self fileTransfer:fileTransfer didSetType:[fileTransfer fileTransferType]];
97 //This always calls gotUpdate and display, so do it last
98 [self fileTransfer:fileTransfer didSetStatus:[fileTransfer status]];
100 //Once we've set up some basic information, tell our owner it can add the view
101 [owner progressRowDidAwakeFromNib:self];
103 [self performSelector:@selector(informOfAwakefromNib)
105 afterDelay:0.000001];
109 - (void)informOfAwakefromNib
111 //Once we've set up some basic information, tell our owner it can add the view
112 [owner progressRowDidAwakeFromNib:self];
116 - (void)fileTransfer:(ESFileTransfer *)inFileTransfer didSetType:(AIFileTransferType)type
118 [self updateSourceAndDestination];
119 [self updateIconImage];
121 [owner progressRowDidChangeType:self];
124 - (AIFileTransferType)type
126 return [fileTransfer fileTransferType];
129 - (void)fileTransfer:(ESFileTransfer *)inFileTransfer didSetSize:(unsigned long long)inSize
133 [sizeString release];
134 sizeString = [[[adium fileTransferController] stringForSize:size] retain];
137 - (void)fileTransfer:(ESFileTransfer *)inFileTransfer didSetLocalFilename:(NSString *)inLocalFilename
139 NSString *filename = [inLocalFilename lastPathComponent];
141 //If we don't have a local file name, try to use the remote file name.
142 if (!filename) filename = [[inFileTransfer remoteFilename] lastPathComponent];
144 [view setFileName:filename];
146 [self updateIconImage];
149 - (void)fileTransfer:(ESFileTransfer *)inFileTransfer didSetStatus:(AIFileTransferStatus)inStatus
152 [self gotUpdateForFileTransfer:inFileTransfer];
154 [view setAllowsCancel:![inFileTransfer isStopped]];
155 [owner progressRowDidChangeStatus:self];
157 [[view window] display];
161 //Handle progress, bytes transferred/bytes total, rate, and time remaining
162 - (void)gotUpdateForFileTransfer:(ESFileTransfer *)inFileTransfer
164 UInt32 updateTick = TickCount();
165 AIFileTransferStatus status = [inFileTransfer status];
167 //Don't update continously; on a LAN transfer, for instance, we'll get almost constant updates
168 if (lastUpdateTick && (((updateTick - lastUpdateTick) / 60.0) < 0.2) && (status == In_Progress_FileTransfer) && !forceUpdate) {
172 unsigned long long bytesSent = [inFileTransfer bytesSent];
173 NSString *transferBytesStatus = nil, *transferSpeedStatus = nil, *transferRemainingStatus = nil;
174 AIFileTransferType type = [inFileTransfer fileTransferType];
177 size = [inFileTransfer size];
179 [sizeString release];
180 sizeString = [[[adium fileTransferController] stringForSize:size] retain];
184 case Unknown_Status_FileTransfer:
185 case Not_Started_FileTransfer:
186 case Accepted_FileTransfer:
187 case Waiting_on_Remote_User_FileTransfer:
188 case Connecting_FileTransfer:
189 [view setProgressIndeterminate:YES];
190 [view setProgressAnimation:YES];
191 transferSpeedStatus = AILocalizedString(@"Waiting to start.","waiting to begin a file transfer status");
193 case Checksumming_Filetransfer:
194 [view setProgressIndeterminate:YES];
195 [view setProgressAnimation:YES];
196 transferSpeedStatus = [AILocalizedString(@"Preparing file","waiting to begin a file transfer status") stringByAppendingEllipsis];
198 case In_Progress_FileTransfer:
199 [view setProgressIndeterminate:NO];
200 [view setProgressDoubleValue:[inFileTransfer percentDone]];
202 case Complete_FileTransfer:
203 [view setProgressVisible:NO];
204 transferSpeedStatus = AILocalizedString(@"Complete",nil);
206 case Cancelled_Local_FileTransfer:
207 case Cancelled_Remote_FileTransfer:
208 [view setProgressVisible:NO];
209 transferSpeedStatus = AILocalizedString(@"Stopped",nil);
211 case Failed_FileTransfer:
212 [view setProgressVisible:NO];
213 transferSpeedStatus = AILocalizedString(@"Failed",nil);
217 if (type == Unknown_FileTransfer) {
218 transferBytesStatus = [AILocalizedString(@"Initiating file transfer",nil) stringByAppendingEllipsis];
222 case Unknown_Status_FileTransfer:
223 case Not_Started_FileTransfer:
224 transferBytesStatus = [AILocalizedString(@"Initiating file transfer",nil) stringByAppendingEllipsis];
226 case Checksumming_Filetransfer:
227 transferBytesStatus = [AILocalizedString(@"Preparing file transfer","File transfer preparing status description") stringByAppendingEllipsis];
229 case Waiting_on_Remote_User_FileTransfer:
230 transferBytesStatus = [AILocalizedString(@"Waiting for transfer to be accepted","File transfer waiting on remote user status description") stringByAppendingEllipsis];
232 case Connecting_FileTransfer:
233 transferBytesStatus = [AILocalizedString(@"Establishing file transfer connection","File transfer connecting status description") stringByAppendingEllipsis];
235 case Accepted_FileTransfer:
236 transferBytesStatus = [AILocalizedString(@"Accepted file transfer",nil) stringByAppendingEllipsis];
238 case In_Progress_FileTransfer:
240 NSString *bytesString = [[adium fileTransferController] stringForSize:bytesSent
242 ofString:sizeString];
245 case Incoming_FileTransfer:
246 transferBytesStatus = BYTES_RECEIVED;
248 case Outgoing_FileTransfer:
249 transferBytesStatus = BYTES_SENT;
257 case Complete_FileTransfer:
259 NSString *bytesString = sizeString;
261 case Incoming_FileTransfer:
262 transferBytesStatus = BYTES_RECEIVED;
264 case Outgoing_FileTransfer:
265 transferBytesStatus = BYTES_SENT;
273 case Cancelled_Local_FileTransfer:
274 transferBytesStatus = AILocalizedString(@"Cancelled","File transfer cancelled locally status description");
276 case Cancelled_Remote_FileTransfer:
277 transferBytesStatus = AILocalizedString(@"Remote contact cancelled","File transfer cancelled remotely status description");
279 case Failed_FileTransfer:
280 transferBytesStatus = AILocalizedString(@"Failed","File transfer failed status description");
287 if ((status == In_Progress_FileTransfer) && lastUpdateTick && lastBytesSent) {
288 if (updateTick != lastUpdateTick) {
289 if ([bytesSentQueue count] == 0) {
290 [bytesSentQueue insertObject:[NSNumber numberWithUnsignedLongLong:lastBytesSent] atIndex:0];
291 [updateTickQueue insertObject:[NSNumber numberWithUnsignedLong:lastUpdateTick] atIndex:0];
292 } else if ([bytesSentQueue count] >= BUFFER_SIZE) {
293 [bytesSentQueue removeObjectAtIndex:0];
294 [updateTickQueue removeObjectAtIndex:0];
297 [bytesSentQueue addObject:[NSNumber numberWithUnsignedLongLong:bytesSent]];
298 [updateTickQueue addObject:[NSNumber numberWithUnsignedLong:updateTick]];
300 unsigned long long bytesDifference = bytesSent - [[bytesSentQueue objectAtIndex:0] unsignedLongLongValue];
301 unsigned long ticksDifference = updateTick - [[updateTickQueue objectAtIndex:0] unsignedLongValue];
302 unsigned long long rate = bytesDifference / (ticksDifference / 60.0);
304 transferSpeedStatus = [NSString stringWithFormat:AILocalizedString(@"%@/sec","Rate of transfer phrase. %@ will be replaced by an abbreviated data amount such as 4 KB or 1 MB"),[[adium fileTransferController] stringForSize:rate]];
307 unsigned long long secsRemaining = ((size - bytesSent) / rate);
308 transferRemainingStatus = [NSString stringWithFormat:AILocalizedString(@"%@ remaining","Time remaining for a file transfer to be completed phrase. %@ will be replaced by an amount of time such as '5 seconds' or '4 minutes and 30 seconds'."),[self readableTimeForSecs:secsRemaining inLongFormat:YES]];
310 transferRemainingStatus = AILocalizedString(@"Stalled","file transfer is stalled status message");
315 [view setTransferBytesStatus:transferBytesStatus
316 remainingStatus:transferRemainingStatus
317 speedStatus:transferSpeedStatus];
318 [view setNeedsDisplay:YES];
320 lastBytesSent = bytesSent;
321 lastUpdateTick = updateTick;
324 - (void)updateIconImage
328 if ((iconImage = [fileTransfer iconImage])) {
329 [view setIconImage:iconImage];
333 - (void)updateSourceAndDestination
335 AIListObject *source = [fileTransfer source];
336 AIListObject *destination = [fileTransfer destination];
338 [view setSourceName:[source formattedUID]];
339 [view setSourceIcon:[AIUserIcons menuUserIconForObject:source]];
341 [view setDestinationName:[destination formattedUID]];
342 [view setDestinationIcon:[AIUserIcons menuUserIconForObject:destination]];
346 #pragma mark Button actions
347 - (IBAction)stopResumeAction:(id)sender
349 [fileTransfer cancel];
351 - (IBAction)revealAction:(id)sender
353 [fileTransfer reveal];
355 - (IBAction)openFileAction:(id)sender
357 if ([fileTransfer status] == Complete_FileTransfer) {
358 [fileTransfer openFile];
361 - (void)removeRowAction:(id)sender
363 if ([fileTransfer isStopped]) {
364 [owner _removeFileTransferRow:self];
368 #pragma mark Contextual menu
369 - (NSMenu *)menuForEvent:(NSEvent *)theEvent
371 NSMenu *contextualMenu = [[NSMenu allocWithZone:[NSMenu menuZone]] init];
372 NSMenuItem *menuItem;
374 //Allow open and show in finder on complete incoming transfers and all outgoing transfers
375 if (([fileTransfer status] == Complete_FileTransfer) ||
376 ([fileTransfer fileTransferType] == Outgoing_FileTransfer)) {
377 menuItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Open",nil)
379 action:@selector(openFileAction:)
380 keyEquivalent:@""] autorelease];
381 [contextualMenu addItem:menuItem];
383 menuItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Show in Finder",nil)
385 action:@selector(revealAction:)
386 keyEquivalent:@""] autorelease];
387 [contextualMenu addItem:menuItem];
391 if ([fileTransfer isStopped]) {
392 menuItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Remove from List",nil)
394 action:@selector(removeRowAction:)
395 keyEquivalent:@""] autorelease];
396 [contextualMenu addItem:menuItem];
398 menuItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Cancel",nil)
400 action:@selector(stopResumeAction:)
401 keyEquivalent:@""] autorelease];
402 [contextualMenu addItem:menuItem];
405 return [contextualMenu autorelease];
408 //Pass height change information on to our owner
409 - (void)fileTransferProgressView:(ESFileTransferProgressView *)inView
410 heightChangedFrom:(float)oldHeight
413 [owner fileTransferProgressRow:self
414 heightChangedFrom:oldHeight
418 #pragma mark Localized readable values
420 - (NSString *)readableTimeForSecs:(NSTimeInterval)secs inLongFormat:(BOOL)longFormat
422 unsigned int i = 0, stop = 0;
423 NSDictionary *desc = [NSDictionary dictionaryWithObjectsAndKeys:AILocalizedString( @"second", "singular second" ), [NSNumber numberWithUnsignedInt:1], AILocalizedString( @"minute", "singular minute" ), [NSNumber numberWithUnsignedInt:60], AILocalizedString( @"hour", "singular hour" ), [NSNumber numberWithUnsignedInt:3600], AILocalizedString( @"day", "singular day" ), [NSNumber numberWithUnsignedInt:86400], AILocalizedString( @"week", "singular week" ), [NSNumber numberWithUnsignedInt:604800], AILocalizedString( @"month", "singular month" ), [NSNumber numberWithUnsignedInt:2628000], AILocalizedString( @"year", "singular year" ), [NSNumber numberWithUnsignedInt:31536000], nil];
424 NSDictionary *plural = [NSDictionary dictionaryWithObjectsAndKeys:AILocalizedString( @"seconds", "plural seconds" ), [NSNumber numberWithUnsignedInt:1], AILocalizedString( @"minutes", "plural minutes" ), [NSNumber numberWithUnsignedInt:60], AILocalizedString( @"hours", "plural hours" ), [NSNumber numberWithUnsignedInt:3600], AILocalizedString( @"days", "plural days" ), [NSNumber numberWithUnsignedInt:86400], AILocalizedString( @"weeks", "plural weeks" ), [NSNumber numberWithUnsignedInt:604800], AILocalizedString( @"months", "plural months" ), [NSNumber numberWithUnsignedInt:2628000], AILocalizedString( @"years", "plural years" ), [NSNumber numberWithUnsignedInt:31536000], nil];
425 NSDictionary *use = nil;
426 NSMutableArray *breaks = nil;
427 unsigned int val = 0.;
428 NSString *retval = nil;
430 if ( secs < 0 ) secs *= -1;
432 breaks = [[[desc allKeys] mutableCopy] autorelease];
433 [breaks sortUsingSelector:@selector( compare: )];
435 while ( i < [breaks count] && secs >= (NSTimeInterval) [[breaks objectAtIndex:i] unsignedIntValue] ) i++;
437 stop = [[breaks objectAtIndex:i] unsignedIntValue];
439 val = (unsigned int) ( secs / stop );
440 use = ( val != 1 ? plural : desc );
441 retval = [NSString stringWithFormat:@"%d %@", val, [use objectForKey:[NSNumber numberWithUnsignedInt:stop]]];
442 if ( longFormat && i > 0 ) {
443 unsigned int rest = (unsigned int) ( (unsigned int) secs % stop );
444 stop = [[breaks objectAtIndex:--i] unsignedIntValue];
445 rest = (unsigned int) ( rest / stop );
447 use = ( rest > 1 ? plural : desc );
448 retval = [retval stringByAppendingFormat:@" %d %@", rest, [use objectForKey:[breaks objectAtIndex:i]]];