1 /******************************************************************************
2 * $Id: CreatorWindowController.m 13251 2012-03-13 02:52:11Z livings124 $
4 * Copyright (c) 2007-2012 Transmission authors and contributors
6 * Permission is hereby granted, free of charge, to any person obtaining a
7 * copy of this software and associated documentation files (the "Software"),
8 * to deal in the Software without restriction, including without limitation
9 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
10 * and/or sell copies of the Software, and to permit persons to whom the
11 * Software is furnished to do so, subject to the following conditions:
13 * The above copyright notice and this permission notice shall be included in
14 * all copies or substantial portions of the Software.
16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
22 * DEALINGS IN THE SOFTWARE.
23 *****************************************************************************/
25 #import "CreatorWindowController.h"
26 #import "NSStringAdditions.h"
28 #import "transmission.h" // required by utils.h
29 #import "utils.h" // tr_urlIsValidTracker()
31 #define TRACKER_ADD_TAG 0
32 #define TRACKER_REMOVE_TAG 1
34 @interface CreatorWindowController (Private)
36 + (NSString *) chooseFile;
38 - (void) createBlankAddressAlertDidEnd: (NSAlert *) alert returnCode: (NSInteger) returnCode contextInfo: (void *) contextInfo;
40 - (void) checkProgress;
41 - (void) failureSheetClosed: (NSAlert *) alert returnCode: (NSInteger) code contextInfo: (void *) info;
45 @implementation CreatorWindowController
47 + (void) createTorrentFile: (tr_session *) handle
49 //get file/folder for torrent
51 if (!(path = [CreatorWindowController chooseFile]))
54 CreatorWindowController * creator = [[self alloc] initWithHandle: handle path: path];
55 [creator showWindow: nil];
58 + (void) createTorrentFile: (tr_session *) handle forFile: (NSString *) file
60 CreatorWindowController * creator = [[self alloc] initWithHandle: handle path: file];
61 [creator showWindow: nil];
64 - (id) initWithHandle: (tr_session *) handle path: (NSString *) path
66 if ((self = [super initWithWindowNibName: @"Creator"]))
70 fPath = [path retain];
71 fInfo = tr_metaInfoBuilderCreate([fPath UTF8String]);
73 if (fInfo->fileCount == 0)
75 NSAlert * alert = [[NSAlert alloc] init];
76 [alert addButtonWithTitle: NSLocalizedString(@"OK", "Create torrent -> no files -> button")];
77 [alert setMessageText: NSLocalizedString(@"This folder contains no files.",
78 "Create torrent -> no files -> title")];
79 [alert setInformativeText: NSLocalizedString(@"There must be at least one file in a folder to create a torrent file.",
80 "Create torrent -> no files -> warning")];
81 [alert setAlertStyle: NSWarningAlertStyle];
89 if (fInfo->totalSize == 0)
91 NSAlert * alert = [[NSAlert alloc] init];
92 [alert addButtonWithTitle: NSLocalizedString(@"OK", "Create torrent -> zero size -> button")];
93 [alert setMessageText: NSLocalizedString(@"The total file size is zero bytes.",
94 "Create torrent -> zero size -> title")];
95 [alert setInformativeText: NSLocalizedString(@"A torrent file cannot be created for files with no size.",
96 "Create torrent -> zero size -> warning")];
97 [alert setAlertStyle: NSWarningAlertStyle];
106 fDefaults = [NSUserDefaults standardUserDefaults];
108 //get list of trackers
109 if (!(fTrackers = [[fDefaults arrayForKey: @"CreatorTrackers"] mutableCopy]))
111 fTrackers = [[NSMutableArray alloc] initWithCapacity: 1];
113 //check for single tracker from versions before 1.3
115 if ((tracker = [fDefaults stringForKey: @"CreatorTracker"]))
117 [fDefaults removeObjectForKey: @"CreatorTracker"];
118 if (![tracker isEqualToString: @""])
120 [fTrackers addObject: tracker];
121 [fDefaults setObject: fTrackers forKey: @"CreatorTrackers"];
126 //remove potentially invalid addresses
127 for (NSInteger i = [fTrackers count]-1; i >= 0; i--)
129 if (!tr_urlIsValidTracker([[fTrackers objectAtIndex: i] UTF8String]))
130 [fTrackers removeObjectAtIndex: i];
136 - (void) awakeFromNib
138 NSString * name = [fPath lastPathComponent];
140 [[self window] setTitle: name];
142 [fNameField setStringValue: name];
143 [fNameField setToolTip: fPath];
145 const BOOL multifile = !fInfo->isSingleFile;
147 NSImage * icon = [[NSWorkspace sharedWorkspace] iconForFileType: multifile
148 ? NSFileTypeForHFSTypeCode(kGenericFolderIcon) : [fPath pathExtension]];
149 [icon setSize: [fIconView frame].size];
150 [fIconView setImage: icon];
152 NSString * statusString = [NSString stringForFileSize: fInfo->totalSize];
155 NSString * fileString;
156 NSInteger count = fInfo->fileCount;
158 fileString = [NSString stringWithFormat: NSLocalizedString(@"%@ files", "Create torrent -> info"),
159 [NSString formattedUInteger: count]];
161 fileString = NSLocalizedString(@"1 file", "Create torrent -> info");
162 statusString = [NSString stringWithFormat: @"%@, %@", fileString, statusString];
164 [fStatusField setStringValue: statusString];
166 if (fInfo->pieceCount == 1)
167 [fPiecesField setStringValue: [NSString stringWithFormat: NSLocalizedString(@"1 piece, %@", "Create torrent -> info"),
168 [NSString stringForFileSize: fInfo->pieceSize]]];
170 [fPiecesField setStringValue: [NSString stringWithFormat: NSLocalizedString(@"%d pieces, %@ each", "Create torrent -> info"),
171 fInfo->pieceCount, [NSString stringForFileSize: fInfo->pieceSize]]];
173 fLocation = [[[[fDefaults stringForKey: @"CreatorLocation"] stringByExpandingTildeInPath] stringByAppendingPathComponent:
174 [name stringByAppendingPathExtension: @"torrent"]] retain];
175 [fLocationField setStringValue: [fLocation stringByAbbreviatingWithTildeInPath]];
176 [fLocationField setToolTip: fLocation];
178 //set previously saved values
179 if ([fDefaults objectForKey: @"CreatorPrivate"])
180 [fPrivateCheck setState: [fDefaults boolForKey: @"CreatorPrivate"] ? NSOnState : NSOffState];
182 [fOpenCheck setState: [fDefaults boolForKey: @"CreatorOpen"] ? NSOnState : NSOffState];
193 tr_metaInfoBuilderFree(fInfo);
200 - (void) setLocation: (id) sender
202 NSSavePanel * panel = [NSSavePanel savePanel];
204 [panel setPrompt: NSLocalizedString(@"Select", "Create torrent -> location sheet -> button")];
205 [panel setMessage: NSLocalizedString(@"Select the name and location for the torrent file.",
206 "Create torrent -> location sheet -> message")];
208 [panel setAllowedFileTypes: [NSArray arrayWithObjects: @"org.bittorrent.torrent", @"torrent", nil]];
209 [panel setCanSelectHiddenExtension: YES];
211 [panel setDirectoryURL: [NSURL fileURLWithPath: [fLocation stringByDeletingLastPathComponent]]];
212 [panel setNameFieldStringValue: [fLocation lastPathComponent]];
214 [panel beginSheetModalForWindow: [self window] completionHandler: ^(NSInteger result) {
215 if (result == NSFileHandlingPanelOKButton)
218 fLocation = [[[panel URL] path] retain];
220 [fLocationField setStringValue: [fLocation stringByAbbreviatingWithTildeInPath]];
221 [fLocationField setToolTip: fLocation];
226 - (void) create: (id) sender
228 //make sure the trackers are no longer being verified
229 if ([fTrackerTable editedRow] != -1)
230 [[self window] endEditingFor: fTrackerTable];
232 const BOOL isPrivate = [fPrivateCheck state] == NSOnState;
233 if ([fTrackers count] == 0
234 && [fDefaults boolForKey: isPrivate ? @"WarningCreatorPrivateBlankAddress" : @"WarningCreatorBlankAddress"])
236 NSAlert * alert = [[NSAlert alloc] init];
237 [alert setMessageText: NSLocalizedString(@"There are no tracker addresses.", "Create torrent -> blank address -> title")];
239 NSString * infoString = isPrivate
240 ? NSLocalizedString(@"A transfer marked as private with no tracker addresses will be unable to connect to peers."
241 " The torrent file will only be useful if you plan to upload the file to a tracker website"
242 " that will add the addresses for you.", "Create torrent -> blank address -> message")
243 : NSLocalizedString(@"The transfer will not contact trackers for peers, and will have to rely solely on"
244 " non-tracker peer discovery methods such as PEX and DHT to download and seed.",
245 "Create torrent -> blank address -> message");
247 [alert setInformativeText: infoString];
248 [alert addButtonWithTitle: NSLocalizedString(@"Create", "Create torrent -> blank address -> button")];
249 [alert addButtonWithTitle: NSLocalizedString(@"Cancel", "Create torrent -> blank address -> button")];
250 [alert setShowsSuppressionButton: YES];
252 [alert beginSheetModalForWindow: [self window] modalDelegate: self
253 didEndSelector: @selector(createBlankAddressAlertDidEnd:returnCode:contextInfo:) contextInfo: nil];
259 - (void) cancelCreateWindow: (id) sender
261 [[self window] close];
264 - (void) windowWillClose: (NSNotification *) notification
269 - (void) cancelCreateProgress: (id) sender
271 fInfo->abortFlag = 1;
275 - (NSInteger) numberOfRowsInTableView: (NSTableView *) tableView
277 return [fTrackers count];
280 - (id) tableView: (NSTableView *) tableView objectValueForTableColumn: (NSTableColumn *) tableColumn row: (NSInteger) row
282 return [fTrackers objectAtIndex: row];
285 - (void) addRemoveTracker: (id) sender
287 //don't allow add/remove when currently adding - it leads to weird results
288 if ([fTrackerTable editedRow] != -1)
291 if ([[sender cell] tagForSegment: [sender selectedSegment]] == TRACKER_REMOVE_TAG)
293 [fTrackers removeObjectsAtIndexes: [fTrackerTable selectedRowIndexes]];
295 [fTrackerTable deselectAll: self];
296 [fTrackerTable reloadData];
300 [fTrackers addObject: @""];
301 [fTrackerTable reloadData];
303 const NSInteger row = [fTrackers count] - 1;
304 [fTrackerTable selectRowIndexes: [NSIndexSet indexSetWithIndex: row] byExtendingSelection: NO];
305 [fTrackerTable editColumn: 0 row: row withEvent: nil select: YES];
309 - (void) tableView: (NSTableView *) tableView setObjectValue: (id) object forTableColumn: (NSTableColumn *) tableColumn
312 NSString * tracker = (NSString *)object;
314 tracker = [tracker stringByTrimmingCharactersInSet: [NSCharacterSet whitespaceAndNewlineCharacterSet]];
316 if ([tracker rangeOfString: @"://"].location == NSNotFound)
317 tracker = [@"http://" stringByAppendingString: tracker];
319 if (!tr_urlIsValidTracker([tracker UTF8String]))
322 [fTrackers removeObjectAtIndex: row];
325 [fTrackers replaceObjectAtIndex: row withObject: tracker];
327 [fTrackerTable deselectAll: self];
328 [fTrackerTable reloadData];
331 - (void) tableViewSelectionDidChange: (NSNotification *) notification
333 [fTrackerAddRemoveControl setEnabled: [fTrackerTable numberOfSelectedRows] > 0 forSegment: TRACKER_REMOVE_TAG];
336 - (void) copy: (id) sender
338 NSArray * addresses = [fTrackers objectsAtIndexes: [fTrackerTable selectedRowIndexes]];
339 NSString * text = [addresses componentsJoinedByString: @"\n"];
341 NSPasteboard * pb = [NSPasteboard generalPasteboard];
343 [pb writeObjects: [NSArray arrayWithObject: text]];
346 - (BOOL) validateMenuItem: (NSMenuItem *) menuItem
348 const SEL action = [menuItem action];
350 if (action == @selector(copy:))
351 return [[self window] firstResponder] == fTrackerTable && [fTrackerTable numberOfSelectedRows] > 0;
353 if (action == @selector(paste:))
354 return [[self window] firstResponder] == fTrackerTable
355 && [[NSPasteboard generalPasteboard] canReadObjectForClasses: [NSArray arrayWithObject: [NSString class]] options: nil];
360 - (void) paste: (id) sender
362 NSMutableArray * tempTrackers = [NSMutableArray array];
364 NSArray * items = [[NSPasteboard generalPasteboard] readObjectsForClasses: [NSArray arrayWithObject: [NSString class]] options: nil];
365 NSAssert(items != nil, @"no string items to paste; should not be able to call this method");
367 for (NSString * pbItem in items)
369 for (NSString * tracker in [pbItem componentsSeparatedByString: @"\n"])
370 [tempTrackers addObject: tracker];
375 for (NSString * tracker in tempTrackers)
377 tracker = [tracker stringByTrimmingCharactersInSet: [NSCharacterSet whitespaceAndNewlineCharacterSet]];
379 if ([tracker rangeOfString: @"://"].location == NSNotFound)
380 tracker = [@"http://" stringByAppendingString: tracker];
382 if (tr_urlIsValidTracker([tracker UTF8String]))
384 [fTrackers addObject: tracker];
391 [fTrackerTable deselectAll: self];
392 [fTrackerTable reloadData];
400 @implementation CreatorWindowController (Private)
402 + (NSString *) chooseFile
404 NSOpenPanel * panel = [NSOpenPanel openPanel];
406 [panel setTitle: NSLocalizedString(@"Create Torrent File", "Create torrent -> select file")];
407 [panel setPrompt: NSLocalizedString(@"Select", "Create torrent -> select file")];
408 [panel setAllowsMultipleSelection: NO];
409 [panel setCanChooseFiles: YES];
410 [panel setCanChooseDirectories: YES];
411 [panel setCanCreateDirectories: NO];
413 [panel setMessage: NSLocalizedString(@"Select a file or folder for the torrent file.", "Create torrent -> select file")];
415 BOOL success = [panel runModal] == NSOKButton;
416 return success ? [[[panel URLs] objectAtIndex: 0] path] : nil;
419 - (void) createBlankAddressAlertDidEnd: (NSAlert *) alert returnCode: (NSInteger) returnCode contextInfo: (void *) contextInfo
421 if ([[alert suppressionButton] state] == NSOnState)
423 [[NSUserDefaults standardUserDefaults] setBool: NO forKey: @"WarningCreatorBlankAddress"]; //set regardless of private/public
424 if ([fPrivateCheck state] == NSOnState)
425 [[NSUserDefaults standardUserDefaults] setBool: NO forKey: @"WarningCreatorPrivateBlankAddress"];
430 if (returnCode == NSAlertFirstButtonReturn)
431 [self performSelectorOnMainThread: @selector(createReal) withObject: nil waitUntilDone: NO];
436 //check if the location currently exists
437 if (![[NSFileManager defaultManager] fileExistsAtPath: [fLocation stringByDeletingLastPathComponent]])
439 NSAlert * alert = [[[NSAlert alloc] init] autorelease];
440 [alert addButtonWithTitle: NSLocalizedString(@"OK", "Create torrent -> directory doesn't exist warning -> button")];
441 [alert setMessageText: NSLocalizedString(@"The chosen torrent file location does not exist.",
442 "Create torrent -> directory doesn't exist warning -> title")];
443 [alert setInformativeText: [NSString stringWithFormat:
444 NSLocalizedString(@"The directory \"%@\" does not currently exist. "
445 "Create this directory or choose a different one to create the torrent file.",
446 "Create torrent -> directory doesn't exist warning -> warning"),
447 [fLocation stringByDeletingLastPathComponent]]];
448 [alert setAlertStyle: NSWarningAlertStyle];
450 [alert beginSheetModalForWindow: [self window] modalDelegate: self didEndSelector: nil contextInfo: nil];
454 //check if a file with the same name and location already exists
455 if ([[NSFileManager defaultManager] fileExistsAtPath: fLocation])
457 NSArray * pathComponents = [fLocation pathComponents];
458 NSInteger count = [pathComponents count];
460 NSAlert * alert = [[[NSAlert alloc] init] autorelease];
461 [alert addButtonWithTitle: NSLocalizedString(@"OK", "Create torrent -> file already exists warning -> button")];
462 [alert setMessageText: NSLocalizedString(@"A torrent file with this name and directory cannot be created.",
463 "Create torrent -> file already exists warning -> title")];
464 [alert setInformativeText: [NSString stringWithFormat:
465 NSLocalizedString(@"A file with the name \"%@\" already exists in the directory \"%@\". "
466 "Choose a new name or directory to create the torrent file.",
467 "Create torrent -> file already exists warning -> warning"),
468 [pathComponents objectAtIndex: count-1], [pathComponents objectAtIndex: count-2]]];
469 [alert setAlertStyle: NSWarningAlertStyle];
471 [alert beginSheetModalForWindow: [self window] modalDelegate: self didEndSelector: nil contextInfo: nil];
475 //parse non-empty tracker strings
476 tr_tracker_info * trackerInfo = tr_new0(tr_tracker_info, [fTrackers count]);
478 for (NSUInteger i = 0; i < [fTrackers count]; i++)
480 trackerInfo[i].announce = (char *)[[fTrackers objectAtIndex: i] UTF8String];
481 trackerInfo[i].tier = i;
485 [fDefaults setObject: fTrackers forKey: @"CreatorTrackers"];
486 [fDefaults setBool: [fPrivateCheck state] == NSOnState forKey: @"CreatorPrivate"];
487 [fDefaults setBool: [fOpenCheck state] == NSOnState forKey: @"CreatorOpen"];
488 [fDefaults setObject: [fLocation stringByDeletingLastPathComponent] forKey: @"CreatorLocation"];
490 [[NSNotificationCenter defaultCenter] postNotificationName: @"BeginCreateTorrentFile" object: fLocation userInfo: nil];
491 tr_makeMetaInfo(fInfo, [fLocation UTF8String], trackerInfo, [fTrackers count], [[fCommentView string] UTF8String],
492 [fPrivateCheck state] == NSOnState);
493 tr_free(trackerInfo);
495 fTimer = [NSTimer scheduledTimerWithTimeInterval: 0.1 target: self selector: @selector(checkProgress)
496 userInfo: nil repeats: YES];
499 - (void) checkProgress
507 switch (fInfo->result)
510 if ([fDefaults boolForKey: @"CreatorOpen"])
512 NSDictionary * dict = [[NSDictionary alloc] initWithObjectsAndKeys: fLocation, @"File",
513 [fPath stringByDeletingLastPathComponent], @"Path", nil];
514 [[NSNotificationCenter defaultCenter] postNotificationName: @"OpenCreatedTorrentFile" object: self userInfo: dict];
517 [[self window] close];
520 case TR_MAKEMETA_CANCELLED:
521 [[self window] close];
525 alert = [[[NSAlert alloc] init] autorelease];
526 [alert addButtonWithTitle: NSLocalizedString(@"OK", "Create torrent -> failed -> button")];
527 [alert setMessageText: [NSString stringWithFormat: NSLocalizedString(@"Creation of \"%@\" failed.",
528 "Create torrent -> failed -> title"), [fLocation lastPathComponent]]];
529 [alert setAlertStyle: NSWarningAlertStyle];
531 if (fInfo->result == TR_MAKEMETA_IO_READ)
532 [alert setInformativeText: [NSString stringWithFormat: NSLocalizedString(@"Could not read \"%s\": %s.",
533 "Create torrent -> failed -> warning"), fInfo->errfile, strerror(fInfo->my_errno)]];
534 else if (fInfo->result == TR_MAKEMETA_IO_WRITE)
535 [alert setInformativeText: [NSString stringWithFormat: NSLocalizedString(@"Could not write \"%s\": %s.",
536 "Create torrent -> failed -> warning"), fInfo->errfile, strerror(fInfo->my_errno)]];
537 else //invalid url should have been caught before creating
538 [alert setInformativeText: [NSString stringWithFormat: @"%@ (%d)",
539 NSLocalizedString(@"An unknown error has occurred.", "Create torrent -> failed -> warning"), fInfo->result]];
541 [alert beginSheetModalForWindow: [self window] modalDelegate: self
542 didEndSelector: @selector(failureSheetClosed:returnCode:contextInfo:) contextInfo: nil];
547 [fProgressIndicator setDoubleValue: (double)fInfo->pieceIndex / fInfo->pieceCount];
553 [fProgressView setHidden: YES];
555 NSWindow * window = [self window];
556 [window setFrameAutosaveName: @""];
558 NSRect windowRect = [window frame];
559 CGFloat difference = [fProgressView frame].size.height - [[window contentView] frame].size.height;
560 windowRect.origin.y -= difference;
561 windowRect.size.height += difference;
563 //don't allow vertical resizing
564 CGFloat height = windowRect.size.height;
565 [window setMinSize: NSMakeSize([window minSize].width, height)];
566 [window setMaxSize: NSMakeSize([window maxSize].width, height)];
568 [window setContentView: fProgressView];
569 [window setFrame: windowRect display: YES animate: YES];
570 [fProgressView setHidden: NO];
572 [[window standardWindowButton: NSWindowCloseButton] setEnabled: NO];
577 - (void) failureSheetClosed: (NSAlert *) alert returnCode: (NSInteger) code contextInfo: (void *) info
579 [[alert window] orderOut: nil];
580 [[self window] close];