1/****************************************************************************** 2 * $Id: Controller.m 13578 2012-10-17 00:25:28Z livings124 $ 3 * 4 * Copyright (c) 2005-2012 Transmission authors and contributors 5 * 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: 12 * 13 * The above copyright notice and this permission notice shall be included in 14 * all copies or substantial portions of the Software. 15 * 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 *****************************************************************************/ 24 25#import <IOKit/IOMessage.h> 26#import <IOKit/pwr_mgt/IOPMLib.h> 27#import <Carbon/Carbon.h> 28 29#import "Controller.h" 30#import "Torrent.h" 31#import "TorrentGroup.h" 32#import "TorrentTableView.h" 33#import "CreatorWindowController.h" 34#import "StatsWindowController.h" 35#import "InfoWindowController.h" 36#import "PrefsController.h" 37#import "GroupsController.h" 38#import "AboutWindowController.h" 39#import "URLSheetWindowController.h" 40#import "AddWindowController.h" 41#import "AddMagnetWindowController.h" 42#import "MessageWindowController.h" 43#import "GlobalOptionsPopoverViewController.h" 44#import "ButtonToolbarItem.h" 45#import "GroupToolbarItem.h" 46#import "ToolbarSegmentedCell.h" 47#import "BlocklistDownloader.h" 48#import "StatusBarController.h" 49#import "FilterBarController.h" 50#import "BonjourController.h" 51#import "Badger.h" 52#import "DragOverlayWindow.h" 53#import "NSApplicationAdditions.h" 54#import "NSMutableArrayAdditions.h" 55#import "NSStringAdditions.h" 56#import "ExpandedPathToPathTransformer.h" 57#import "ExpandedPathToIconTransformer.h" 58 59#import "transmission.h" 60#import "bencode.h" 61#import "utils.h" 62 63#import "UKKQueue.h" 64#import <Sparkle/Sparkle.h> 65 66#define TOOLBAR_CREATE @"Toolbar Create" 67#define TOOLBAR_OPEN_FILE @"Toolbar Open" 68#define TOOLBAR_OPEN_WEB @"Toolbar Open Web" 69#define TOOLBAR_REMOVE @"Toolbar Remove" 70#define TOOLBAR_INFO @"Toolbar Info" 71#define TOOLBAR_PAUSE_ALL @"Toolbar Pause All" 72#define TOOLBAR_RESUME_ALL @"Toolbar Resume All" 73#define TOOLBAR_PAUSE_RESUME_ALL @"Toolbar Pause / Resume All" 74#define TOOLBAR_PAUSE_SELECTED @"Toolbar Pause Selected" 75#define TOOLBAR_RESUME_SELECTED @"Toolbar Resume Selected" 76#define TOOLBAR_PAUSE_RESUME_SELECTED @"Toolbar Pause / Resume Selected" 77#define TOOLBAR_FILTER @"Toolbar Toggle Filter" 78#define TOOLBAR_QUICKLOOK @"Toolbar QuickLook" 79 80typedef enum 81{ 82 TOOLBAR_PAUSE_TAG = 0, 83 TOOLBAR_RESUME_TAG = 1 84} toolbarGroupTag; 85 86#define SORT_DATE @"Date" 87#define SORT_NAME @"Name" 88#define SORT_STATE @"State" 89#define SORT_PROGRESS @"Progress" 90#define SORT_TRACKER @"Tracker" 91#define SORT_ORDER @"Order" 92#define SORT_ACTIVITY @"Activity" 93#define SORT_SIZE @"Size" 94 95typedef enum 96{ 97 SORT_ORDER_TAG = 0, 98 SORT_DATE_TAG = 1, 99 SORT_NAME_TAG = 2, 100 SORT_PROGRESS_TAG = 3, 101 SORT_STATE_TAG = 4, 102 SORT_TRACKER_TAG = 5, 103 SORT_ACTIVITY_TAG = 6, 104 SORT_SIZE_TAG = 7 105} sortTag; 106 107typedef enum 108{ 109 SORT_ASC_TAG = 0, 110 SORT_DESC_TAG = 1 111} sortOrderTag; 112 113#define GROWL_DOWNLOAD_COMPLETE @"Download Complete" 114#define GROWL_SEEDING_COMPLETE @"Seeding Complete" 115#define GROWL_AUTO_ADD @"Torrent Auto Added" 116#define GROWL_AUTO_SPEED_LIMIT @"Speed Limit Auto Changed" 117 118#define TORRENT_TABLE_VIEW_DATA_TYPE @"TorrentTableViewDataType" 119 120#define ROW_HEIGHT_REGULAR 62.0 121#define ROW_HEIGHT_SMALL 22.0 122#define WINDOW_REGULAR_WIDTH 468.0 123 124#define STATUS_BAR_HEIGHT 21.0 125#define FILTER_BAR_HEIGHT 23.0 126 127#define UPDATE_UI_SECONDS 1.0 128 129#define TRANSFER_PLIST @"Transfers.plist" 130 131#define WEBSITE_URL @"http://www.transmissionbt.com/" 132#define FORUM_URL @"http://forum.transmissionbt.com/" 133#define TRAC_URL @"http://trac.transmissionbt.com/" 134#define DONATE_URL @"http://www.transmissionbt.com/donate.php" 135 136#define DONATE_NAG_TIME (60 * 60 * 24 * 7) 137 138static void altSpeedToggledCallback(tr_session * handle UNUSED, bool active, bool byUser, void * controller) 139{ 140 NSDictionary * dict = [[NSDictionary alloc] initWithObjectsAndKeys: [[NSNumber alloc] initWithBool: active], @"Active", 141 [[NSNumber alloc] initWithBool: byUser], @"ByUser", nil]; 142 [(Controller *)controller performSelectorOnMainThread: @selector(altSpeedToggledCallbackIsLimited:) 143 withObject: dict waitUntilDone: NO]; 144} 145 146static tr_rpc_callback_status rpcCallback(tr_session * handle UNUSED, tr_rpc_callback_type type, struct tr_torrent * torrentStruct, 147 void * controller) 148{ 149 [(Controller *)controller rpcCallback: type forTorrentStruct: torrentStruct]; 150 return TR_RPC_NOREMOVE; //we'll do the remove manually 151} 152 153static void sleepCallback(void * controller, io_service_t y, natural_t messageType, void * messageArgument) 154{ 155 [(Controller *)controller sleepCallback: messageType argument: messageArgument]; 156} 157 158@implementation Controller 159 160#warning remove ivars in header when 64-bit only (or it compiles in 32-bit mode) 161@synthesize prefsController = fPrefsController; 162@synthesize messageWindowController = fMessageController; 163 164+ (void) initialize 165{ 166 //make sure another Transmission.app isn't running already 167 NSArray * apps = [NSRunningApplication runningApplicationsWithBundleIdentifier: [[NSBundle mainBundle] bundleIdentifier]]; 168 if ([apps count] > 1) 169 { 170 NSAlert * alert = [[NSAlert alloc] init]; 171 [alert addButtonWithTitle: NSLocalizedString(@"OK", "Transmission already running alert -> button")]; 172 [alert setMessageText: NSLocalizedString(@"Transmission is already running.", 173 "Transmission already running alert -> title")]; 174 [alert setInformativeText: NSLocalizedString(@"There is already a copy of Transmission running. " 175 "This copy cannot be opened until that instance is quit.", "Transmission already running alert -> message")]; 176 [alert setAlertStyle: NSCriticalAlertStyle]; 177 178 [alert runModal]; 179 [alert release]; 180 181 //kill ourselves right away 182 exit(0); 183 } 184 185 [[NSUserDefaults standardUserDefaults] registerDefaults: [NSDictionary dictionaryWithContentsOfFile: 186 [[NSBundle mainBundle] pathForResource: @"Defaults" ofType: @"plist"]]]; 187 188 //set custom value transformers 189 ExpandedPathToPathTransformer * pathTransformer = [[[ExpandedPathToPathTransformer alloc] init] autorelease]; 190 [NSValueTransformer setValueTransformer: pathTransformer forName: @"ExpandedPathToPathTransformer"]; 191 192 ExpandedPathToIconTransformer * iconTransformer = [[[ExpandedPathToIconTransformer alloc] init] autorelease]; 193 [NSValueTransformer setValueTransformer: iconTransformer forName: @"ExpandedPathToIconTransformer"]; 194 195 //cover our asses 196 if ([[NSUserDefaults standardUserDefaults] boolForKey: @"WarningLegal"]) 197 { 198 NSAlert * alert = [[NSAlert alloc] init]; 199 [alert addButtonWithTitle: NSLocalizedString(@"I Accept", "Legal alert -> button")]; 200 [alert addButtonWithTitle: NSLocalizedString(@"Quit", "Legal alert -> button")]; 201 [alert setMessageText: NSLocalizedString(@"Welcome to Transmission", "Legal alert -> title")]; 202 [alert setInformativeText: NSLocalizedString(@"Transmission is a file-sharing program." 203 " When you run a torrent, its data will be made available to others by means of upload." 204 " You and you alone are fully responsible for exercising proper judgement and abiding by your local laws.", 205 "Legal alert -> message")]; 206 [alert setAlertStyle: NSInformationalAlertStyle]; 207 208 if ([alert runModal] == NSAlertSecondButtonReturn) 209 exit(0); 210 [alert release]; 211 212 [[NSUserDefaults standardUserDefaults] setBool: NO forKey: @"WarningLegal"]; 213 } 214} 215 216- (id) init 217{ 218 if ((self = [super init])) 219 { 220 fDefaults = [NSUserDefaults standardUserDefaults]; 221 222 //checks for old version speeds of -1 223 if ([fDefaults integerForKey: @"UploadLimit"] < 0) 224 { 225 [fDefaults removeObjectForKey: @"UploadLimit"]; 226 [fDefaults setBool: NO forKey: @"CheckUpload"]; 227 } 228 if ([fDefaults integerForKey: @"DownloadLimit"] < 0) 229 { 230 [fDefaults removeObjectForKey: @"DownloadLimit"]; 231 [fDefaults setBool: NO forKey: @"CheckDownload"]; 232 } 233 234 //upgrading from versions < 2.40: clear recent items 235 [[NSDocumentController sharedDocumentController] clearRecentDocuments: nil]; 236 237 tr_benc settings; 238 tr_bencInitDict(&settings, 41); 239 tr_sessionGetDefaultSettings(&settings); 240 241 const BOOL usesSpeedLimitSched = [fDefaults boolForKey: @"SpeedLimitAuto"]; 242 if (!usesSpeedLimitSched) 243 tr_bencDictAddBool(&settings, TR_PREFS_KEY_ALT_SPEED_ENABLED, [fDefaults boolForKey: @"SpeedLimit"]); 244 245 tr_bencDictAddInt(&settings, TR_PREFS_KEY_ALT_SPEED_UP_KBps, [fDefaults integerForKey: @"SpeedLimitUploadLimit"]); 246 tr_bencDictAddInt(&settings, TR_PREFS_KEY_ALT_SPEED_DOWN_KBps, [fDefaults integerForKey: @"SpeedLimitDownloadLimit"]); 247 248 tr_bencDictAddBool(&settings, TR_PREFS_KEY_ALT_SPEED_TIME_ENABLED, [fDefaults boolForKey: @"SpeedLimitAuto"]); 249 tr_bencDictAddInt(&settings, TR_PREFS_KEY_ALT_SPEED_TIME_BEGIN, [PrefsController dateToTimeSum: 250 [fDefaults objectForKey: @"SpeedLimitAutoOnDate"]]); 251 tr_bencDictAddInt(&settings, TR_PREFS_KEY_ALT_SPEED_TIME_END, [PrefsController dateToTimeSum: 252 [fDefaults objectForKey: @"SpeedLimitAutoOffDate"]]); 253 tr_bencDictAddInt(&settings, TR_PREFS_KEY_ALT_SPEED_TIME_DAY, [fDefaults integerForKey: @"SpeedLimitAutoDay"]); 254 255 tr_bencDictAddInt(&settings, TR_PREFS_KEY_DSPEED_KBps, [fDefaults integerForKey: @"DownloadLimit"]); 256 tr_bencDictAddBool(&settings, TR_PREFS_KEY_DSPEED_ENABLED, [fDefaults boolForKey: @"CheckDownload"]); 257 tr_bencDictAddInt(&settings, TR_PREFS_KEY_USPEED_KBps, [fDefaults integerForKey: @"UploadLimit"]); 258 tr_bencDictAddBool(&settings, TR_PREFS_KEY_USPEED_ENABLED, [fDefaults boolForKey: @"CheckUpload"]); 259 260 //hidden prefs 261 if ([fDefaults objectForKey: @"BindAddressIPv4"]) 262 tr_bencDictAddStr(&settings, TR_PREFS_KEY_BIND_ADDRESS_IPV4, [[fDefaults stringForKey: @"BindAddressIPv4"] UTF8String]); 263 if ([fDefaults objectForKey: @"BindAddressIPv6"]) 264 tr_bencDictAddStr(&settings, TR_PREFS_KEY_BIND_ADDRESS_IPV6, [[fDefaults stringForKey: @"BindAddressIPv6"] UTF8String]); 265 266 tr_bencDictAddBool(&settings, TR_PREFS_KEY_BLOCKLIST_ENABLED, [fDefaults boolForKey: @"BlocklistNew"]); 267 if ([fDefaults objectForKey: @"BlocklistURL"]) 268 tr_bencDictAddStr(&settings, TR_PREFS_KEY_BLOCKLIST_URL, [[fDefaults stringForKey: @"BlocklistURL"] UTF8String]); 269 tr_bencDictAddBool(&settings, TR_PREFS_KEY_DHT_ENABLED, [fDefaults boolForKey: @"DHTGlobal"]); 270 tr_bencDictAddStr(&settings, TR_PREFS_KEY_DOWNLOAD_DIR, [[[fDefaults stringForKey: @"DownloadFolder"] 271 stringByExpandingTildeInPath] UTF8String]); 272 tr_bencDictAddBool(&settings, TR_PREFS_KEY_DOWNLOAD_QUEUE_ENABLED, [fDefaults boolForKey: @"Queue"]); 273 tr_bencDictAddInt(&settings, TR_PREFS_KEY_DOWNLOAD_QUEUE_SIZE, [fDefaults integerForKey: @"QueueDownloadNumber"]); 274 tr_bencDictAddInt(&settings, TR_PREFS_KEY_IDLE_LIMIT, [fDefaults integerForKey: @"IdleLimitMinutes"]); 275 tr_bencDictAddBool(&settings, TR_PREFS_KEY_IDLE_LIMIT_ENABLED, [fDefaults boolForKey: @"IdleLimitCheck"]); 276 tr_bencDictAddStr(&settings, TR_PREFS_KEY_INCOMPLETE_DIR, [[[fDefaults stringForKey: @"IncompleteDownloadFolder"] 277 stringByExpandingTildeInPath] UTF8String]); 278 tr_bencDictAddBool(&settings, TR_PREFS_KEY_INCOMPLETE_DIR_ENABLED, [fDefaults boolForKey: @"UseIncompleteDownloadFolder"]); 279 tr_bencDictAddBool(&settings, TR_PREFS_KEY_LPD_ENABLED, [fDefaults boolForKey: @"LocalPeerDiscoveryGlobal"]); 280 tr_bencDictAddInt(&settings, TR_PREFS_KEY_MSGLEVEL, TR_MSG_DBG); 281 tr_bencDictAddInt(&settings, TR_PREFS_KEY_PEER_LIMIT_GLOBAL, [fDefaults integerForKey: @"PeersTotal"]); 282 tr_bencDictAddInt(&settings, TR_PREFS_KEY_PEER_LIMIT_TORRENT, [fDefaults integerForKey: @"PeersTorrent"]); 283 284 const BOOL randomPort = [fDefaults boolForKey: @"RandomPort"]; 285 tr_bencDictAddBool(&settings, TR_PREFS_KEY_PEER_PORT_RANDOM_ON_START, randomPort); 286 if (!randomPort) 287 tr_bencDictAddInt(&settings, TR_PREFS_KEY_PEER_PORT, [fDefaults integerForKey: @"BindPort"]); 288 289 //hidden pref 290 if ([fDefaults objectForKey: @"PeerSocketTOS"]) 291 tr_bencDictAddStr(&settings, TR_PREFS_KEY_PEER_SOCKET_TOS, [[fDefaults stringForKey: @"PeerSocketTOS"] UTF8String]); 292 293 tr_bencDictAddBool(&settings, TR_PREFS_KEY_PEX_ENABLED, [fDefaults boolForKey: @"PEXGlobal"]); 294 tr_bencDictAddBool(&settings, TR_PREFS_KEY_PORT_FORWARDING, [fDefaults boolForKey: @"NatTraversal"]); 295 tr_bencDictAddBool(&settings, TR_PREFS_KEY_QUEUE_STALLED_ENABLED, [fDefaults boolForKey: @"CheckStalled"]); 296 tr_bencDictAddInt(&settings, TR_PREFS_KEY_QUEUE_STALLED_MINUTES, [fDefaults integerForKey: @"StalledMinutes"]); 297 tr_bencDictAddReal(&settings, TR_PREFS_KEY_RATIO, [fDefaults floatForKey: @"RatioLimit"]); 298 tr_bencDictAddBool(&settings, TR_PREFS_KEY_RATIO_ENABLED, [fDefaults boolForKey: @"RatioCheck"]); 299 tr_bencDictAddBool(&settings, TR_PREFS_KEY_RENAME_PARTIAL_FILES, [fDefaults boolForKey: @"RenamePartialFiles"]); 300 tr_bencDictAddBool(&settings, TR_PREFS_KEY_RPC_AUTH_REQUIRED, [fDefaults boolForKey: @"RPCAuthorize"]); 301 tr_bencDictAddBool(&settings, TR_PREFS_KEY_RPC_ENABLED, [fDefaults boolForKey: @"RPC"]); 302 tr_bencDictAddInt(&settings, TR_PREFS_KEY_RPC_PORT, [fDefaults integerForKey: @"RPCPort"]); 303 tr_bencDictAddStr(&settings, TR_PREFS_KEY_RPC_USERNAME, [[fDefaults stringForKey: @"RPCUsername"] UTF8String]); 304 tr_bencDictAddBool(&settings, TR_PREFS_KEY_RPC_WHITELIST_ENABLED, [fDefaults boolForKey: @"RPCUseWhitelist"]); 305 tr_bencDictAddBool(&settings, TR_PREFS_KEY_SEED_QUEUE_ENABLED, [fDefaults boolForKey: @"QueueSeed"]); 306 tr_bencDictAddInt(&settings, TR_PREFS_KEY_SEED_QUEUE_SIZE, [fDefaults integerForKey: @"QueueSeedNumber"]); 307 tr_bencDictAddBool(&settings, TR_PREFS_KEY_START, [fDefaults boolForKey: @"AutoStartDownload"]); 308 tr_bencDictAddBool(&settings, TR_PREFS_KEY_SCRIPT_TORRENT_DONE_ENABLED, [fDefaults boolForKey: @"DoneScriptEnabled"]); 309 tr_bencDictAddStr(&settings, TR_PREFS_KEY_SCRIPT_TORRENT_DONE_FILENAME, [[fDefaults stringForKey: @"DoneScriptPath"] UTF8String]); 310 tr_bencDictAddBool(&settings, TR_PREFS_KEY_UTP_ENABLED, [fDefaults boolForKey: @"UTPGlobal"]); 311 312 313 NSString * kbString, * mbString, * gbString, * tbString; 314 if ([NSApp isOnMountainLionOrBetter]) 315 { 316 NSByteCountFormatter * unitFormatter = [[NSByteCountFormatterMtLion alloc] init]; 317 [unitFormatter setIncludesCount: NO]; 318 [unitFormatter setAllowsNonnumericFormatting: NO]; 319 320 [unitFormatter setAllowedUnits: NSByteCountFormatterUseKB]; 321 kbString = [unitFormatter stringFromByteCount: 17]; //use a random value to avoid possible pluralization issues with 1 or 0 (an example is if we use 1 for bytes, we'd get "byte" when we'd want "bytes" for the generic libtransmission value at least) 322 323 [unitFormatter setAllowedUnits: NSByteCountFormatterUseMB]; 324 mbString = [unitFormatter stringFromByteCount: 17]; 325 326 [unitFormatter setAllowedUnits: NSByteCountFormatterUseGB]; 327 gbString = [unitFormatter stringFromByteCount: 17]; 328 329 [unitFormatter setAllowedUnits: NSByteCountFormatterUseTB]; 330 tbString = [unitFormatter stringFromByteCount: 17]; 331 332 [unitFormatter release]; 333 } 334 else 335 { 336 kbString = NSLocalizedString(@"KB", "file/memory size - kilobytes"); 337 mbString = NSLocalizedString(@"MB", "file/memory size - megabytes"); 338 gbString = NSLocalizedString(@"GB", "file/memory size - gigabytes"); 339 tbString = NSLocalizedString(@"TB", "file/memory size - terabytes"); 340 } 341 342 tr_formatter_size_init(1000, [kbString UTF8String], 343 [mbString UTF8String], 344 [gbString UTF8String], 345 [tbString UTF8String]); 346 347 tr_formatter_speed_init(1000, [NSLocalizedString(@"KB/s", "Transfer speed (kilobytes per second)") UTF8String], 348 [NSLocalizedString(@"MB/s", "Transfer speed (megabytes per second)") UTF8String], 349 [NSLocalizedString(@"GB/s", "Transfer speed (gigabytes per second)") UTF8String], 350 [NSLocalizedString(@"TB/s", "Transfer speed (terabytes per second)") UTF8String]); //why not? 351 352 tr_formatter_mem_init(1000, [kbString UTF8String], 353 [mbString UTF8String], 354 [gbString UTF8String], 355 [tbString UTF8String]); 356 357 const char * configDir = tr_getDefaultConfigDir("Transmission"); 358 fLib = tr_sessionInit("macosx", configDir, YES, &settings); 359 tr_bencFree(&settings); 360 361 fConfigDirectory = [[NSString alloc] initWithUTF8String: configDir]; 362 363 [NSApp setDelegate: self]; 364 365 //register for magnet URLs (has to be in init) 366 [[NSAppleEventManager sharedAppleEventManager] setEventHandler: self andSelector: @selector(handleOpenContentsEvent:replyEvent:) 367 forEventClass: kInternetEventClass andEventID: kAEGetURL]; 368 369 fTorrents = [[NSMutableArray alloc] init]; 370 fDisplayedTorrents = [[NSMutableArray alloc] init]; 371 372 fInfoController = [[InfoWindowController alloc] init]; 373 374 fPrefsController = [[PrefsController alloc] initWithHandle: fLib]; 375 376 fQuitting = NO; 377 fGlobalPopoverShown = NO; 378 fSoundPlaying = NO; 379 380 tr_sessionSetAltSpeedFunc(fLib, altSpeedToggledCallback, self); 381 if (usesSpeedLimitSched) 382 [fDefaults setBool: tr_sessionUsesAltSpeed(fLib) forKey: @"SpeedLimit"]; 383 384 tr_sessionSetRPCCallback(fLib, rpcCallback, self); 385 386 [GrowlApplicationBridge setGrowlDelegate: self]; 387 388 [[UKKQueue sharedFileWatcher] setDelegate: self]; 389 390 [[SUUpdater sharedUpdater] setDelegate: self]; 391 fQuitRequested = NO; 392 393 fPauseOnLaunch = (GetCurrentKeyModifiers() & (optionKey | rightOptionKey)) != 0; 394 } 395 return self; 396} 397 398- (void) awakeFromNib 399{ 400 NSToolbar * toolbar = [[NSToolbar alloc] initWithIdentifier: @"TRMainToolbar"]; 401 [toolbar setDelegate: self]; 402 [toolbar setAllowsUserCustomization: YES]; 403 [toolbar setAutosavesConfiguration: YES]; 404 [toolbar setDisplayMode: NSToolbarDisplayModeIconOnly]; 405 [fWindow setToolbar: toolbar]; 406 [toolbar release]; 407 408 [fWindow setDelegate: self]; //do manually to avoid placement issue 409 410 [fWindow makeFirstResponder: fTableView]; 411 [fWindow setExcludedFromWindowsMenu: YES]; 412 413 //set table size 414 const BOOL small = [fDefaults boolForKey: @"SmallView"]; 415 if (small) 416 [fTableView setRowHeight: ROW_HEIGHT_SMALL]; 417 [fTableView setUsesAlternatingRowBackgroundColors: !small]; 418 419 [fWindow setContentBorderThickness: NSMinY([[fTableView enclosingScrollView] frame]) forEdge: NSMinYEdge]; 420 [fWindow setMovableByWindowBackground: YES]; 421 422 [[fTotalTorrentsField cell] setBackgroundStyle: NSBackgroundStyleRaised]; 423 424 //set up filter bar 425 [self showFilterBar: [fDefaults boolForKey: @"FilterBar"] animate: NO]; 426 427 //set up status bar 428 [self showStatusBar: [fDefaults boolForKey: @"StatusBar"] animate: NO]; 429 430 [fActionButton setToolTip: NSLocalizedString(@"Shortcuts for changing global settings.", 431 "Main window -> 1st bottom left button (action) tooltip")]; 432 [fSpeedLimitButton setToolTip: NSLocalizedString(@"Speed Limit overrides the total bandwidth limits with its own limits.", 433 "Main window -> 2nd bottom left button (turtle) tooltip")]; 434 [fClearCompletedButton setToolTip: NSLocalizedString(@"Remove all transfers that have completed seeding.", 435 "Main window -> 3rd bottom left button (remove all) tooltip")]; 436 437 [fTableView registerForDraggedTypes: [NSArray arrayWithObject: TORRENT_TABLE_VIEW_DATA_TYPE]]; 438 [fWindow registerForDraggedTypes: [NSArray arrayWithObjects: NSFilenamesPboardType, NSURLPboardType, nil]]; 439 440 //sort the sort menu items (localization is from strings file) 441 NSMutableArray * sortMenuItems = [NSMutableArray arrayWithCapacity: 7]; 442 NSUInteger sortMenuIndex = 0; 443 BOOL foundSortItem = NO; 444 for (NSMenuItem * item in [fSortMenu itemArray]) 445 { 446 //assume all sort items are together and the Queue Order item is first 447 if ([item action] == @selector(setSort:) && [item tag] != SORT_ORDER_TAG) 448 { 449 [sortMenuItems addObject: item]; 450 [fSortMenu removeItemAtIndex: sortMenuIndex]; 451 foundSortItem = YES; 452 } 453 else 454 { 455 if (foundSortItem) 456 break; 457 ++sortMenuIndex; 458 } 459 } 460 461 [sortMenuItems sortUsingDescriptors: [NSArray arrayWithObject: [NSSortDescriptor sortDescriptorWithKey: @"title" ascending: YES selector: @selector(localizedCompare:)]]]; 462 463 for (NSMenuItem * item in sortMenuItems) 464 [fSortMenu insertItem: item atIndex: sortMenuIndex++]; 465 466 //you would think this would be called later in this method from updateUI, but it's not reached in awakeFromNib 467 //this must be called after showStatusBar: 468 [fStatusBar updateWithDownload: 0.0 upload: 0.0]; 469 470 //this should also be after the rest of the setup 471 [self updateForAutoSize]; 472 473 //register for sleep notifications 474 IONotificationPortRef notify; 475 io_object_t iterator; 476 if ((fRootPort = IORegisterForSystemPower(self, & notify, sleepCallback, &iterator))) 477 CFRunLoopAddSource(CFRunLoopGetCurrent(), IONotificationPortGetRunLoopSource(notify), kCFRunLoopCommonModes); 478 else 479 NSLog(@"Could not IORegisterForSystemPower"); 480 481 //load previous transfers 482 NSString * historyFile = [fConfigDirectory stringByAppendingPathComponent: TRANSFER_PLIST]; 483 NSArray * history = [NSArray arrayWithContentsOfFile: historyFile]; 484 if (!history) 485 { 486 //old version saved transfer info in prefs file 487 if ((history = [fDefaults arrayForKey: @"History"])) 488 [fDefaults removeObjectForKey: @"History"]; 489 } 490 491 if (history) 492 { 493 NSMutableArray * waitToStartTorrents = [NSMutableArray arrayWithCapacity: (([history count] > 0 && !fPauseOnLaunch) ? [history count]-1 : 0)]; //theoretical max without doing a lot of work 494 495 for (NSDictionary * historyItem in history) 496 { 497 Torrent * torrent; 498 if ((torrent = [[Torrent alloc] initWithHistory: historyItem lib: fLib forcePause: fPauseOnLaunch])) 499 { 500 [fTorrents addObject: torrent]; 501 502 NSNumber * waitToStart; 503 if (!fPauseOnLaunch && (waitToStart = [historyItem objectForKey: @"WaitToStart"]) && [waitToStart boolValue]) 504 [waitToStartTorrents addObject: torrent]; 505 506 [torrent release]; 507 } 508 } 509 510 //now that all are loaded, let's set those in the queue to waiting 511 for (Torrent * torrent in waitToStartTorrents) 512 [torrent startTransfer]; 513 } 514 515 fBadger = [[Badger alloc] initWithLib: fLib]; 516 517 if ([NSApp isOnMountainLionOrBetter]) 518 [[NSUserNotificationCenterMtLion defaultUserNotificationCenter] setDelegate: self]; 519 520 //observe notifications 521 NSNotificationCenter * nc = [NSNotificationCenter defaultCenter]; 522 523 [nc addObserver: self selector: @selector(updateUI) 524 name: @"UpdateUI" object: nil]; 525 526 [nc addObserver: self selector: @selector(torrentFinishedDownloading:) 527 name: @"TorrentFinishedDownloading" object: nil]; 528 529 [nc addObserver: self selector: @selector(torrentRestartedDownloading:) 530 name: @"TorrentRestartedDownloading" object: nil]; 531 532 [nc addObserver: self selector: @selector(torrentFinishedSeeding:) 533 name: @"TorrentFinishedSeeding" object: nil]; 534 535 //avoids need of setting delegate 536 [nc addObserver: self selector: @selector(torrentTableViewSelectionDidChange:) 537 name: NSOutlineViewSelectionDidChangeNotification object: fTableView]; 538 539 [nc addObserver: self selector: @selector(changeAutoImport) 540 name: @"AutoImportSettingChange" object: nil]; 541 542 [nc addObserver: self selector: @selector(updateForAutoSize) 543 name: @"AutoSizeSettingChange" object: nil]; 544 545 [nc addObserver: self selector: @selector(updateForExpandCollape) 546 name: @"OutlineExpandCollapse" object: nil]; 547 548 [nc addObserver: fWindow selector: @selector(makeKeyWindow) 549 name: @"MakeWindowKey" object: nil]; 550 551 [nc addObserver: self selector: @selector(fullUpdateUI) 552 name: @"UpdateQueue" object: nil]; 553 554 [nc addObserver: self selector: @selector(applyFilter) 555 name: @"ApplyFilter" object: nil]; 556 557 //open newly created torrent file 558 [nc addObserver: self selector: @selector(beginCreateFile:) 559 name: @"BeginCreateTorrentFile" object: nil]; 560 561 //open newly created torrent file 562 [nc addObserver: self selector: @selector(openCreatedFile:) 563 name: @"OpenCreatedTorrentFile" object: nil]; 564 565 [nc addObserver: self selector: @selector(applyFilter) 566 name: @"UpdateGroups" object: nil]; 567 568 //timer to update the interface every second 569 [self updateUI]; 570 fTimer = [[NSTimer scheduledTimerWithTimeInterval: UPDATE_UI_SECONDS target: self 571 selector: @selector(updateUI) userInfo: nil repeats: YES] retain]; 572 [[NSRunLoop currentRunLoop] addTimer: fTimer forMode: NSModalPanelRunLoopMode]; 573 [[NSRunLoop currentRunLoop] addTimer: fTimer forMode: NSEventTrackingRunLoopMode]; 574 575 [self applyFilter]; 576 577 [fWindow makeKeyAndOrderFront: nil]; 578 579 if ([fDefaults boolForKey: @"InfoVisible"]) 580 [self showInfo: nil]; 581} 582 583- (void) applicationDidFinishLaunching: (NSNotification *) notification 584{ 585 [NSApp setServicesProvider: self]; 586 587 //register for dock icon drags (has to be in applicationDidFinishLaunching: to work) 588 [[NSAppleEventManager sharedAppleEventManager] setEventHandler: self andSelector: @selector(handleOpenContentsEvent:replyEvent:) 589 forEventClass: kCoreEventClass andEventID: kAEOpenContents]; 590 591 //if we were opened from a user notification, do the corresponding action 592 if ([NSApp isOnMountainLionOrBetter]) 593 { 594 NSUserNotification * launchNotification = [[notification userInfo] objectForKey: NSApplicationLaunchUserNotificationKey]; 595 if (launchNotification) 596 [self userNotificationCenter: nil didActivateNotification: launchNotification]; 597 } 598 599 //auto importing 600 [self checkAutoImportDirectory]; 601 602 //registering the Web UI to Bonjour 603 if ([fDefaults boolForKey: @"RPC"] && [fDefaults boolForKey: @"RPCWebDiscovery"]) 604 [[BonjourController defaultController] startWithPort: [fDefaults integerForKey: @"RPCPort"]]; 605 606 //shamelessly ask for donations 607 if ([fDefaults boolForKey: @"WarningDonate"]) 608 { 609 tr_session_stats stats; 610 tr_sessionGetCumulativeStats(fLib, &stats); 611 const BOOL firstLaunch = stats.sessionCount <= 1; 612 613 NSDate * lastDonateDate = [fDefaults objectForKey: @"DonateAskDate"]; 614 const BOOL timePassed = !lastDonateDate || (-1 * [lastDonateDate timeIntervalSinceNow]) >= DONATE_NAG_TIME; 615 616 if (!firstLaunch && timePassed) 617 { 618 [fDefaults setObject: [NSDate date] forKey: @"DonateAskDate"]; 619 620 NSAlert * alert = [[NSAlert alloc] init]; 621 [alert setMessageText: NSLocalizedString(@"Support open-source indie software", "Donation beg -> title")]; 622 623 NSString * donateMessage = [NSString stringWithFormat: @"%@\n\n%@", 624 NSLocalizedString(@"Transmission is a full-featured torrent application." 625 " A lot of time and effort have gone into development, coding, and refinement." 626 " If you enjoy using it, please consider showing your love with a donation.", "Donation beg -> message"), 627 NSLocalizedString(@"Donate or not, there will be no difference to your torrenting experience.", "Donation beg -> message")]; 628 629 [alert setInformativeText: donateMessage]; 630 [alert setAlertStyle: NSInformationalAlertStyle]; 631 632 [alert addButtonWithTitle: [NSLocalizedString(@"Donate", "Donation beg -> button") stringByAppendingEllipsis]]; 633 NSButton * noDonateButton = [alert addButtonWithTitle: NSLocalizedString(@"Nope", "Donation beg -> button")]; 634 [noDonateButton setKeyEquivalent: @"\e"]; //escape key 635 636 const BOOL allowNeverAgain = lastDonateDate != nil; //hide the "don't show again" check the first time - give them time to try the app 637 [alert setShowsSuppressionButton: allowNeverAgain]; 638 if (allowNeverAgain) 639 [[alert suppressionButton] setTitle: NSLocalizedString(@"Don't bug me about this ever again.", "Donation beg -> button")]; 640 641 const NSInteger donateResult = [alert runModal]; 642 if (donateResult == NSAlertFirstButtonReturn) 643 [self linkDonate: self]; 644 645 if (allowNeverAgain) 646 [fDefaults setBool: ([[alert suppressionButton] state] != NSOnState) forKey: @"WarningDonate"]; 647 648 [alert release]; 649 } 650 } 651} 652 653- (BOOL) applicationShouldHandleReopen: (NSApplication *) app hasVisibleWindows: (BOOL) visibleWindows 654{ 655 NSWindow * mainWindow = [NSApp mainWindow]; 656 if (!mainWindow || ![mainWindow isVisible]) 657 [fWindow makeKeyAndOrderFront: nil]; 658 659 return NO; 660} 661 662- (NSApplicationTerminateReply) applicationShouldTerminate: (NSApplication *) sender 663{ 664 if (!fQuitRequested && [fDefaults boolForKey: @"CheckQuit"]) 665 { 666 NSInteger active = 0, downloading = 0; 667 for (Torrent * torrent in fTorrents) 668 if ([torrent isActive] && ![torrent isStalled]) 669 { 670 active++; 671 if (![torrent allDownloaded]) 672 downloading++; 673 } 674 675 if ([fDefaults boolForKey: @"CheckQuitDownloading"] ? downloading > 0 : active > 0) 676 { 677 NSString * message = active == 1 678 ? NSLocalizedString(@"There is an active transfer that will be paused on quit." 679 " The transfer will automatically resume on the next launch.", "Confirm Quit panel -> message") 680 : [NSString stringWithFormat: NSLocalizedString(@"There are %d active transfers that will be paused on quit." 681 " The transfers will automatically resume on the next launch.", "Confirm Quit panel -> message"), active]; 682 683 NSBeginAlertSheet(NSLocalizedString(@"Are you sure you want to quit?", "Confirm Quit panel -> title"), 684 NSLocalizedString(@"Quit", "Confirm Quit panel -> button"), 685 NSLocalizedString(@"Cancel", "Confirm Quit panel -> button"), nil, fWindow, self, 686 @selector(quitSheetDidEnd:returnCode:contextInfo:), nil, nil, @"%@", message); 687 return NSTerminateLater; 688 } 689 } 690 691 return NSTerminateNow; 692} 693 694- (void) quitSheetDidEnd: (NSWindow *) sheet returnCode: (NSInteger) returnCode contextInfo: (void *) contextInfo 695{ 696 [NSApp replyToApplicationShouldTerminate: returnCode == NSAlertDefaultReturn]; 697} 698 699- (void) applicationWillTerminate: (NSNotification *) notification 700{ 701 fQuitting = YES; 702 703 //stop the Bonjour service 704 if ([BonjourController defaultControllerExists]) 705 [[BonjourController defaultController] stop]; 706 707 //stop blocklist download 708 if ([BlocklistDownloader isRunning]) 709 [[BlocklistDownloader downloader] cancelDownload]; 710 711 //stop timers and notification checking 712 [[NSNotificationCenter defaultCenter] removeObserver: self]; 713 714 [fTimer invalidate]; 715 [fTimer release]; 716 717 if (fAutoImportTimer) 718 { 719 if ([fAutoImportTimer isValid]) 720 [fAutoImportTimer invalidate]; 721 [fAutoImportTimer release]; 722 } 723 724 [fBadger setQuitting]; 725 726 //remove all torrent downloads 727 if (fPendingTorrentDownloads) 728 { 729 for (NSDictionary * downloadDict in fPendingTorrentDownloads) 730 { 731 NSURLDownload * download = [downloadDict objectForKey: @"Download"]; 732 [download cancel]; 733 [download release]; 734 } 735 [fPendingTorrentDownloads release]; 736 } 737 738 //remember window states and close all windows 739 [fDefaults setBool: [[fInfoController window] isVisible] forKey: @"InfoVisible"]; 740 741 if ([QLPreviewPanel sharedPreviewPanelExists] && [[QLPreviewPanel sharedPreviewPanel] isVisible]) 742 [[QLPreviewPanel sharedPreviewPanel] updateController]; 743 744 for (NSWindow * window in [NSApp windows]) 745 [window close]; 746 747 [self showStatusBar: NO animate: NO]; 748 [self showFilterBar: NO animate: NO]; 749 750 //save history 751 [self updateTorrentHistory]; 752 [fTableView saveCollapsedGroups]; 753 754 //remaining calls the same as dealloc 755 [fInfoController release]; 756 [fMessageController release]; 757 [fPrefsController release]; 758 759 [fStatusBar release]; 760 [fFilterBar release]; 761 762 [fTorrents release]; 763 [fDisplayedTorrents release]; 764 765 [fAddWindows release]; 766 [fAddingTransfers release]; 767 768 [fOverlayWindow release]; 769 [fBadger release]; 770 771 [fAutoImportedNames release]; 772 773 [fPreviewPanel release]; 774 775 [fConfigDirectory release]; 776 777 //complete cleanup 778 tr_sessionClose(fLib); 779} 780 781- (tr_session *) sessionHandle 782{ 783 return fLib; 784} 785 786- (void) handleOpenContentsEvent: (NSAppleEventDescriptor *) event replyEvent: (NSAppleEventDescriptor *) replyEvent 787{ 788 NSString * urlString = nil; 789 790 NSAppleEventDescriptor * directObject = [event paramDescriptorForKeyword: keyDirectObject]; 791 if ([directObject descriptorType] == typeAEList) 792 { 793 for (NSInteger i = 1; i <= [directObject numberOfItems]; i++) 794 if ((urlString = [[directObject descriptorAtIndex: i] stringValue])) 795 break; 796 } 797 else 798 urlString = [directObject stringValue]; 799 800 if (urlString) 801 [self openURL: urlString]; 802} 803 804- (void) download: (NSURLDownload *) download decideDestinationWithSuggestedFilename: (NSString *) suggestedName 805{ 806 if ([[suggestedName pathExtension] caseInsensitiveCompare: @"torrent"] != NSOrderedSame) 807 { 808 [download cancel]; 809 810 [fPendingTorrentDownloads removeObjectForKey: [[download request] URL]]; 811 if ([fPendingTorrentDownloads count] == 0) 812 { 813 [fPendingTorrentDownloads release]; 814 fPendingTorrentDownloads = nil; 815 } 816 817 NSRunAlertPanel(NSLocalizedString(@"Torrent download failed", "Download not a torrent -> title"), 818 [NSString stringWithFormat: NSLocalizedString(@"It appears that the file \"%@\" from %@ is not a torrent file.", 819 "Download not a torrent -> message"), suggestedName, 820 [[[[download request] URL] absoluteString] stringByReplacingPercentEscapesUsingEncoding: NSUTF8StringEncoding]], 821 NSLocalizedString(@"OK", "Download not a torrent -> button"), nil, nil); 822 823 [download release]; 824 } 825 else 826 [download setDestination: [NSTemporaryDirectory() stringByAppendingPathComponent: [suggestedName lastPathComponent]] 827 allowOverwrite: NO]; 828} 829 830-(void) download: (NSURLDownload *) download didCreateDestination: (NSString *) path 831{ 832 [(NSMutableDictionary *)[fPendingTorrentDownloads objectForKey: [[download request] URL]] setObject: path forKey: @"Path"]; 833} 834 835- (void) download: (NSURLDownload *) download didFailWithError: (NSError *) error 836{ 837 NSRunAlertPanel(NSLocalizedString(@"Torrent download failed", "Torrent download error -> title"), 838 [NSString stringWithFormat: NSLocalizedString(@"The torrent could not be downloaded from %@: %@.", 839 "Torrent download failed -> message"), 840 [[[[download request] URL] absoluteString] stringByReplacingPercentEscapesUsingEncoding: NSUTF8StringEncoding], 841 [error localizedDescription]], NSLocalizedString(@"OK", "Torrent download failed -> button"), nil, nil); 842 843 [fPendingTorrentDownloads removeObjectForKey: [[download request] URL]]; 844 if ([fPendingTorrentDownloads count] == 0) 845 { 846 [fPendingTorrentDownloads release]; 847 fPendingTorrentDownloads = nil; 848 } 849 850 [download release]; 851} 852 853- (void) downloadDidFinish: (NSURLDownload *) download 854{ 855 NSString * path = [[fPendingTorrentDownloads objectForKey: [[download request] URL]] objectForKey: @"Path"]; 856 857 [self openFiles: [NSArray arrayWithObject: path] addType: ADD_URL forcePath: nil]; 858 859 //delete the torrent file after opening 860 [[NSFileManager defaultManager] removeItemAtPath: path error: NULL]; 861 862 [fPendingTorrentDownloads removeObjectForKey: [[download request] URL]]; 863 if ([fPendingTorrentDownloads count] == 0) 864 { 865 [fPendingTorrentDownloads release]; 866 fPendingTorrentDownloads = nil; 867 } 868 869 [download release]; 870} 871 872- (void) application: (NSApplication *) app openFiles: (NSArray *) filenames 873{ 874 [self openFiles: filenames addType: ADD_MANUAL forcePath: nil]; 875} 876 877- (void) openFiles: (NSArray *) filenames addType: (addType) type forcePath: (NSString *) path 878{ 879 BOOL deleteTorrentFile, canToggleDelete = NO; 880 switch (type) 881 { 882 case ADD_CREATED: 883 deleteTorrentFile = NO; 884 break; 885 case ADD_URL: 886 deleteTorrentFile = YES; 887 break; 888 default: 889 deleteTorrentFile = [fDefaults boolForKey: @"DeleteOriginalTorrent"]; 890 canToggleDelete = YES; 891 } 892 893 for (NSString * torrentPath in filenames) 894 { 895 //ensure torrent doesn't already exist 896 tr_ctor * ctor = tr_ctorNew(fLib); 897 tr_ctorSetMetainfoFromFile(ctor, [torrentPath UTF8String]); 898 899 tr_info info; 900 const tr_parse_result result = tr_torrentParse(ctor, &info); 901 tr_ctorFree(ctor); 902 903 if (result != TR_PARSE_OK) 904 { 905 if (result == TR_PARSE_DUPLICATE) 906 [self duplicateOpenAlert: [NSString stringWithUTF8String: info.name]]; 907 else if (result == TR_PARSE_ERR) 908 { 909 if (type != ADD_AUTO) 910 [self invalidOpenAlert: [torrentPath lastPathComponent]]; 911 } 912 else 913 NSAssert2(NO, @"Unknown error code (%d) when attempting to open \"%@\"", result, torrentPath); 914 915 tr_metainfoFree(&info); 916 continue; 917 } 918 919 //determine download location 920 NSString * location; 921 BOOL lockDestination = NO; //don't override the location with a group location if it has a hardcoded path 922 if (path) 923 { 924 location = [path stringByExpandingTildeInPath]; 925 lockDestination = YES; 926 } 927 else if ([fDefaults boolForKey: @"DownloadLocationConstant"]) 928 location = [[fDefaults stringForKey: @"DownloadFolder"] stringByExpandingTildeInPath]; 929 else if (type != ADD_URL) 930 location = [torrentPath stringByDeletingLastPathComponent]; 931 else 932 location = nil; 933 934 //determine to show the options window 935 const BOOL showWindow = type == ADD_SHOW_OPTIONS || ([fDefaults boolForKey: @"DownloadAsk"] 936 && (info.isMultifile || ![fDefaults boolForKey: @"DownloadAskMulti"]) 937 && (type != ADD_AUTO || ![fDefaults boolForKey: @"DownloadAskManual"])); 938 tr_metainfoFree(&info); 939 940 Torrent * torrent; 941 if (!(torrent = [[Torrent alloc] initWithPath: torrentPath location: location 942 deleteTorrentFile: showWindow ? NO : deleteTorrentFile lib: fLib])) 943 continue; 944 945 //change the location if the group calls for it (this has to wait until after the torrent is created) 946 if (!lockDestination && [[GroupsController groups] usesCustomDownloadLocationForIndex: [torrent groupValue]]) 947 { 948 location = [[GroupsController groups] customDownloadLocationForIndex: [torrent groupValue]]; 949 [torrent changeDownloadFolderBeforeUsing: location]; 950 } 951 952 //verify the data right away if it was newly created 953 if (type == ADD_CREATED) 954 [torrent resetCache]; 955 956 //show the add window or add directly 957 if (showWindow || !location) 958 { 959 AddWindowController * addController = [[AddWindowController alloc] initWithTorrent: torrent destination: location 960 lockDestination: lockDestination controller: self torrentFile: torrentPath 961 deleteTorrentCheckEnableInitially: deleteTorrentFile canToggleDelete: canToggleDelete]; 962 [addController showWindow: self]; 963 964 if (!fAddWindows) 965 fAddWindows = [[NSMutableSet alloc] init]; 966 [fAddWindows addObject: addController]; 967 [addController release]; 968 } 969 else 970 { 971 if ([fDefaults boolForKey: @"AutoStartDownload"]) 972 [torrent startTransfer]; 973 974 [torrent update]; 975 [fTorrents addObject: torrent]; 976 [torrent release]; 977 978 if (!fAddingTransfers) 979 fAddingTransfers = [[NSMutableSet alloc] init]; 980 [fAddingTransfers addObject: torrent]; 981 } 982 } 983 984 [self fullUpdateUI]; 985} 986 987- (void) askOpenConfirmed: (AddWindowController *) addController add: (BOOL) add 988{ 989 Torrent * torrent = [addController torrent]; 990 991 if (add) 992 { 993 [torrent setQueuePosition: [fTorrents count]]; 994 995 [torrent update]; 996 [fTorrents addObject: torrent]; 997 [torrent release]; 998 999 if (!fAddingTransfers) 1000 fAddingTransfers = [[NSMutableSet alloc] init]; 1001 [fAddingTransfers addObject: torrent]; 1002 1003 [self fullUpdateUI]; 1004 } 1005 else 1006 { 1007 [torrent closeRemoveTorrent: NO]; 1008 [torrent release]; 1009 } 1010 1011 [fAddWindows removeObject: addController]; 1012 if ([fAddWindows count] == 0) 1013 { 1014 [fAddWindows release]; 1015 fAddWindows = nil; 1016 } 1017} 1018 1019- (void) openMagnet: (NSString *) address 1020{ 1021 tr_torrent * duplicateTorrent; 1022 if ((duplicateTorrent = tr_torrentFindFromMagnetLink(fLib, [address UTF8String]))) 1023 { 1024 const tr_info * info = tr_torrentInfo(duplicateTorrent); 1025 NSString * name = (info != NULL && info->name != NULL) ? [NSString stringWithUTF8String: info->name] : nil; 1026 [self duplicateOpenMagnetAlert: address transferName: name]; 1027 return; 1028 } 1029 1030 //determine download location 1031 NSString * location = nil; 1032 if ([fDefaults boolForKey: @"DownloadLocationConstant"]) 1033 location = [[fDefaults stringForKey: @"DownloadFolder"] stringByExpandingTildeInPath]; 1034 1035 Torrent * torrent; 1036 if (!(torrent = [[Torrent alloc] initWithMagnetAddress: address location: location lib: fLib])) 1037 { 1038 [self invalidOpenMagnetAlert: address]; 1039 return; 1040 } 1041 1042 //change the location if the group calls for it (this has to wait until after the torrent is created) 1043 if ([[GroupsController groups] usesCustomDownloadLocationForIndex: [torrent groupValue]]) 1044 { 1045 location = [[GroupsController groups] customDownloadLocationForIndex: [torrent groupValue]]; 1046 [torrent changeDownloadFolderBeforeUsing: location]; 1047 } 1048 1049 if ([fDefaults boolForKey: @"MagnetOpenAsk"] || !location) 1050 { 1051 AddMagnetWindowController * addController = [[AddMagnetWindowController alloc] initWithTorrent: torrent destination: location 1052 controller: self]; 1053 [addController showWindow: self]; 1054 1055 if (!fAddWindows) 1056 fAddWindows = [[NSMutableSet alloc] init]; 1057 [fAddWindows addObject: addController]; 1058 [addController release]; 1059 } 1060 else 1061 { 1062 if ([fDefaults boolForKey: @"AutoStartDownload"]) 1063 [torrent startTransfer]; 1064 1065 [torrent update]; 1066 [fTorrents addObject: torrent]; 1067 [torrent release]; 1068 1069 if (!fAddingTransfers) 1070 fAddingTransfers = [[NSMutableSet alloc] init]; 1071 [fAddingTransfers addObject: torrent]; 1072 } 1073 1074 [self fullUpdateUI]; 1075} 1076 1077- (void) askOpenMagnetConfirmed: (AddMagnetWindowController *) addController add: (BOOL) add 1078{ 1079 Torrent * torrent = [addController torrent]; 1080 1081 if (add) 1082 { 1083 [torrent setQueuePosition: [fTorrents count]]; 1084 1085 [torrent update]; 1086 [fTorrents addObject: torrent]; 1087 [torrent release]; 1088 1089 if (!fAddingTransfers) 1090 fAddingTransfers = [[NSMutableSet alloc] init]; 1091 [fAddingTransfers addObject: torrent]; 1092 1093 [self fullUpdateUI]; 1094 } 1095 else 1096 { 1097 [torrent closeRemoveTorrent: NO]; 1098 [torrent release]; 1099 } 1100 1101 [fAddWindows removeObject: addController]; 1102 if ([fAddWindows count] == 0) 1103 { 1104 [fAddWindows release]; 1105 fAddWindows = nil; 1106 } 1107} 1108 1109- (void) openCreatedFile: (NSNotification *) notification 1110{ 1111 NSDictionary * dict = [notification userInfo]; 1112 [self openFiles: [NSArray arrayWithObject: [dict objectForKey: @"File"]] addType: ADD_CREATED forcePath: [dict objectForKey: @"Path"]]; 1113 [dict release]; 1114} 1115 1116- (void) openFilesWithDict: (NSDictionary *) dictionary 1117{ 1118 [self openFiles: [dictionary objectForKey: @"Filenames"] addType: [[dictionary objectForKey: @"AddType"] intValue] forcePath: nil]; 1119 1120 [dictionary release]; 1121} 1122 1123//called on by applescript 1124- (void) open: (NSArray *) files 1125{ 1126 NSDictionary * dict = [[NSDictionary alloc] initWithObjectsAndKeys: files, @"Filenames", 1127 [NSNumber numberWithInt: ADD_MANUAL], @"AddType", nil]; 1128 [self performSelectorOnMainThread: @selector(openFilesWithDict:) withObject: dict waitUntilDone: NO]; 1129} 1130 1131- (void) openShowSheet: (id) sender 1132{ 1133 NSOpenPanel * panel = [NSOpenPanel openPanel]; 1134 1135 [panel setAllowsMultipleSelection: YES]; 1136 [panel setCanChooseFiles: YES]; 1137 [panel setCanChooseDirectories: NO]; 1138 1139 [panel setAllowedFileTypes: [NSArray arrayWithObjects: @"org.bittorrent.torrent", @"torrent", nil]]; 1140 1141 [panel beginSheetModalForWindow: fWindow completionHandler: ^(NSInteger result) { 1142 if (result == NSFileHandlingPanelOKButton) 1143 { 1144 NSMutableArray * filenames = [NSMutableArray arrayWithCapacity: [[panel URLs] count]]; 1145 for (NSURL * url in [panel URLs]) 1146 [filenames addObject: [url path]]; 1147 1148 NSDictionary * dictionary = [[NSDictionary alloc] initWithObjectsAndKeys: filenames, @"Filenames", 1149 [NSNumber numberWithInt: sender == fOpenIgnoreDownloadFolder ? ADD_SHOW_OPTIONS : ADD_MANUAL], @"AddType", nil]; 1150 [self performSelectorOnMainThread: @selector(openFilesWithDict:) withObject: dictionary waitUntilDone: NO]; 1151 } 1152 }]; 1153} 1154 1155- (void) invalidOpenAlert: (NSString *) filename 1156{ 1157 if (![fDefaults boolForKey: @"WarningInvalidOpen"]) 1158 return; 1159 1160 NSAlert * alert = [[NSAlert alloc] init]; 1161 [alert setMessageText: [NSString stringWithFormat: NSLocalizedString(@"\"%@\" is not a valid torrent file.", 1162 "Open invalid alert -> title"), filename]]; 1163 [alert setInformativeText: 1164 NSLocalizedString(@"The torrent file cannot be opened because it contains invalid data.", 1165 "Open invalid alert -> message")]; 1166 [alert setAlertStyle: NSWarningAlertStyle]; 1167 [alert addButtonWithTitle: NSLocalizedString(@"OK", "Open invalid alert -> button")]; 1168 1169 [alert runModal]; 1170 if ([[alert suppressionButton] state] == NSOnState) 1171 [fDefaults setBool: NO forKey: @"WarningInvalidOpen"]; 1172 [alert release]; 1173} 1174 1175- (void) invalidOpenMagnetAlert: (NSString *) address 1176{ 1177 if (![fDefaults boolForKey: @"WarningInvalidOpen"]) 1178 return; 1179 1180 NSAlert * alert = [[NSAlert alloc] init]; 1181 [alert setMessageText: NSLocalizedString(@"Adding magnetized transfer failed.", "Magnet link failed -> title")]; 1182 [alert setInformativeText: [NSString stringWithFormat: NSLocalizedString(@"There was an error when adding the magnet link \"%@\"." 1183 " The transfer will not occur.", "Magnet link failed -> message"), address]]; 1184 [alert setAlertStyle: NSWarningAlertStyle]; 1185 [alert addButtonWithTitle: NSLocalizedString(@"OK", "Magnet link failed -> button")]; 1186 1187 [alert runModal]; 1188 if ([[alert suppressionButton] state] == NSOnState) 1189 [fDefaults setBool: NO forKey: @"WarningInvalidOpen"]; 1190 [alert release]; 1191} 1192 1193- (void) duplicateOpenAlert: (NSString *) name 1194{ 1195 if (![fDefaults boolForKey: @"WarningDuplicate"]) 1196 return; 1197 1198 NSAlert * alert = [[NSAlert alloc] init]; 1199 [alert setMessageText: [NSString stringWithFormat: NSLocalizedString(@"A transfer of \"%@\" already exists.", 1200 "Open duplicate alert -> title"), name]]; 1201 [alert setInformativeText: 1202 NSLocalizedString(@"The transfer cannot be added because it is a duplicate of an already existing transfer.", 1203 "Open duplicate alert -> message")]; 1204 [alert setAlertStyle: NSWarningAlertStyle]; 1205 [alert addButtonWithTitle: NSLocalizedString(@"OK", "Open duplicate alert -> button")]; 1206 [alert setShowsSuppressionButton: YES]; 1207 1208 [alert runModal]; 1209 if ([[alert suppressionButton] state]) 1210 [fDefaults setBool: NO forKey: @"WarningDuplicate"]; 1211 [alert release]; 1212} 1213 1214- (void) duplicateOpenMagnetAlert: (NSString *) address transferName: (NSString *) name 1215{ 1216 if (![fDefaults boolForKey: @"WarningDuplicate"]) 1217 return; 1218 1219 NSAlert * alert = [[NSAlert alloc] init]; 1220 if (name) 1221 [alert setMessageText: [NSString stringWithFormat: NSLocalizedString(@"A transfer of \"%@\" already exists.", 1222 "Open duplicate magnet alert -> title"), name]]; 1223 else 1224 [alert setMessageText: NSLocalizedString(@"Magnet link is a duplicate of an existing transfer.", 1225 "Open duplicate magnet alert -> title")]; 1226 [alert setInformativeText: [NSString stringWithFormat: 1227 NSLocalizedString(@"The magnet link \"%@\" cannot be added because it is a duplicate of an already existing transfer.", 1228 "Open duplicate magnet alert -> message"), address]]; 1229 [alert setAlertStyle: NSWarningAlertStyle]; 1230 [alert addButtonWithTitle: NSLocalizedString(@"OK", "Open duplicate magnet alert -> button")]; 1231 [alert setShowsSuppressionButton: YES]; 1232 1233 [alert runModal]; 1234 if ([[alert suppressionButton] state]) 1235 [fDefaults setBool: NO forKey: @"WarningDuplicate"]; 1236 [alert release]; 1237} 1238 1239- (void) openURL: (NSString *) urlString 1240{ 1241 if ([urlString rangeOfString: @"magnet:" options: (NSAnchoredSearch | NSCaseInsensitiveSearch)].location != NSNotFound) 1242 [self openMagnet: urlString]; 1243 else 1244 { 1245 if ([urlString rangeOfString: @"://"].location == NSNotFound) 1246 { 1247 if ([urlString rangeOfString: @"."].location == NSNotFound) 1248 { 1249 NSInteger beforeCom; 1250 if ((beforeCom = [urlString rangeOfString: @"/"].location) != NSNotFound) 1251 urlString = [NSString stringWithFormat: @"http://www.%@.com/%@", 1252 [urlString substringToIndex: beforeCom], 1253 [urlString substringFromIndex: beforeCom + 1]]; 1254 else 1255 urlString = [NSString stringWithFormat: @"http://www.%@.com/", urlString]; 1256 } 1257 else 1258 urlString = [@"http://" stringByAppendingString: urlString]; 1259 } 1260 1261 NSURLRequest * request = [NSURLRequest requestWithURL: [NSURL URLWithString: urlString] 1262 cachePolicy: NSURLRequestReloadIgnoringLocalAndRemoteCacheData timeoutInterval: 60]; 1263 1264 if ([fPendingTorrentDownloads objectForKey: [request URL]]) 1265 { 1266 NSLog(@"Already downloading %@", [request URL]); 1267 return; 1268 } 1269 1270 NSURLDownload * download = [[NSURLDownload alloc] initWithRequest: request delegate: self]; 1271 1272 if (!fPendingTorrentDownloads) 1273 fPendingTorrentDownloads = [[NSMutableDictionary alloc] init]; 1274 [fPendingTorrentDownloads setObject: [NSMutableDictionary dictionaryWithObject: download forKey: @"Download"] forKey: [request URL]]; 1275 } 1276} 1277 1278- (void) openURLShowSheet: (id) sender 1279{ 1280 if (!fUrlSheetController) 1281 { 1282 fUrlSheetController = [[URLSheetWindowController alloc] initWithController: self]; 1283 1284 [NSApp beginSheet: [fUrlSheetController window] modalForWindow: fWindow modalDelegate: self didEndSelector: @selector(urlSheetDidEnd:returnCode:contextInfo:) contextInfo: nil]; 1285 } 1286} 1287 1288- (void) urlSheetDidEnd: (NSWindow *) sheet returnCode: (NSInteger) returnCode contextInfo: (void *) contextInfo 1289{ 1290 if (returnCode == 1) 1291 { 1292 NSString * urlString = [fUrlSheetController urlString]; 1293 [self performSelectorOnMainThread: @selector(openURL:) withObject: urlString waitUntilDone: NO]; 1294 } 1295 1296 [fUrlSheetController release]; 1297 fUrlSheetController = nil; 1298} 1299 1300- (void) createFile: (id) sender 1301{ 1302 [CreatorWindowController createTorrentFile: fLib]; 1303} 1304 1305- (void) resumeSelectedTorrents: (id) sender 1306{ 1307 [self resumeTorrents: [fTableView selectedTorrents]]; 1308} 1309 1310- (void) resumeAllTorrents: (id) sender 1311{ 1312 NSMutableArray * torrents = [NSMutableArray arrayWithCapacity: [fTorrents count]]; 1313 1314 for (Torrent * torrent in fTorrents) 1315 if (![torrent isFinishedSeeding]) 1316 [torrents addObject: torrent]; 1317 1318 [self resumeTorrents: torrents]; 1319} 1320 1321- (void) resumeTorrents: (NSArray *) torrents 1322{ 1323 for (Torrent * torrent in torrents) 1324 [torrent startTransfer]; 1325 1326 [self fullUpdateUI]; 1327} 1328 1329- (void) resumeSelectedTorrentsNoWait: (id) sender 1330{ 1331 [self resumeTorrentsNoWait: [fTableView selectedTorrents]]; 1332} 1333 1334- (void) resumeWaitingTorrents: (id) sender 1335{ 1336 NSMutableArray * torrents = [NSMutableArray arrayWithCapacity: [fTorrents count]]; 1337 1338 for (Torrent * torrent in fTorrents) 1339 if ([torrent waitingToStart]) 1340 [torrents addObject: torrent]; 1341 1342 [self resumeTorrentsNoWait: torrents]; 1343} 1344 1345- (void) resumeTorrentsNoWait: (NSArray *) torrents 1346{ 1347 //iterate through instead of all at once to ensure no conflicts 1348 for (Torrent * torrent in torrents) 1349 [torrent startTransferNoQueue]; 1350 1351 [self fullUpdateUI]; 1352} 1353 1354- (void) stopSelectedTorrents: (id) sender 1355{ 1356 [self stopTorrents: [fTableView selectedTorrents]]; 1357} 1358 1359- (void) stopAllTorrents: (id) sender 1360{ 1361 [self stopTorrents: fTorrents]; 1362} 1363 1364- (void) stopTorrents: (NSArray *) torrents 1365{ 1366 //don't want any of these starting then stopping 1367 for (Torrent * torrent in torrents) 1368 if ([torrent waitingToStart]) 1369 [torrent stopTransfer]; 1370 1371 for (Torrent * torrent in torrents) 1372 [torrent stopTransfer]; 1373 1374 [self fullUpdateUI]; 1375} 1376 1377- (void) removeTorrents: (NSArray *) torrents deleteData: (BOOL) deleteData 1378{ 1379 [torrents retain]; 1380 1381 if ([fDefaults boolForKey: @"CheckRemove"]) 1382 { 1383 NSUInteger active = 0, downloading = 0; 1384 for (Torrent * torrent in torrents) 1385 if ([torrent isActive]) 1386 { 1387 ++active; 1388 if (![torrent isSeeding]) 1389 ++downloading; 1390 } 1391 1392 if ([fDefaults boolForKey: @"CheckRemoveDownloading"] ? downloading > 0 : active > 0) 1393 { 1394 NSDictionary * dict = [[NSDictionary alloc] initWithObjectsAndKeys: 1395 torrents, @"Torrents", 1396 [NSNumber numberWithBool: deleteData], @"DeleteData", nil]; 1397 1398 NSString * title, * message; 1399 1400 const NSInteger selected = [torrents count]; 1401 if (selected == 1) 1402 { 1403 NSString * torrentName = [(Torrent *)[torrents objectAtIndex: 0] name]; 1404 1405 if (deleteData) 1406 title = [NSString stringWithFormat: 1407 NSLocalizedString(@"Are you sure you want to remove \"%@\" from the transfer list" 1408 " and trash the data file?", "Removal confirm panel -> title"), torrentName]; 1409 else 1410 title = [NSString stringWithFormat: 1411 NSLocalizedString(@"Are you sure you want to remove \"%@\" from the transfer list?", 1412 "Removal confirm panel -> title"), torrentName]; 1413 1414 message = NSLocalizedString(@"This transfer is active." 1415 " Once removed, continuing the transfer will require the torrent file or magnet link.", 1416 "Removal confirm panel -> message"); 1417 } 1418 else 1419 { 1420 if (deleteData) 1421 title = [NSString stringWithFormat: 1422 NSLocalizedString(@"Are you sure you want to remove %@ transfers from the transfer list" 1423 " and trash the data files?", "Removal confirm panel -> title"), [NSString formattedUInteger: selected]]; 1424 else 1425 title = [NSString stringWithFormat: 1426 NSLocalizedString(@"Are you sure you want to remove %@ transfers from the transfer list?", 1427 "Removal confirm panel -> title"), [NSString formattedUInteger: selected]]; 1428 1429 if (selected == active) 1430 message = [NSString stringWithFormat: NSLocalizedString(@"There are %@ active transfers.", 1431 "Removal confirm panel -> message part 1"), [NSString formattedUInteger: active]]; 1432 else 1433 message = [NSString stringWithFormat: NSLocalizedString(@"There are %@ transfers (%@ active).", 1434 "Removal confirm panel -> message part 1"), [NSString formattedUInteger: selected], [NSString formattedUInteger: active]]; 1435 message = [message stringByAppendingFormat: @" %@", 1436 NSLocalizedString(@"Once removed, continuing the transfers will require the torrent files or magnet links.", 1437 "Removal confirm panel -> message part 2")]; 1438 } 1439 1440 NSBeginAlertSheet(title, NSLocalizedString(@"Remove", "Removal confirm panel -> button"), 1441 NSLocalizedString(@"Cancel", "Removal confirm panel -> button"), nil, fWindow, self, 1442 nil, @selector(removeSheetDidEnd:returnCode:contextInfo:), dict, @"%@", message); 1443 return; 1444 } 1445 } 1446 1447 [self confirmRemoveTorrents: torrents deleteData: deleteData]; 1448} 1449 1450- (void) removeSheetDidEnd: (NSWindow *) sheet returnCode: (NSInteger) returnCode contextInfo: (NSDictionary *) dict 1451{ 1452 NSArray * torrents = [dict objectForKey: @"Torrents"]; 1453 if (returnCode == NSAlertDefaultReturn) 1454 [self confirmRemoveTorrents: [torrents retain] deleteData: [[dict objectForKey: @"DeleteData"] boolValue]]; 1455 1456 [torrents release]; 1457 [dict release]; 1458} 1459 1460- (void) confirmRemoveTorrents: (NSArray *) torrents deleteData: (BOOL) deleteData 1461{ 1462 NSMutableArray * selectedValues = nil; 1463 if (![NSApp isOnLionOrBetter]) 1464 { 1465 selectedValues = [NSMutableArray arrayWithArray: [fTableView selectedValues]]; 1466 [selectedValues removeObjectsInArray: torrents]; 1467 } 1468 1469 //miscellaneous 1470 for (Torrent * torrent in torrents) 1471 { 1472 //don't want any of these starting then stopping 1473 if ([torrent waitingToStart]) 1474 [torrent stopTransfer]; 1475 1476 //let's expand all groups that have removed items - they either don't exist anymore, are already expanded, or are collapsed (rpc) 1477 [fTableView removeCollapsedGroup: [torrent groupValue]]; 1478 1479 //we can't assume the window is active - RPC removal, for example 1480 [fBadger removeTorrent: torrent]; 1481 } 1482 1483 [fTorrents removeObjectsInArray: torrents]; 1484 1485 //set up helpers to remove from the table 1486 __block BOOL beganUpdate = NO; 1487 1488 void (^doTableRemoval)(NSMutableArray *, id) = ^(NSMutableArray * displayedTorrents, id parent) { 1489 NSIndexSet * indexes = [displayedTorrents indexesOfObjectsWithOptions: NSEnumerationConcurrent passingTest: ^(id obj, NSUInteger idx, BOOL * stop) { 1490 return [torrents containsObject: obj]; 1491 }]; 1492 1493 if ([indexes count] > 0) 1494 { 1495 if ([NSApp isOnLionOrBetter]) 1496 { 1497 if (!beganUpdate) 1498 { 1499 [NSAnimationContext beginGrouping]; //this has to be before we set the completion handler (#4874) 1500 1501 //we can't closeRemoveTorrent: until it's no longer in the GUI at all 1502 [[NSAnimationContext currentContext] setCompletionHandler: ^{ 1503 for (Torrent * torrent in torrents) 1504 [torrent closeRemoveTorrent: deleteData]; 1505 }]; 1506 1507 [fTableView beginUpdates]; 1508 beganUpdate = YES; 1509 } 1510 1511 [fTableView removeItemsAtIndexes: indexes inParent: parent withAnimation: NSTableViewAnimationSlideLeft]; 1512 } 1513 [displayedTorrents removeObjectsAtIndexes: indexes]; 1514 } 1515 }; 1516 1517 //if not removed from the displayed torrents here, fullUpdateUI might cause a crash 1518 if ([fDisplayedTorrents count] > 0) 1519 { 1520 if ([[fDisplayedTorrents objectAtIndex: 0] isKindOfClass: [TorrentGroup class]]) 1521 { 1522 for (TorrentGroup * group in fDisplayedTorrents) 1523 doTableRemoval([group torrents], group); 1524 } 1525 else 1526 doTableRemoval(fDisplayedTorrents, nil); 1527 1528 if (beganUpdate) 1529 { 1530 [fTableView endUpdates]; 1531 [NSAnimationContext endGrouping]; 1532 } 1533 } 1534 1535 if (!beganUpdate) 1536 { 1537 //do here if we're not doing it at the end of the animation 1538 for (Torrent * torrent in torrents) 1539 [torrent closeRemoveTorrent: deleteData]; 1540 } 1541 1542 if (selectedValues) 1543 [fTableView selectValues: selectedValues]; 1544 1545 [self fullUpdateUI]; 1546 1547 #warning why do we need them retained? 1548 [torrents autorelease]; 1549} 1550 1551- (void) removeNoDelete: (id) sender 1552{ 1553 [self removeTorrents: [fTableView selectedTorrents] deleteData: NO]; 1554} 1555 1556- (void) removeDeleteData: (id) sender 1557{ 1558 [self removeTorrents: [fTableView selectedTorrents] deleteData: YES]; 1559} 1560 1561- (void) clearCompleted: (id) sender 1562{ 1563 NSMutableArray * torrents = [[NSMutableArray alloc] init]; 1564 1565 for (Torrent * torrent in fTorrents) 1566 if ([torrent isFinishedSeeding]) 1567 [torrents addObject: torrent]; 1568 1569 if ([fDefaults boolForKey: @"WarningRemoveCompleted"]) 1570 { 1571 NSString * message, * info; 1572 if ([torrents count] == 1) 1573 { 1574 NSString * torrentName = [(Torrent *)[torrents objectAtIndex: 0] name]; 1575 message = [NSString stringWithFormat: NSLocalizedString(@"Are you sure you want to remove \"%@\" from the transfer list?", 1576 "Remove completed confirm panel -> title"), torrentName]; 1577 1578 info = NSLocalizedString(@"Once removed, continuing the transfer will require the torrent file or magnet link.", 1579 "Remove completed confirm panel -> message"); 1580 } 1581 else 1582 { 1583 message = [NSString stringWithFormat: NSLocalizedString(@"Are you sure you want to remove %@ completed transfers from the transfer list?", 1584 "Remove completed confirm panel -> title"), [NSString formattedUInteger: [torrents count]]]; 1585 1586 info = NSLocalizedString(@"Once removed, continuing the transfers will require the torrent files or magnet links.", 1587 "Remove completed confirm panel -> message"); 1588 } 1589 1590 NSAlert * alert = [[[NSAlert alloc] init] autorelease]; 1591 [alert setMessageText: message]; 1592 [alert setInformativeText: info]; 1593 [alert setAlertStyle: NSWarningAlertStyle]; 1594 [alert addButtonWithTitle: NSLocalizedString(@"Remove", "Remove completed confirm panel -> button")]; 1595 [alert addButtonWithTitle: NSLocalizedString(@"Cancel", "Remove completed confirm panel -> button")]; 1596 [alert setShowsSuppressionButton: YES]; 1597 1598 const NSInteger returnCode = [alert runModal]; 1599 if ([[alert suppressionButton] state]) 1600 [fDefaults setBool: NO forKey: @"WarningRemoveCompleted"]; 1601 1602 if (returnCode != NSAlertFirstButtonReturn) 1603 { 1604 [torrents release]; 1605 return; 1606 } 1607 } 1608 1609 [self confirmRemoveTorrents: torrents deleteData: NO]; 1610} 1611 1612- (void) moveDataFilesSelected: (id) sender 1613{ 1614 [self moveDataFiles: [fTableView selectedTorrents]]; 1615} 1616 1617- (void) moveDataFiles: (NSArray *) torrents 1618{ 1619 NSOpenPanel * panel = [NSOpenPanel openPanel]; 1620 [panel setPrompt: NSLocalizedString(@"Select", "Move torrent -> prompt")]; 1621 [panel setAllowsMultipleSelection: NO]; 1622 [panel setCanChooseFiles: NO]; 1623 [panel setCanChooseDirectories: YES]; 1624 [panel setCanCreateDirectories: YES]; 1625 1626 NSInteger count = [torrents count]; 1627 if (count == 1) 1628 [panel setMessage: [NSString stringWithFormat: NSLocalizedString(@"Select the new folder for \"%@\".", 1629 "Move torrent -> select destination folder"), [(Torrent *)[torrents objectAtIndex: 0] name]]]; 1630 else 1631 [panel setMessage: [NSString stringWithFormat: NSLocalizedString(@"Select the new folder for %d data files.", 1632 "Move torrent -> select destination folder"), count]]; 1633 1634 [panel beginSheetModalForWindow: fWindow completionHandler: ^(NSInteger result) { 1635 if (result == NSFileHandlingPanelOKButton) 1636 { 1637 for (Torrent * torrent in torrents) 1638 [torrent moveTorrentDataFileTo: [[[panel URLs] objectAtIndex: 0] path]]; 1639 } 1640 }]; 1641} 1642 1643- (void) copyTorrentFiles: (id) sender 1644{ 1645 [self copyTorrentFileForTorrents: [[NSMutableArray alloc] initWithArray: [fTableView selectedTorrents]]]; 1646} 1647 1648- (void) copyTorrentFileForTorrents: (NSMutableArray *) torrents 1649{ 1650 if ([torrents count] == 0) 1651 { 1652 [torrents release]; 1653 return; 1654 } 1655 1656 Torrent * torrent = [torrents objectAtIndex: 0]; 1657 1658 if (![torrent isMagnet] && [[NSFileManager defaultManager] fileExistsAtPath: [torrent torrentLocation]]) 1659 { 1660 NSSavePanel * panel = [NSSavePanel savePanel]; 1661 [panel setAllowedFileTypes: [NSArray arrayWithObjects: @"org.bittorrent.torrent", @"torrent", nil]]; 1662 [panel setExtensionHidden: NO]; 1663 1664 [panel setNameFieldStringValue: [torrent name]]; 1665 1666 [panel beginSheetModalForWindow: fWindow completionHandler: ^(NSInteger result) { 1667 //copy torrent to new location with name of data file 1668 if (result == NSFileHandlingPanelOKButton) 1669 [torrent copyTorrentFileTo: [[panel URL] path]]; 1670 1671 [torrents removeObjectAtIndex: 0]; 1672 [self performSelectorOnMainThread: @selector(copyTorrentFileForTorrents:) withObject: torrents waitUntilDone: NO]; 1673 }]; 1674 } 1675 else 1676 { 1677 if (![torrent isMagnet]) 1678 { 1679 NSAlert * alert = [[NSAlert alloc] init]; 1680 [alert addButtonWithTitle: NSLocalizedString(@"OK", "Torrent file copy alert -> button")]; 1681 [alert setMessageText: [NSString stringWithFormat: NSLocalizedString(@"Copy of \"%@\" Cannot Be Created", 1682 "Torrent file copy alert -> title"), [torrent name]]]; 1683 [alert setInformativeText: [NSString stringWithFormat: 1684 NSLocalizedString(@"The torrent file (%@) cannot be found.", "Torrent file copy alert -> message"), 1685 [torrent torrentLocation]]]; 1686 [alert setAlertStyle: NSWarningAlertStyle]; 1687 1688 [alert runModal]; 1689 [alert release]; 1690 } 1691 1692 [torrents removeObjectAtIndex: 0]; 1693 [self copyTorrentFileForTorrents: torrents]; 1694 } 1695} 1696 1697- (void) copyMagnetLinks: (id) sender 1698{ 1699 NSArray * torrents = [fTableView selectedTorrents]; 1700 1701 if ([torrents count] <= 0) 1702 return; 1703 1704 NSMutableArray * links = [NSMutableArray arrayWithCapacity: [torrents count]]; 1705 for (Torrent * torrent in torrents) 1706 [links addObject: [torrent magnetLink]]; 1707 1708 NSString * text = [links componentsJoinedByString: @"\n"]; 1709 1710 NSPasteboard * pb = [NSPasteboard generalPasteboard]; 1711 [pb clearContents]; 1712 [pb writeObjects: [NSArray arrayWithObject: text]]; 1713} 1714 1715- (void) revealFile: (id) sender 1716{ 1717 NSArray * selected = [fTableView selectedTorrents]; 1718 NSMutableArray * paths = [NSMutableArray arrayWithCapacity: [selected count]]; 1719 for (Torrent * torrent in selected) 1720 { 1721 NSString * location = [torrent dataLocation]; 1722 if (location) 1723 [paths addObject: [NSURL fileURLWithPath: location]]; 1724 } 1725 1726 if ([paths count] > 0) 1727 [[NSWorkspace sharedWorkspace] activateFileViewerSelectingURLs: paths]; 1728} 1729 1730- (void) announceSelectedTorrents: (id) sender 1731{ 1732 for (Torrent * torrent in [fTableView selectedTorrents]) 1733 { 1734 if ([torrent canManualAnnounce]) 1735 [torrent manualAnnounce]; 1736 } 1737} 1738 1739- (void) verifySelectedTorrents: (id) sender 1740{ 1741 [self verifyTorrents: [fTableView selectedTorrents]]; 1742} 1743 1744- (void) verifyTorrents: (NSArray *) torrents 1745{ 1746 for (Torrent * torrent in torrents) 1747 [torrent resetCache]; 1748 1749 [self applyFilter]; 1750} 1751 1752- (void) showPreferenceWindow: (id) sender 1753{ 1754 NSWindow * window = [fPrefsController window]; 1755 if (![window isVisible]) 1756 [window center]; 1757 1758 [window makeKeyAndOrderFront: nil]; 1759} 1760 1761- (void) showAboutWindow: (id) sender 1762{ 1763 [[AboutWindowController aboutController] showWindow: nil]; 1764} 1765 1766- (void) showInfo: (id) sender 1767{ 1768 if ([[fInfoController window] isVisible]) 1769 [fInfoController close]; 1770 else 1771 { 1772 [fInfoController updateInfoStats]; 1773 [[fInfoController window] orderFront: nil]; 1774 1775 if ([fInfoController canQuickLook] && [QLPreviewPanel sharedPreviewPanelExists] && [[QLPreviewPanel sharedPreviewPanel] isVisible]) 1776 [[QLPreviewPanel sharedPreviewPanel] reloadData]; 1777 } 1778 1779 [[fWindow toolbar] validateVisibleItems]; 1780} 1781 1782- (void) resetInfo 1783{ 1784 [fInfoController setInfoForTorrents: [fTableView selectedTorrents]]; 1785 1786 if ([QLPreviewPanel sharedPreviewPanelExists] && [[QLPreviewPanel sharedPreviewPanel] isVisible]) 1787 [[QLPreviewPanel sharedPreviewPanel] reloadData]; 1788} 1789 1790- (void) setInfoTab: (id) sender 1791{ 1792 if (sender == fNextInfoTabItem) 1793 [fInfoController setNextTab]; 1794 else 1795 [fInfoController setPreviousTab]; 1796} 1797 1798- (MessageWindowController *) messageWindowController 1799{ 1800 if (!fMessageController) 1801 fMessageController = [[MessageWindowController alloc] init]; 1802 1803 return fMessageController; 1804} 1805 1806- (void) showMessageWindow: (id) sender 1807{ 1808 [[self messageWindowController] showWindow: nil]; 1809} 1810 1811- (void) showStatsWindow: (id) sender 1812{ 1813 [[StatsWindowController statsWindow] showWindow: nil]; 1814} 1815 1816- (void) updateUI 1817{ 1818 CGFloat dlRate = 0.0, ulRate = 0.0; 1819 BOOL anyCompleted = NO; 1820 for (Torrent * torrent in fTorrents) 1821 { 1822 [torrent update]; 1823 1824 //pull the upload and download speeds - most consistent by using current stats 1825 dlRate += [torrent downloadRate]; 1826 ulRate += [torrent uploadRate]; 1827 1828 anyCompleted |= [torrent isFinishedSeeding]; 1829 } 1830 1831 if (![NSApp isHidden]) 1832 { 1833 if ([fWindow isVisible]) 1834 { 1835 [self sortTorrents: NO]; 1836 1837 [fStatusBar updateWithDownload: dlRate upload: ulRate]; 1838 1839 [fClearCompletedButton setHidden: !anyCompleted]; 1840 } 1841 1842 //update non-constant parts of info window 1843 if ([[fInfoController window] isVisible]) 1844 [fInfoController updateInfoStats]; 1845 } 1846 1847 //badge dock 1848 [fBadger updateBadgeWithDownload: dlRate upload: ulRate]; 1849} 1850 1851#warning can this be removed or refined? 1852- (void) fullUpdateUI 1853{ 1854 [self updateUI]; 1855 [self applyFilter]; 1856 [[fWindow toolbar] validateVisibleItems]; 1857 [self updateTorrentHistory]; 1858} 1859 1860- (void) setBottomCountText: (BOOL) filtering 1861{ 1862 NSString * totalTorrentsString; 1863 NSUInteger totalCount = [fTorrents count]; 1864 if (totalCount != 1) 1865 totalTorrentsString = [NSString stringWithFormat: NSLocalizedString(@"%@ transfers", "Status bar transfer count"), 1866 [NSString formattedUInteger: totalCount]]; 1867 else 1868 totalTorrentsString = NSLocalizedString(@"1 transfer", "Status bar transfer count"); 1869 1870 if (filtering) 1871 { 1872 NSUInteger count = [fTableView numberOfRows]; //have to factor in collapsed rows 1873 if (count > 0 && ![[fDisplayedTorrents objectAtIndex: 0] isKindOfClass: [Torrent class]]) 1874 count -= [fDisplayedTorrents count]; 1875 1876 totalTorrentsString = [NSString stringWithFormat: NSLocalizedString(@"%@ of %@", "Status bar transfer count"), 1877 [NSString formattedUInteger: count], totalTorrentsString]; 1878 } 1879 1880 [fTotalTorrentsField setStringValue: totalTorrentsString]; 1881} 1882 1883- (BOOL) userNotificationCenter: (NSUserNotificationCenter *) center shouldPresentNotification:(NSUserNotification *) notification 1884{ 1885 return YES; 1886} 1887 1888- (void) userNotificationCenter: (NSUserNotificationCenter *) center didActivateNotification: (NSUserNotification *) notification 1889{ 1890 if (![notification userInfo]) 1891 return; 1892 1893 if ([notification activationType] == NSUserNotificationActivationTypeActionButtonClicked) //reveal 1894 { 1895 Torrent * torrent = [self torrentForHash: [[notification userInfo] objectForKey: @"Hash"]]; 1896 NSString * location = [torrent dataLocation]; 1897 if (!location) 1898 location = [[notification userInfo] objectForKey: @"Location"]; 1899 if (location) 1900 [[NSWorkspace sharedWorkspace] activateFileViewerSelectingURLs: @[[NSURL fileURLWithPath: location]]]; 1901 } 1902 else if ([notification activationType] == NSUserNotificationActivationTypeContentsClicked) 1903 { 1904 Torrent * torrent = [self torrentForHash: [[notification userInfo] objectForKey: @"Hash"]]; 1905 if (torrent) 1906 { 1907 //select in the table - first see if it's already shown 1908 NSInteger row = [fTableView rowForItem: torrent]; 1909 if (row == -1) 1910 { 1911 //if it's not shown, see if it's in a collapsed row 1912 if ([fDefaults boolForKey: @"SortByGroup"]) 1913 { 1914 __block TorrentGroup * parent = nil; 1915 [fDisplayedTorrents enumerateObjectsWithOptions: NSEnumerationConcurrent usingBlock: ^(TorrentGroup * group, NSUInteger idx, BOOL *stop) { 1916 if ([[group torrents] containsObject: torrent]) 1917 { 1918 parent = group; 1919 *stop = YES; 1920 } 1921 }]; 1922 if (parent) 1923 { 1924 [[fTableView animator] expandItem: parent]; 1925 row = [fTableView rowForItem: torrent]; 1926 } 1927 } 1928 1929 if (row == -1) 1930 { 1931 //not found - must be filtering 1932 NSAssert([fDefaults boolForKey: @"FilterBar"], @"expected the filter to be enabled"); 1933 [fFilterBar reset: YES]; 1934 1935 row = [fTableView rowForItem: torrent]; 1936 1937 //if it's not shown, it has to be in a collapsed row...again 1938 if ([fDefaults boolForKey: @"SortByGroup"]) 1939 { 1940 __block TorrentGroup * parent = nil; 1941 [fDisplayedTorrents enumerateObjectsWithOptions: NSEnumerationConcurrent usingBlock: ^(TorrentGroup * group, NSUInteger idx, BOOL *stop) { 1942 if ([[group torrents] containsObject: torrent]) 1943 { 1944 parent = group; 1945 *stop = YES; 1946 } 1947 }]; 1948 if (parent) 1949 { 1950 [[fTableView animator] expandItem: parent]; 1951 row = [fTableView rowForItem: torrent]; 1952 } 1953 } 1954 } 1955 } 1956 1957 NSAssert1(row != -1, @"expected a row to be found for torrent %@", torrent); 1958 [fTableView selectRowIndexes: [NSIndexSet indexSetWithIndex: row] byExtendingSelection:NO]; 1959 #warning focus the window 1960 } 1961 } 1962} 1963 1964- (Torrent *) torrentForHash: (NSString *) hash 1965{ 1966 NSParameterAssert(hash != nil); 1967 1968 __block Torrent * torrent = nil; 1969 [fTorrents enumerateObjectsWithOptions: NSEnumerationConcurrent usingBlock: ^(id obj, NSUInteger idx, BOOL * stop) { 1970 if ([[(Torrent *)obj hashString] isEqualToString: hash]) 1971 { 1972 torrent = obj; 1973 *stop = YES; 1974 } 1975 }]; 1976 return torrent; 1977} 1978 1979- (void) torrentFinishedDownloading: (NSNotification *) notification 1980{ 1981 Torrent * torrent = [notification object]; 1982 1983 if ([[[notification userInfo] objectForKey: @"WasRunning"] boolValue]) 1984 { 1985 if (!fSoundPlaying && [fDefaults boolForKey: @"PlayDownloadSound"]) 1986 { 1987 NSSound * sound; 1988 if ((sound = [NSSound soundNamed: [fDefaults stringForKey: @"DownloadSound"]])) 1989 { 1990 [sound setDelegate: self]; 1991 fSoundPlaying = YES; 1992 [sound play]; 1993 } 1994 } 1995 1996 NSString * location = [torrent dataLocation]; 1997 1998 NSString * notificationTitle = NSLocalizedString(@"Download Complete", "notification title"); 1999 if ([NSApp isOnMountainLionOrBetter]) 2000 { 2001 NSUserNotification * notification = [[NSUserNotificationMtLion alloc] init]; 2002 [notification setTitle: notificationTitle]; 2003 [notification setInformativeText: [torrent name]]; 2004 2005 [notification setHasActionButton: YES]; 2006 [notification setActionButtonTitle: NSLocalizedString(@"Show", "notification button")]; 2007 2008 NSMutableDictionary * userInfo = [NSMutableDictionary dictionaryWithObject: [torrent hashString] forKey: @"Hash"]; 2009 if (location) 2010 [userInfo setObject: location forKey: @"Location"]; 2011 [notification setUserInfo: userInfo]; 2012 2013 [[NSUserNotificationCenterMtLion defaultUserNotificationCenter] deliverNotification: notification]; 2014 [notification release]; 2015 } 2016 2017 NSMutableDictionary * clickContext = [NSMutableDictionary dictionaryWithObjectsAndKeys: 2018 GROWL_DOWNLOAD_COMPLETE, @"Type", nil]; 2019 2020 if (location) 2021 [clickContext setObject: location forKey: @"Location"]; 2022 2023 [GrowlApplicationBridge notifyWithTitle: notificationTitle 2024 description: [torrent name] notificationName: GROWL_DOWNLOAD_COMPLETE 2025 iconData: nil priority: 0 isSticky: NO clickContext: clickContext]; 2026 2027 //NSLog(@"delegate: %@", [[NSUserNotificationCenterMtLion defaultUserNotificationCenter] delegate]); 2028 2029 if (![fWindow isMainWindow]) 2030 [fBadger addCompletedTorrent: torrent]; 2031 2032 //bounce download stack 2033 [[NSDistributedNotificationCenter defaultCenter] postNotificationName: @"com.apple.DownloadFileFinished" 2034 object: [torrent dataLocation]]; 2035 } 2036 2037 [self fullUpdateUI]; 2038} 2039 2040- (void) torrentRestartedDownloading: (NSNotification *) notification 2041{ 2042 [self fullUpdateUI]; 2043} 2044 2045- (void) torrentFinishedSeeding: (NSNotification *) notification 2046{ 2047 Torrent * torrent = [[notification object] retain]; 2048 2049 if (!fSoundPlaying && [fDefaults boolForKey: @"PlaySeedingSound"]) 2050 { 2051 NSSound * sound; 2052 if ((sound = [NSSound soundNamed: [fDefaults stringForKey: @"SeedingSound"]])) 2053 { 2054 [sound setDelegate: self]; 2055 fSoundPlaying = YES; 2056 [sound play]; 2057 } 2058 } 2059 2060 NSString * location = [torrent dataLocation]; 2061 2062 NSString * notificationTitle = NSLocalizedString(@"Seeding Complete", "notification title"); 2063 if ([NSApp isOnMountainLionOrBetter]) 2064 { 2065 NSUserNotification * notification = [[NSUserNotificationMtLion alloc] init]; 2066 [notification setTitle: notificationTitle]; 2067 [notification setInformativeText: [torrent name]]; 2068 2069 [notification setHasActionButton: YES]; 2070 [notification setActionButtonTitle: NSLocalizedString(@"Show", "notification button")]; 2071 2072 NSMutableDictionary * userInfo = [NSMutableDictionary dictionaryWithObject: [torrent hashString] forKey: @"Hash"]; 2073 if (location) 2074 [userInfo setObject: location forKey: @"Location"]; 2075 [notification setUserInfo: userInfo]; 2076 2077 [[NSUserNotificationCenterMtLion defaultUserNotificationCenter] deliverNotification: notification]; 2078 [notification release]; 2079 } 2080 2081 NSMutableDictionary * clickContext = [NSMutableDictionary dictionaryWithObject: GROWL_SEEDING_COMPLETE forKey: @"Type"]; 2082 2083 if (location) 2084 [clickContext setObject: location forKey: @"Location"]; 2085 2086 [GrowlApplicationBridge notifyWithTitle: notificationTitle 2087 description: [torrent name] notificationName: GROWL_SEEDING_COMPLETE 2088 iconData: nil priority: 0 isSticky: NO clickContext: clickContext]; 2089 2090 //removing from the list calls fullUpdateUI 2091 if ([torrent removeWhenFinishSeeding]) 2092 [self confirmRemoveTorrents: [[NSArray arrayWithObject: torrent] retain] deleteData: NO]; 2093 else 2094 { 2095 if (![fWindow isMainWindow]) 2096 [fBadger addCompletedTorrent: torrent]; 2097 2098 [self fullUpdateUI]; 2099 2100 if ([[fTableView selectedTorrents] containsObject: torrent]) 2101 { 2102 [fInfoController updateInfoStats]; 2103 [fInfoController updateOptions]; 2104 } 2105 } 2106 2107 [torrent release]; 2108} 2109 2110- (void) updateTorrentHistory 2111{ 2112 NSMutableArray * history = [NSMutableArray arrayWithCapacity: [fTorrents count]]; 2113 2114 for (Torrent * torrent in fTorrents) 2115 [history addObject: [torrent history]]; 2116 2117 NSString * historyFile = [fConfigDirectory stringByAppendingPathComponent: TRANSFER_PLIST]; 2118 [history writeToFile: historyFile atomically: YES]; 2119} 2120 2121- (void) setSort: (id) sender 2122{ 2123 NSString * sortType; 2124 switch ([(NSMenuItem *)sender tag]) 2125 { 2126 case SORT_ORDER_TAG: 2127 sortType = SORT_ORDER; 2128 [fDefaults setBool: NO forKey: @"SortReverse"]; 2129 break; 2130 case SORT_DATE_TAG: 2131 sortType = SORT_DATE; 2132 break; 2133 case SORT_NAME_TAG: 2134 sortType = SORT_NAME; 2135 break; 2136 case SORT_PROGRESS_TAG: 2137 sortType = SORT_PROGRESS; 2138 break; 2139 case SORT_STATE_TAG: 2140 sortType = SORT_STATE; 2141 break; 2142 case SORT_TRACKER_TAG: 2143 sortType = SORT_TRACKER; 2144 break; 2145 case SORT_ACTIVITY_TAG: 2146 sortType = SORT_ACTIVITY; 2147 break; 2148 case SORT_SIZE_TAG: 2149 sortType = SORT_SIZE; 2150 break; 2151 default: 2152 NSAssert1(NO, @"Unknown sort tag received: %ld", [(NSMenuItem *)sender tag]); 2153 return; 2154 } 2155 2156 [fDefaults setObject: sortType forKey: @"Sort"]; 2157 2158 [self sortTorrents: YES]; 2159} 2160 2161- (void) setSortByGroup: (id) sender 2162{ 2163 BOOL sortByGroup = ![fDefaults boolForKey: @"SortByGroup"]; 2164 [fDefaults setBool: sortByGroup forKey: @"SortByGroup"]; 2165 2166 [self applyFilter]; 2167} 2168 2169- (void) setSortReverse: (id) sender 2170{ 2171 const BOOL setReverse = [(NSMenuItem *)sender tag] == SORT_DESC_TAG; 2172 if (setReverse != [fDefaults boolForKey: @"SortReverse"]) 2173 { 2174 [fDefaults setBool: setReverse forKey: @"SortReverse"]; 2175 [self sortTorrents: NO]; 2176 } 2177} 2178 2179- (void) sortTorrents: (BOOL) includeQueueOrder 2180{ 2181 const BOOL onLion = [NSApp isOnLionOrBetter]; 2182 2183 NSArray * selectedValues; 2184 if (!onLion) 2185 selectedValues = [fTableView selectedValues]; 2186 2187 //actually sort 2188 [self sortTorrentsCallUpdates: YES includeQueueOrder: includeQueueOrder]; 2189 2190 if (!onLion) 2191 [fTableView selectValues: selectedValues]; 2192 2193 [fTableView setNeedsDisplay: YES]; 2194} 2195 2196- (void) sortTorrentsCallUpdates: (BOOL) callUpdates includeQueueOrder: (BOOL) includeQueueOrder 2197{ 2198 const BOOL asc = ![fDefaults boolForKey: @"SortReverse"]; 2199 2200 NSArray * descriptors; 2201 NSSortDescriptor * nameDescriptor = [NSSortDescriptor sortDescriptorWithKey: @"name" ascending: asc selector: @selector(localizedStandardCompare:)]; 2202 2203 NSString * sortType = [fDefaults stringForKey: @"Sort"]; 2204 if ([sortType isEqualToString: SORT_STATE]) 2205 { 2206 NSSortDescriptor * stateDescriptor = [NSSortDescriptor sortDescriptorWithKey: @"stateSortKey" ascending: !asc], 2207 * progressDescriptor = [NSSortDescriptor sortDescriptorWithKey: @"progress" ascending: !asc], 2208 * ratioDescriptor = [NSSortDescriptor sortDescriptorWithKey: @"ratio" ascending: !asc]; 2209 2210 descriptors = [NSArray arrayWithObjects: stateDescriptor, progressDescriptor, ratioDescriptor, nameDescriptor, nil]; 2211 } 2212 else if ([sortType isEqualToString: SORT_PROGRESS]) 2213 { 2214 NSSortDescriptor * progressDescriptor = [NSSortDescriptor sortDescriptorWithKey: @"progress" ascending: asc], 2215 * ratioProgressDescriptor = [NSSortDescriptor sortDescriptorWithKey: @"progressStopRatio" ascending: asc], 2216 * ratioDescriptor = [NSSortDescriptor sortDescriptorWithKey: @"ratio" ascending: asc]; 2217 2218 descriptors = [NSArray arrayWithObjects: progressDescriptor, ratioProgressDescriptor, ratioDescriptor, nameDescriptor, nil]; 2219 } 2220 else if ([sortType isEqualToString: SORT_TRACKER]) 2221 { 2222 NSSortDescriptor * trackerDescriptor = [NSSortDescriptor sortDescriptorWithKey: @"trackerSortKey" ascending: asc selector: @selector(localizedCaseInsensitiveCompare:)]; 2223 2224 descriptors = [NSArray arrayWithObjects: trackerDescriptor, nameDescriptor, nil]; 2225 } 2226 else if ([sortType isEqualToString: SORT_ACTIVITY]) 2227 { 2228 NSSortDescriptor * rateDescriptor = [NSSortDescriptor sortDescriptorWithKey: @"totalRate" ascending: !asc]; 2229 NSSortDescriptor * activityDescriptor = [NSSortDescriptor sortDescriptorWithKey: @"dateActivityOrAdd" ascending: !asc]; 2230 2231 descriptors = [NSArray arrayWithObjects: rateDescriptor, activityDescriptor, nameDescriptor, nil]; 2232 } 2233 else if ([sortType isEqualToString: SORT_DATE]) 2234 { 2235 NSSortDescriptor * dateDescriptor = [NSSortDescriptor sortDescriptorWithKey: @"dateAdded" ascending: asc]; 2236 2237 descriptors = [NSArray arrayWithObjects: dateDescriptor, nameDescriptor, nil]; 2238 } 2239 else if ([sortType isEqualToString: SORT_SIZE]) 2240 { 2241 NSSortDescriptor * sizeDescriptor = [NSSortDescriptor sortDescriptorWithKey: @"size" ascending: asc]; 2242 2243 descriptors = [NSArray arrayWithObjects: sizeDescriptor, nameDescriptor, nil]; 2244 } 2245 else if ([sortType isEqualToString: SORT_NAME]) 2246 { 2247 descriptors = [NSArray arrayWithObject: nameDescriptor]; 2248 } 2249 else 2250 { 2251 NSAssert1([sortType isEqualToString: SORT_ORDER], @"Unknown sort type received: %@", sortType); 2252 2253 if (!includeQueueOrder) 2254 return; 2255 2256 NSSortDescriptor * orderDescriptor = [NSSortDescriptor sortDescriptorWithKey: @"queuePosition" ascending: asc]; 2257 2258 descriptors = [NSArray arrayWithObject: orderDescriptor]; 2259 } 2260 2261 BOOL beganTableUpdate = !callUpdates || ![NSApp isOnLionOrBetter]; 2262 2263 //actually sort 2264 if ([fDefaults boolForKey: @"SortByGroup"]) 2265 { 2266 for (TorrentGroup * group in fDisplayedTorrents) 2267 [self rearrangeTorrentTableArray: [group torrents] forParent: group withSortDescriptors: descriptors beganTableUpdate: &beganTableUpdate]; 2268 } 2269 else 2270 [self rearrangeTorrentTableArray: fDisplayedTorrents forParent: nil withSortDescriptors: descriptors beganTableUpdate: &beganTableUpdate]; 2271 2272 if (beganTableUpdate && callUpdates) 2273 { 2274 if ([NSApp isOnLionOrBetter]) 2275 [fTableView endUpdates]; 2276 else 2277 [fTableView reloadData]; 2278 } 2279} 2280 2281#warning redo so that we search a copy once again (best explained by changing sorting from ascending to descending) 2282- (void) rearrangeTorrentTableArray: (NSMutableArray *) rearrangeArray forParent: parent withSortDescriptors: (NSArray *) descriptors beganTableUpdate: (BOOL *) beganTableUpdate 2283{ 2284 for (NSUInteger currentIndex = 1; currentIndex < [rearrangeArray count]; ++currentIndex) 2285 { 2286 //manually do the sorting in-place 2287 const NSUInteger insertIndex = [rearrangeArray indexOfObject: [rearrangeArray objectAtIndex: currentIndex] inSortedRange: NSMakeRange(0, currentIndex) options: (NSBinarySearchingInsertionIndex | NSBinarySearchingLastEqual) usingComparator: ^NSComparisonResult(id obj1, id obj2) { 2288 for (NSSortDescriptor * descriptor in descriptors) 2289 { 2290 const NSComparisonResult result = [descriptor compareObject: obj1 toObject: obj2]; 2291 if (result != NSOrderedSame) 2292 return result; 2293 } 2294 2295 return NSOrderedSame; 2296 }]; 2297 2298 if (insertIndex != currentIndex) 2299 { 2300 if (!*beganTableUpdate) 2301 { 2302 *beganTableUpdate = YES; 2303 if ([NSApp isOnLionOrBetter]) 2304 [fTableView beginUpdates]; 2305 } 2306 2307 [rearrangeArray moveObjectAtIndex: currentIndex toIndex: insertIndex]; 2308 if ([NSApp isOnLionOrBetter]) 2309 [fTableView moveItemAtIndex: currentIndex inParent: parent toIndex: insertIndex inParent: parent]; 2310 } 2311 } 2312 2313 NSAssert2([rearrangeArray isEqualToArray: [rearrangeArray sortedArrayUsingDescriptors: descriptors]], @"Torrent rearranging didn't work! %@ %@", rearrangeArray, [rearrangeArray sortedArrayUsingDescriptors: descriptors]); 2314} 2315 2316- (void) applyFilter 2317{ 2318 const BOOL onLion = [NSApp isOnLionOrBetter]; 2319 2320 NSArray * selectedValuesSL = nil; 2321 if (!onLion) 2322 selectedValuesSL = [fTableView selectedValues]; 2323 2324 __block NSUInteger active = 0, downloading = 0, seeding = 0, paused = 0; 2325 NSString * filterType = [fDefaults stringForKey: @"Filter"]; 2326 BOOL filterActive = NO, filterDownload = NO, filterSeed = NO, filterPause = NO, filterStatus = YES; 2327 if ([filterType isEqualToString: FILTER_ACTIVE]) 2328 filterActive = YES; 2329 else if ([filterType isEqualToString: FILTER_DOWNLOAD]) 2330 filterDownload = YES; 2331 else if ([filterType isEqualToString: FILTER_SEED]) 2332 filterSeed = YES; 2333 else if ([filterType isEqualToString: FILTER_PAUSE]) 2334 filterPause = YES; 2335 else 2336 filterStatus = NO; 2337 2338 const NSInteger groupFilterValue = [fDefaults integerForKey: @"FilterGroup"]; 2339 const BOOL filterGroup = groupFilterValue != GROUP_FILTER_ALL_TAG; 2340 2341 NSArray * searchStrings = [fFilterBar searchStrings]; 2342 if (searchStrings && [searchStrings count] == 0) 2343 searchStrings = nil; 2344 const BOOL filterTracker = searchStrings && [[fDefaults stringForKey: @"FilterSearchType"] isEqualToString: FILTER_TYPE_TRACKER]; 2345 2346 //filter & get counts of each type 2347 NSIndexSet * indexesOfNonFilteredTorrents = [fTorrents indexesOfObjectsWithOptions: NSEnumerationConcurrent passingTest: ^BOOL(Torrent * torrent, NSUInteger idx, BOOL * stop) { 2348 //check status 2349 if ([torrent isActive] && ![torrent isCheckingWaiting]) 2350 { 2351 const BOOL isActive = ![torrent isStalled]; 2352 if (isActive) 2353 ++active; 2354 2355 if ([torrent isSeeding]) 2356 { 2357 ++seeding; 2358 if (filterStatus && !((filterActive && isActive) || filterSeed)) 2359 return NO; 2360 } 2361 else 2362 { 2363 ++downloading; 2364 if (filterStatus && !((filterActive && isActive) || filterDownload)) 2365 return NO; 2366 } 2367 } 2368 else 2369 { 2370 ++paused; 2371 if (filterStatus && !filterPause) 2372 return NO; 2373 } 2374 2375 //checkGroup 2376 if (filterGroup) 2377 if ([torrent groupValue] != groupFilterValue) 2378 return NO; 2379 2380 //check text field 2381 if (searchStrings) 2382 { 2383 __block BOOL removeTextField = NO; 2384 if (filterTracker) 2385 { 2386 NSArray * trackers = [torrent allTrackersFlat]; 2387 2388 //to count, we need each string in at least 1 tracker 2389 [searchStrings enumerateObjectsWithOptions: NSEnumerationConcurrent usingBlock: ^(id searchString, NSUInteger idx, BOOL * stop) { 2390 __block BOOL found = NO; 2391 [trackers enumerateObjectsWithOptions: NSEnumerationConcurrent usingBlock: ^(id tracker, NSUInteger idx, BOOL * stopTracker) { 2392 if ([tracker rangeOfString: searchString options: (NSCaseInsensitiveSearch | NSDiacriticInsensitiveSearch)].location != NSNotFound) 2393 { 2394 found = YES; 2395 *stopTracker = YES; 2396 } 2397 }]; 2398 if (!found) 2399 { 2400 removeTextField = YES; 2401 *stop = YES; 2402 } 2403 }]; 2404 } 2405 else 2406 { 2407 [searchStrings enumerateObjectsWithOptions: NSEnumerationConcurrent usingBlock: ^(id searchString, NSUInteger idx, BOOL * stop) { 2408 if ([[torrent name] rangeOfString: searchString options: (NSCaseInsensitiveSearch | NSDiacriticInsensitiveSearch)].location == NSNotFound) 2409 { 2410 removeTextField = YES; 2411 *stop = YES; 2412 } 2413 }]; 2414 } 2415 2416 if (removeTextField) 2417 return NO; 2418 } 2419 2420 return YES; 2421 }]; 2422 2423 NSArray * allTorrents = [fTorrents objectsAtIndexes: indexesOfNonFilteredTorrents]; 2424 2425 //set button tooltips 2426 if (fFilterBar) 2427 [fFilterBar setCountAll: [fTorrents count] active: active downloading: downloading seeding: seeding paused: paused]; 2428 2429 //if either the previous or current lists are blank, set its value to the other 2430 const BOOL groupRows = [allTorrents count] > 0 ? [fDefaults boolForKey: @"SortByGroup"] : ([fDisplayedTorrents count] > 0 && [[fDisplayedTorrents objectAtIndex: 0] isKindOfClass: [TorrentGroup class]]); 2431 const BOOL wasGroupRows = [fDisplayedTorrents count] > 0 ? [[fDisplayedTorrents objectAtIndex: 0] isKindOfClass: [TorrentGroup class]] : groupRows; 2432 2433 #warning could probably be merged with later code somehow 2434 //clear display cache for not-shown torrents 2435 if ([fDisplayedTorrents count] > 0) 2436 { 2437 //for each torrent, removes the previous piece info if it's not in allTorrents, and keeps track of which torrents we already found in allTorrents 2438 void (^removePreviousFinishedPieces)(id, NSUInteger, BOOL *) = ^(Torrent * torrent, NSUInteger idx, BOOL * stop) { 2439 //we used to keep track of which torrents we already found in allTorrents, but it wasn't safe fo concurrent enumeration 2440 if (![allTorrents containsObject: torrent]) 2441 [torrent setPreviousFinishedPieces: nil]; 2442 }; 2443 2444 if (wasGroupRows) 2445 [fDisplayedTorrents enumerateObjectsWithOptions: NSEnumerationConcurrent usingBlock: ^(id obj, NSUInteger idx, BOOL * stop) { 2446 [[(TorrentGroup *)obj torrents] enumerateObjectsWithOptions: NSEnumerationConcurrent usingBlock: removePreviousFinishedPieces]; 2447 }]; 2448 else 2449 [fDisplayedTorrents enumerateObjectsWithOptions: NSEnumerationConcurrent usingBlock: removePreviousFinishedPieces]; 2450 } 2451 2452 BOOL beganUpdates = NO; 2453 2454 if (onLion) 2455 { 2456 //don't animate torrents when first launching 2457 static dispatch_once_t onceToken; 2458 dispatch_once(&onceToken, ^{ 2459 [[NSAnimationContext currentContext] setDuration: 0]; 2460 }); 2461 [NSAnimationContext beginGrouping]; 2462 } 2463 2464 //add/remove torrents (and rearrange for groups), one by one 2465 if (!groupRows && !wasGroupRows) 2466 { 2467 NSMutableIndexSet * addIndexes = [NSMutableIndexSet indexSet], 2468 * removePreviousIndexes = [NSMutableIndexSet indexSetWithIndexesInRange: NSMakeRange(0, [fDisplayedTorrents count])]; 2469 2470 //for each of the torrents to add, find if it already exists (and keep track of those we've already added & those we need to remove) 2471 [allTorrents enumerateObjectsWithOptions: 0 usingBlock: ^(id objAll, NSUInteger previousIndex, BOOL * stop) { 2472 const NSUInteger currentIndex = [fDisplayedTorrents indexOfObjectAtIndexes: removePreviousIndexes options: NSEnumerationConcurrent passingTest: ^(id objDisplay, NSUInteger idx, BOOL *stop) { 2473 return (BOOL)(objAll == objDisplay); 2474 }]; 2475 if (currentIndex == NSNotFound) 2476 [addIndexes addIndex: previousIndex]; 2477 else 2478 [removePreviousIndexes removeIndex: currentIndex]; 2479 }]; 2480 2481 if ([addIndexes count] > 0 || [removePreviousIndexes count] > 0) 2482 { 2483 beganUpdates = YES; 2484 if (onLion) 2485 [fTableView beginUpdates]; 2486 2487 //remove torrents we didn't find 2488 if ([removePreviousIndexes count] > 0) 2489 { 2490 [fDisplayedTorrents removeObjectsAtIndexes: removePreviousIndexes]; 2491 if (onLion) 2492 [fTableView removeItemsAtIndexes: removePreviousIndexes inParent: nil withAnimation: NSTableViewAnimationSlideDown]; 2493 } 2494 2495 //add new torrents 2496 if ([addIndexes count] > 0) 2497 { 2498 //slide new torrents in differently 2499 if (fAddingTransfers) 2500 { 2501 NSIndexSet * newAddIndexes = [allTorrents indexesOfObjectsAtIndexes: addIndexes options: NSEnumerationConcurrent passingTest: ^BOOL(id obj, NSUInteger idx, BOOL * stop) { 2502 return [fAddingTransfers containsObject: obj]; 2503 }]; 2504 2505 [addIndexes removeIndexes: newAddIndexes]; 2506 2507 [fDisplayedTorrents addObjectsFromArray: [allTorrents objectsAtIndexes: newAddIndexes]]; 2508 if (onLion) 2509 [fTableView insertItemsAtIndexes: [NSIndexSet indexSetWithIndexesInRange: NSMakeRange([fDisplayedTorrents count] - [newAddIndexes count], [newAddIndexes count])] inParent: nil withAnimation: NSTableViewAnimationSlideLeft]; 2510 } 2511 2512 [fDisplayedTorrents addObjectsFromArray: [allTorrents objectsAtIndexes: addIndexes]]; 2513 if (onLion) 2514 [fTableView insertItemsAtIndexes: [NSIndexSet indexSetWithIndexesInRange: NSMakeRange([fDisplayedTorrents count] - [addIndexes count], [addIndexes count])] inParent: nil withAnimation: NSTableViewAnimationSlideDown]; 2515 } 2516 } 2517 } 2518 else if (groupRows && wasGroupRows) 2519 { 2520 NSAssert(groupRows && wasGroupRows, @"Should have had group rows and should remain with group rows"); 2521 2522 #warning don't always do? 2523 beganUpdates = YES; 2524 if (onLion) 2525 [fTableView beginUpdates]; 2526 2527 NSMutableIndexSet * unusedAllTorrentsIndexes = [NSMutableIndexSet indexSetWithIndexesInRange: NSMakeRange(0, [allTorrents count])]; 2528 2529 NSMutableDictionary * groupsByIndex = [NSMutableDictionary dictionaryWithCapacity: [fDisplayedTorrents count]]; 2530 for (TorrentGroup * group in fDisplayedTorrents) 2531 [groupsByIndex setObject: group forKey: [NSNumber numberWithInteger: [group groupIndex]]]; 2532 2533 const NSUInteger originalGroupCount = [fDisplayedTorrents count]; 2534 for (NSUInteger index = 0; index < originalGroupCount; ++index) 2535 { 2536 TorrentGroup * group = [fDisplayedTorrents objectAtIndex: index]; 2537 2538 NSMutableIndexSet * removeIndexes = [NSMutableIndexSet indexSet]; 2539 2540 //needs to be a signed integer 2541 for (NSInteger indexInGroup = 0; indexInGroup < [[group torrents] count]; ++indexInGroup) 2542 { 2543 Torrent * torrent = [[group torrents] objectAtIndex: indexInGroup]; 2544 const NSUInteger allIndex = [allTorrents indexOfObjectAtIndexes: unusedAllTorrentsIndexes options: NSEnumerationConcurrent passingTest: ^(id obj, NSUInteger idx, BOOL * stop) { 2545 return (BOOL)(obj == torrent); 2546 }]; 2547 if (allIndex == NSNotFound) 2548 [removeIndexes addIndex: indexInGroup]; 2549 else 2550 { 2551 BOOL markTorrentAsUsed = YES; 2552 2553 const NSInteger groupValue = [torrent groupValue]; 2554 if (groupValue != [group groupIndex]) 2555 { 2556 TorrentGroup * newGroup = [groupsByIndex objectForKey: [NSNumber numberWithInteger: groupValue]]; 2557 if (!newGroup) 2558 { 2559 newGroup = [[[TorrentGroup alloc] initWithGroup: groupValue] autorelease]; 2560 [groupsByIndex setObject: newGroup forKey: [NSNumber numberWithInteger: groupValue]]; 2561 [fDisplayedTorrents addObject: newGroup]; 2562 2563 if (onLion) 2564 { 2565 [fTableView insertItemsAtIndexes: [NSIndexSet indexSetWithIndex: [fDisplayedTorrents count]-1] inParent: nil withAnimation: NSTableViewAnimationEffectFade]; 2566 [fTableView isGroupCollapsed: groupValue] ? [fTableView collapseItem: newGroup] : [fTableView expandItem: newGroup]; 2567 } 2568 } 2569 else //if we haven't processed the other group yet, we have to make sure we don't flag it for removal the next time 2570 { 2571 //ugggh, but shouldn't happen too often 2572 if ([fDisplayedTorrents indexOfObject: newGroup inRange: NSMakeRange(index+1, originalGroupCount-(index+1))] != NSNotFound) 2573 markTorrentAsUsed = NO; 2574 } 2575 2576 [[group torrents] removeObjectAtIndex: indexInGroup]; 2577 [[newGroup torrents] addObject: torrent]; 2578 2579 if (onLion) 2580 [fTableView moveItemAtIndex: indexInGroup inParent: group toIndex: [[newGroup torrents] count]-1 inParent: newGroup]; 2581 2582 --indexInGroup; 2583 } 2584 2585 if (markTorrentAsUsed) 2586 [unusedAllTorrentsIndexes removeIndex: allIndex]; 2587 } 2588 } 2589 2590 if ([removeIndexes count] > 0) 2591 { 2592 [[group torrents] removeObjectsAtIndexes: removeIndexes]; 2593 if (onLion) 2594 [fTableView removeItemsAtIndexes: removeIndexes inParent: group withAnimation: NSTableViewAnimationEffectFade]; 2595 } 2596 } 2597 2598 //add remaining new torrents 2599 for (Torrent * torrent in [allTorrents objectsAtIndexes: unusedAllTorrentsIndexes]) 2600 { 2601 const NSInteger groupValue = [torrent groupValue]; 2602 TorrentGroup * group = [groupsByIndex objectForKey: [NSNumber numberWithInteger: groupValue]]; 2603 if (!group) 2604 { 2605 group = [[[TorrentGroup alloc] initWithGroup: groupValue] autorelease]; 2606 [groupsByIndex setObject: group forKey: [NSNumber numberWithInteger: groupValue]]; 2607 [fDisplayedTorrents addObject: group]; 2608 2609 if (onLion) 2610 { 2611 [fTableView insertItemsAtIndexes: [NSIndexSet indexSetWithIndex: [fDisplayedTorrents count]-1] inParent: nil withAnimation: NSTableViewAnimationEffectFade]; 2612 [fTableView isGroupCollapsed: groupValue] ? [fTableView collapseItem: group] : [fTableView expandItem: group]; 2613 } 2614 } 2615 2616 [[group torrents] addObject: torrent]; 2617 if (onLion) 2618 { 2619 const BOOL newTorrent = fAddingTransfers && [fAddingTransfers containsObject: torrent]; 2620 [fTableView insertItemsAtIndexes: [NSIndexSet indexSetWithIndex: [[group torrents] count]-1] inParent: group withAnimation: newTorrent ? NSTableViewAnimationSlideLeft : NSTableViewAnimationSlideDown]; 2621 } 2622 } 2623 2624 //remove empty groups 2625 NSIndexSet * removeGroupIndexes = [fDisplayedTorrents indexesOfObjectsAtIndexes: [NSIndexSet indexSetWithIndexesInRange: NSMakeRange(0, originalGroupCount)] options: NSEnumerationConcurrent passingTest: ^BOOL(id obj, NSUInteger idx, BOOL * stop) { 2626 return [[(TorrentGroup *)obj torrents] count] == 0; 2627 }]; 2628 2629 if ([removeGroupIndexes count] > 0) 2630 { 2631 [fDisplayedTorrents removeObjectsAtIndexes: removeGroupIndexes]; 2632 if (onLion) 2633 [fTableView removeItemsAtIndexes: removeGroupIndexes inParent: nil withAnimation: NSTableViewAnimationEffectFade]; 2634 } 2635 2636 //now that all groups are there, sort them - don't insert on the fly in case groups were reordered in prefs 2637 NSSortDescriptor * groupDescriptor = [NSSortDescriptor sortDescriptorWithKey: @"groupOrderValue" ascending: YES]; 2638 [self rearrangeTorrentTableArray: fDisplayedTorrents forParent: nil withSortDescriptors: [NSArray arrayWithObject: groupDescriptor] beganTableUpdate: &beganUpdates]; 2639 } 2640 else 2641 { 2642 NSAssert(groupRows != wasGroupRows, @"Trying toggling group-torrent reordering when we weren't expecting to."); 2643 2644 //set all groups as expanded 2645 [fTableView removeAllCollapsedGroups]; 2646 2647 //since we're not doing this the right way (boo buggy animation), we need to remember selected values 2648 #warning when Lion-only and using views instead of cells, this likely won't be needed 2649 NSArray * selectedValues = [fTableView selectedValues]; 2650 2651 beganUpdates = YES; 2652 if (onLion) 2653 [fTableView beginUpdates]; 2654 2655 if (onLion) 2656 [fTableView removeItemsAtIndexes: [NSIndexSet indexSetWithIndexesInRange: NSMakeRange(0, [fDisplayedTorrents count])] inParent: nil withAnimation: NSTableViewAnimationSlideDown]; 2657 2658 if (groupRows) 2659 { 2660 //a map for quickly finding groups 2661 NSMutableDictionary * groupsByIndex = [NSMutableDictionary dictionaryWithCapacity: [[GroupsController groups] numberOfGroups]]; 2662 for (Torrent * torrent in allTorrents) 2663 { 2664 const NSInteger groupValue = [torrent groupValue]; 2665 TorrentGroup * group = [groupsByIndex objectForKey: [NSNumber numberWithInteger: groupValue]]; 2666 if (!group) 2667 { 2668 group = [[[TorrentGroup alloc] initWithGroup: groupValue] autorelease]; 2669 [groupsByIndex setObject: group forKey: [NSNumber numberWithInteger: groupValue]]; 2670 } 2671 2672 [[group torrents] addObject: torrent]; 2673 } 2674 2675 [fDisplayedTorrents setArray: [groupsByIndex allValues]]; 2676 2677 //we need the groups to be sorted, and we can do it without moving items in the table, too! 2678 NSSortDescriptor * groupDescriptor = [NSSortDescriptor sortDescriptorWithKey: @"groupOrderValue" ascending: YES]; 2679 [fDisplayedTorrents sortUsingDescriptors: [NSArray arrayWithObject: groupDescriptor]]; 2680 } 2681 else 2682 [fDisplayedTorrents setArray: allTorrents]; 2683 2684 if (onLion) 2685 [fTableView insertItemsAtIndexes: [NSIndexSet indexSetWithIndexesInRange: NSMakeRange(0, [fDisplayedTorrents count])] inParent: nil withAnimation: NSTableViewAnimationEffectFade]; 2686 2687 if (groupRows) 2688 { 2689 //actually expand group rows 2690 for (TorrentGroup * group in fDisplayedTorrents) 2691 [fTableView expandItem: group]; 2692 } 2693 2694 if (selectedValues) 2695 [fTableView selectValues: selectedValues]; 2696 } 2697 2698 //sort the torrents (won't sort the groups, though) 2699 [self sortTorrentsCallUpdates: !beganUpdates includeQueueOrder: YES]; 2700 2701 if (onLion) 2702 { 2703 if (beganUpdates) 2704 [fTableView endUpdates]; 2705 [fTableView setNeedsDisplay: YES]; 2706 2707 [NSAnimationContext endGrouping]; 2708 } 2709 else 2710 { 2711 [fTableView reloadData]; 2712 2713 if (groupRows) 2714 { 2715 for (TorrentGroup * group in fDisplayedTorrents) 2716 { 2717 if ([fTableView isGroupCollapsed: [group groupIndex]]) 2718 [fTableView collapseItem: group]; 2719 else 2720 [fTableView expandItem: group]; 2721 } 2722 } 2723 } 2724 2725 if (!onLion) 2726 [fTableView selectValues: selectedValuesSL]; 2727 2728 [self resetInfo]; //if group is already selected, but the torrents in it change 2729 2730 [self setBottomCountText: groupRows || filterStatus || filterGroup || searchStrings]; 2731 2732 [self setWindowSizeToFit]; 2733 2734 if (fAddingTransfers) 2735 { 2736 [fAddingTransfers release]; 2737 fAddingTransfers = nil; 2738 } 2739} 2740 2741- (void) switchFilter: (id) sender 2742{ 2743 [fFilterBar switchFilter: sender == fNextFilterItem]; 2744} 2745 2746- (IBAction) showGlobalPopover: (id) sender 2747{ 2748 if ([NSApp isOnLionOrBetter]) 2749 { 2750 if (fGlobalPopoverShown) 2751 return; 2752 2753 NSPopover * popover = [[NSPopoverLion alloc] init]; 2754 [popover setBehavior: NSPopoverBehaviorTransient]; 2755 GlobalOptionsPopoverViewController * viewController = [[GlobalOptionsPopoverViewController alloc] initWithHandle: fLib]; 2756 [popover setContentViewController: viewController]; 2757 [popover setDelegate: self]; 2758 2759 [popover showRelativeToRect: [sender frame] ofView: sender preferredEdge: NSMaxYEdge]; 2760 2761 [viewController release]; 2762 [popover release]; 2763 } 2764 else 2765 { 2766 //place menu below button 2767 NSRect rect = [sender frame]; 2768 NSPoint location = rect.origin; 2769 location.y += NSHeight(rect) + 5.0; 2770 2771 [fActionMenu popUpMenuPositioningItem: nil atLocation: location inView: sender]; 2772 } 2773} 2774 2775//don't show multiple popovers when clicking the gear button repeatedly 2776- (void) popoverWillShow: (NSNotification *) notification 2777{ 2778 fGlobalPopoverShown = YES; 2779} 2780 2781- (void) popoverWillClose: (NSNotification *) notification 2782{ 2783 fGlobalPopoverShown = NO; 2784} 2785 2786- (void) menuNeedsUpdate: (NSMenu *) menu 2787{ 2788 if (menu == fGroupsSetMenu || menu == fGroupsSetContextMenu) 2789 { 2790 for (NSInteger i = [menu numberOfItems]-1; i >= 0; i--) 2791 [menu removeItemAtIndex: i]; 2792 2793 NSMenu * groupMenu = [[GroupsController groups] groupMenuWithTarget: self action: @selector(setGroup:) isSmall: NO]; 2794 2795 const NSInteger groupMenuCount = [groupMenu numberOfItems]; 2796 for (NSInteger i = 0; i < groupMenuCount; i++) 2797 { 2798 NSMenuItem * item = [[groupMenu itemAtIndex: 0] retain]; 2799 [groupMenu removeItemAtIndex: 0]; 2800 [menu addItem: item]; 2801 [item release]; 2802 } 2803 } 2804 else if (menu == fUploadMenu || menu == fDownloadMenu) 2805 { 2806 if ([menu numberOfItems] > 3) 2807 return; 2808 2809 const NSInteger speedLimitActionValue[] = { 5, 10, 20, 30, 40, 50, 75, 100, 150, 200, 250, 500, 750, 1000, 1500, 2000, -1 }; 2810 2811 NSMenuItem * item; 2812 for (NSInteger i = 0; speedLimitActionValue[i] != -1; i++) 2813 { 2814 item = [[NSMenuItem alloc] initWithTitle: [NSString stringWithFormat: NSLocalizedString(@"%d KB/s", 2815 "Action menu -> upload/download limit"), speedLimitActionValue[i]] action: @selector(setQuickLimitGlobal:) 2816 keyEquivalent: @""]; 2817 [item setTarget: self]; 2818 [item setRepresentedObject: [NSNumber numberWithInt: speedLimitActionValue[i]]]; 2819 [menu addItem: item]; 2820 [item release]; 2821 } 2822 } 2823 else if (menu == fRatioStopMenu) 2824 { 2825 if ([menu numberOfItems] > 3) 2826 return; 2827 2828 const float ratioLimitActionValue[] = { 0.25, 0.5, 0.75, 1.0, 1.5, 2.0, 3.0, -1 }; 2829 2830 NSMenuItem * item; 2831 for (NSInteger i = 0; ratioLimitActionValue[i] != -1; i++) 2832 { 2833 item = [[NSMenuItem alloc] initWithTitle: [NSString localizedStringWithFormat: @"%.2f", ratioLimitActionValue[i]] 2834 action: @selector(setQuickRatioGlobal:) keyEquivalent: @""]; 2835 [item setTarget: self]; 2836 [item setRepresentedObject: [NSNumber numberWithFloat: ratioLimitActionValue[i]]]; 2837 [menu addItem: item]; 2838 [item release]; 2839 } 2840 } 2841 else; 2842} 2843 2844- (void) setGroup: (id) sender 2845{ 2846 for (Torrent * torrent in [fTableView selectedTorrents]) 2847 { 2848 [fTableView removeCollapsedGroup: [torrent groupValue]]; //remove old collapsed group 2849 2850 [torrent setGroupValue: [(NSMenuItem *)sender tag]]; 2851 } 2852 2853 [self applyFilter]; 2854 [self updateUI]; 2855 [self updateTorrentHistory]; 2856} 2857 2858- (void) toggleSpeedLimit: (id) sender 2859{ 2860 [fDefaults setBool: ![fDefaults boolForKey: @"SpeedLimit"] forKey: @"SpeedLimit"]; 2861 [self speedLimitChanged: sender]; 2862} 2863 2864- (void) speedLimitChanged: (id) sender 2865{ 2866 tr_sessionUseAltSpeed(fLib, [fDefaults boolForKey: @"SpeedLimit"]); 2867 [fStatusBar updateSpeedFieldsToolTips]; 2868} 2869 2870//dict has been retained 2871- (void) altSpeedToggledCallbackIsLimited: (NSDictionary *) dict 2872{ 2873 const BOOL isLimited = [[dict objectForKey: @"Active"] boolValue]; 2874 2875 [fDefaults setBool: isLimited forKey: @"SpeedLimit"]; 2876 [fStatusBar updateSpeedFieldsToolTips]; 2877 2878 if (![[dict objectForKey: @"ByUser"] boolValue]) 2879 [GrowlApplicationBridge notifyWithTitle: isLimited 2880 ? NSLocalizedString(@"Speed Limit Auto Enabled", "Growl notification title") 2881 : NSLocalizedString(@"Speed Limit Auto Disabled", "Growl notification title") 2882 description: NSLocalizedString(@"Bandwidth settings changed", "Growl notification description") 2883 notificationName: GROWL_AUTO_SPEED_LIMIT iconData: nil priority: 0 isSticky: NO clickContext: nil]; 2884 2885 [dict release]; 2886} 2887 2888- (void) setLimitGlobalEnabled: (id) sender 2889{ 2890 BOOL upload = [sender menu] == fUploadMenu; 2891 [fDefaults setBool: sender == (upload ? fUploadLimitItem : fDownloadLimitItem) forKey: upload ? @"CheckUpload" : @"CheckDownload"]; 2892 2893 [fPrefsController applySpeedSettings: nil]; 2894} 2895 2896- (void) setQuickLimitGlobal: (id) sender 2897{ 2898 BOOL upload = [sender menu] == fUploadMenu; 2899 [fDefaults setInteger: [[sender representedObject] intValue] forKey: upload ? @"UploadLimit" : @"DownloadLimit"]; 2900 [fDefaults setBool: YES forKey: upload ? @"CheckUpload" : @"CheckDownload"]; 2901 2902 [fPrefsController updateLimitFields]; 2903 [fPrefsController applySpeedSettings: nil]; 2904} 2905 2906- (void) setRatioGlobalEnabled: (id) sender 2907{ 2908 [fDefaults setBool: sender == fCheckRatioItem forKey: @"RatioCheck"]; 2909 2910 [fPrefsController applyRatioSetting: nil]; 2911} 2912 2913- (void) setQuickRatioGlobal: (id) sender 2914{ 2915 [fDefaults setBool: YES forKey: @"RatioCheck"]; 2916 [fDefaults setFloat: [[sender representedObject] floatValue] forKey: @"RatioLimit"]; 2917 2918 [fPrefsController updateRatioStopFieldOld]; 2919} 2920 2921- (void) sound: (NSSound *) sound didFinishPlaying: (BOOL) finishedPlaying 2922{ 2923 fSoundPlaying = NO; 2924} 2925 2926- (void) watcher: (id<UKFileWatcher>) watcher receivedNotification: (NSString *) notification forPath: (NSString *) path 2927{ 2928 if ([notification isEqualToString: UKFileWatcherWriteNotification]) 2929 { 2930 if (![fDefaults boolForKey: @"AutoImport"] || ![fDefaults stringForKey: @"AutoImportDirectory"]) 2931 return; 2932 2933 if (fAutoImportTimer) 2934 { 2935 if ([fAutoImportTimer isValid]) 2936 [fAutoImportTimer invalidate]; 2937 [fAutoImportTimer release]; 2938 fAutoImportTimer = nil; 2939 } 2940 2941 //check again in 10 seconds in case torrent file wasn't complete 2942 fAutoImportTimer = [[NSTimer scheduledTimerWithTimeInterval: 10.0 target: self 2943 selector: @selector(checkAutoImportDirectory) userInfo: nil repeats: NO] retain]; 2944 2945 [self checkAutoImportDirectory]; 2946 } 2947} 2948 2949- (void) changeAutoImport 2950{ 2951 if (fAutoImportTimer) 2952 { 2953 if ([fAutoImportTimer isValid]) 2954 [fAutoImportTimer invalidate]; 2955 [fAutoImportTimer release]; 2956 fAutoImportTimer = nil; 2957 } 2958 [fAutoImportedNames release]; 2959 fAutoImportedNames = nil; 2960 2961 [self checkAutoImportDirectory]; 2962} 2963 2964- (void) checkAutoImportDirectory 2965{ 2966 NSString * path; 2967 if (![fDefaults boolForKey: @"AutoImport"] || !(path = [fDefaults stringForKey: @"AutoImportDirectory"])) 2968 return; 2969 2970 path = [path stringByExpandingTildeInPath]; 2971 2972 NSArray * importedNames; 2973 if (!(importedNames = [[NSFileManager defaultManager] contentsOfDirectoryAtPath: path error: NULL])) 2974 return; 2975 2976 //only check files that have not been checked yet 2977 NSMutableArray * newNames = [importedNames mutableCopy]; 2978 2979 if (fAutoImportedNames) 2980 [newNames removeObjectsInArray: fAutoImportedNames]; 2981 else 2982 fAutoImportedNames = [[NSMutableArray alloc] init]; 2983 [fAutoImportedNames setArray: importedNames]; 2984 2985 for (NSString * file in newNames) 2986 { 2987 if ([file hasPrefix: @"."]) 2988 continue; 2989 2990 NSString * fullFile = [path stringByAppendingPathComponent: file]; 2991 2992 if (!([[[NSWorkspace sharedWorkspace] typeOfFile: fullFile error: NULL] isEqualToString: @"org.bittorrent.torrent"] 2993 || [[fullFile pathExtension] caseInsensitiveCompare: @"torrent"] == NSOrderedSame)) 2994 continue; 2995 2996 tr_ctor * ctor = tr_ctorNew(fLib); 2997 tr_ctorSetMetainfoFromFile(ctor, [fullFile UTF8String]); 2998 2999 switch (tr_torrentParse(ctor, NULL)) 3000 { 3001 case TR_PARSE_OK: 3002 [self openFiles: [NSArray arrayWithObject: fullFile] addType: ADD_AUTO forcePath: nil]; 3003 3004 NSString * notificationTitle = NSLocalizedString(@"Torrent File Auto Added", "notification title"); 3005 if ([NSApp isOnMountainLionOrBetter]) 3006 { 3007 NSUserNotification* notification = [[NSUserNotificationMtLion alloc] init]; 3008 [notification setTitle: notificationTitle]; 3009 [notification setInformativeText: file]; 3010 3011 [notification setHasActionButton: NO]; 3012 3013 [[NSUserNotificationCenterMtLion defaultUserNotificationCenter] deliverNotification: notification]; 3014 [notification release]; 3015 } 3016 3017 [GrowlApplicationBridge notifyWithTitle: notificationTitle 3018 description: file notificationName: GROWL_AUTO_ADD iconData: nil priority: 0 isSticky: NO 3019 clickContext: nil]; 3020 break; 3021 3022 case TR_PARSE_ERR: 3023 [fAutoImportedNames removeObject: file]; 3024 break; 3025 3026 case TR_PARSE_DUPLICATE: //let's ignore this (but silence a warning) 3027 break; 3028 } 3029 3030 tr_ctorFree(ctor); 3031 } 3032 3033 [newNames release]; 3034} 3035 3036- (void) beginCreateFile: (NSNotification *) notification 3037{ 3038 if (![fDefaults boolForKey: @"AutoImport"]) 3039 return; 3040 3041 NSString * location = [(NSURL *)[notification object] path], 3042 * path = [fDefaults stringForKey: @"AutoImportDirectory"]; 3043 3044 if (location && path && [[[location stringByDeletingLastPathComponent] stringByExpandingTildeInPath] 3045 isEqualToString: [path stringByExpandingTildeInPath]]) 3046 [fAutoImportedNames addObject: [location lastPathComponent]]; 3047} 3048 3049- (NSInteger) outlineView: (NSOutlineView *) outlineView numberOfChildrenOfItem: (id) item 3050{ 3051 if (item) 3052 return [[item torrents] count]; 3053 else 3054 return [fDisplayedTorrents count]; 3055} 3056 3057- (id) outlineView: (NSOutlineView *) outlineView child: (NSInteger) index ofItem: (id) item 3058{ 3059 if (item) 3060 return [[item torrents] objectAtIndex: index]; 3061 else 3062 return [fDisplayedTorrents objectAtIndex: index]; 3063} 3064 3065- (BOOL) outlineView: (NSOutlineView *) outlineView isItemExpandable: (id) item 3066{ 3067 return ![item isKindOfClass: [Torrent class]]; 3068} 3069 3070- (id) outlineView: (NSOutlineView *) outlineView objectValueForTableColumn: (NSTableColumn *) tableColumn byItem: (id) item 3071{ 3072 if ([item isKindOfClass: [Torrent class]]) { 3073 if (tableColumn) 3074 return nil; 3075 return [item hashString]; 3076 } 3077 else 3078 { 3079 NSString * ident = [tableColumn identifier]; 3080 if ([ident isEqualToString: @"Group"]) 3081 { 3082 NSInteger group = [item groupIndex]; 3083 return group != -1 ? [[GroupsController groups] nameForIndex: group] 3084 : NSLocalizedString(@"No Group", "Group table row"); 3085 } 3086 else if ([ident isEqualToString: @"Color"]) 3087 { 3088 NSInteger group = [item groupIndex]; 3089 return [[GroupsController groups] imageForIndex: group]; 3090 } 3091 else if ([ident isEqualToString: @"DL Image"]) 3092 return [NSImage imageNamed: @"DownArrowGroupTemplate"]; 3093 else if ([ident isEqualToString: @"UL Image"]) 3094 return [NSImage imageNamed: [fDefaults boolForKey: @"DisplayGroupRowRatio"] 3095 ? @"YingYangGroupTemplate" : @"UpArrowGroupTemplate"]; 3096 else 3097 { 3098 TorrentGroup * group = (TorrentGroup *)item; 3099 3100 if ([fDefaults boolForKey: @"DisplayGroupRowRatio"]) 3101 return [NSString stringForRatio: [group ratio]]; 3102 else 3103 { 3104 CGFloat rate = [ident isEqualToString: @"UL"] ? [group uploadRate] : [group downloadRate]; 3105 return [NSString stringForSpeed: rate]; 3106 } 3107 } 3108 } 3109} 3110 3111- (BOOL) outlineView: (NSOutlineView *) outlineView writeItems: (NSArray *) items toPasteboard: (NSPasteboard *) pasteboard 3112{ 3113 //only allow reordering of rows if sorting by order 3114 if ([fDefaults boolForKey: @"SortByGroup"] || [[fDefaults stringForKey: @"Sort"] isEqualToString: SORT_ORDER]) 3115 { 3116 NSMutableIndexSet * indexSet = [NSMutableIndexSet indexSet]; 3117 for (id torrent in items) 3118 { 3119 if (![torrent isKindOfClass: [Torrent class]]) 3120 return NO; 3121 3122 [indexSet addIndex: [fTableView rowForItem: torrent]]; 3123 } 3124 3125 [pasteboard declareTypes: [NSArray arrayWithObject: TORRENT_TABLE_VIEW_DATA_TYPE] owner: self]; 3126 [pasteboard setData: [NSKeyedArchiver archivedDataWithRootObject: indexSet] forType: TORRENT_TABLE_VIEW_DATA_TYPE]; 3127 return YES; 3128 } 3129 return NO; 3130} 3131 3132- (NSDragOperation) outlineView: (NSOutlineView *) outlineView validateDrop: (id < NSDraggingInfo >) info proposedItem: (id) item 3133 proposedChildIndex: (NSInteger) index 3134{ 3135 NSPasteboard * pasteboard = [info draggingPasteboard]; 3136 if ([[pasteboard types] containsObject: TORRENT_TABLE_VIEW_DATA_TYPE]) 3137 { 3138 if ([fDefaults boolForKey: @"SortByGroup"]) 3139 { 3140 if (!item) 3141 return NSDragOperationNone; 3142 3143 if ([[fDefaults stringForKey: @"Sort"] isEqualToString: SORT_ORDER]) 3144 { 3145 if ([item isKindOfClass: [Torrent class]]) 3146 { 3147 TorrentGroup * group = [fTableView parentForItem: item]; 3148 index = [[group torrents] indexOfObject: item] + 1; 3149 item = group; 3150 } 3151 } 3152 else 3153 { 3154 if ([item isKindOfClass: [Torrent class]]) 3155 item = [fTableView parentForItem: item]; 3156 index = NSOutlineViewDropOnItemIndex; 3157 } 3158 } 3159 else 3160 { 3161 if (index == NSOutlineViewDropOnItemIndex) 3162 return NSDragOperationNone; 3163 3164 if (item) 3165 { 3166 index = [fTableView rowForItem: item] + 1; 3167 item = nil; 3168 } 3169 } 3170 3171 [fTableView setDropItem: item dropChildIndex: index]; 3172 return NSDragOperationGeneric; 3173 } 3174 3175 return NSDragOperationNone; 3176} 3177 3178- (BOOL) outlineView: (NSOutlineView *) outlineView acceptDrop: (id < NSDraggingInfo >) info item: (id) item childIndex: (NSInteger) newRow 3179{ 3180 NSPasteboard * pasteboard = [info draggingPasteboard]; 3181 if ([[pasteboard types] containsObject: TORRENT_TABLE_VIEW_DATA_TYPE]) 3182 { 3183 //remember selected rows 3184 NSArray * selectedValues = nil; 3185 if (![NSApp isOnLionOrBetter]) 3186 selectedValues = [fTableView selectedValues]; 3187 3188 NSIndexSet * indexes = [NSKeyedUnarchiver unarchiveObjectWithData: [pasteboard dataForType: TORRENT_TABLE_VIEW_DATA_TYPE]]; 3189 3190 //get the torrents to move 3191 NSMutableArray * movingTorrents = [NSMutableArray arrayWithCapacity: [indexes count]]; 3192 for (NSUInteger i = [indexes firstIndex]; i != NSNotFound; i = [indexes indexGreaterThanIndex: i]) 3193 { 3194 Torrent * torrent = [fTableView itemAtRow: i]; 3195 [movingTorrents addObject: torrent]; 3196 3197 //change groups 3198 if (item) 3199 [torrent setGroupValue: [item groupIndex]]; 3200 } 3201 3202 //reorder queue order 3203 if (newRow != NSOutlineViewDropOnItemIndex) 3204 { 3205 //find torrent to place under 3206 NSArray * groupTorrents = item ? [item torrents] : fDisplayedTorrents; 3207 Torrent * topTorrent = nil; 3208 for (NSInteger i = newRow-1; i >= 0; i--) 3209 { 3210 Torrent * tempTorrent = [groupTorrents objectAtIndex: i]; 3211 if (![movingTorrents containsObject: tempTorrent]) 3212 { 3213 topTorrent = tempTorrent; 3214 break; 3215 } 3216 } 3217 3218 //remove objects to reinsert 3219 [fTorrents removeObjectsInArray: movingTorrents]; 3220 3221 //insert objects at new location 3222 const NSUInteger insertIndex = topTorrent ? [fTorrents indexOfObject: topTorrent] + 1 : 0; 3223 NSIndexSet * insertIndexes = [NSIndexSet indexSetWithIndexesInRange: NSMakeRange(insertIndex, [movingTorrents count])]; 3224 [fTorrents insertObjects: movingTorrents atIndexes: insertIndexes]; 3225 3226 //we need to make sure the queue order is updated in the Torrent object before we sort - safest to just reset all queue positions 3227 NSUInteger i = 0; 3228 for (Torrent * torrent in fTorrents) 3229 { 3230 [torrent setQueuePosition: i++]; 3231 [torrent update]; 3232 } 3233 3234 //do the drag animation here so that the dragged torrents are the ones that are animated as moving, and not the torrents around them 3235 const BOOL onLion = [NSApp isOnLionOrBetter]; 3236 if (onLion) 3237 [fTableView beginUpdates]; 3238 3239 NSUInteger insertDisplayIndex = topTorrent ? [groupTorrents indexOfObject: topTorrent] + 1 : 0; 3240 3241 for (Torrent * torrent in movingTorrents) 3242 { 3243 TorrentGroup * oldParent = item ? [fTableView parentForItem: torrent] : nil; 3244 NSMutableArray * oldTorrents = oldParent ? [oldParent torrents] : fDisplayedTorrents; 3245 const NSUInteger oldIndex = [oldTorrents indexOfObject: torrent]; 3246 3247 if (item == oldParent) 3248 { 3249 if (oldIndex < insertDisplayIndex) 3250 --insertDisplayIndex; 3251 [oldTorrents moveObjectAtIndex: oldIndex toIndex: insertDisplayIndex]; 3252 } 3253 else 3254 { 3255 NSAssert(item && oldParent, @"Expected to be dragging between group rows"); 3256 3257 NSMutableArray * newTorrents = [(TorrentGroup *)item torrents]; 3258 [newTorrents insertObject: torrent atIndex: insertDisplayIndex]; 3259 [oldTorrents removeObjectAtIndex: oldIndex]; 3260 } 3261 3262 if (onLion) 3263 [fTableView moveItemAtIndex: oldIndex inParent: oldParent toIndex: insertDisplayIndex inParent: item]; 3264 3265 ++insertDisplayIndex; 3266 } 3267 3268 if (onLion) 3269 [fTableView endUpdates]; 3270 } 3271 3272 [self applyFilter]; 3273 if (selectedValues) 3274 [fTableView selectValues: selectedValues]; 3275 } 3276 3277 return YES; 3278} 3279 3280- (void) torrentTableViewSelectionDidChange: (NSNotification *) notification 3281{ 3282 [self resetInfo]; 3283 [[fWindow toolbar] validateVisibleItems]; 3284} 3285 3286- (NSDragOperation) draggingEntered: (id <NSDraggingInfo>) info 3287{ 3288 NSPasteboard * pasteboard = [info draggingPasteboard]; 3289 if ([[pasteboard types] containsObject: NSFilenamesPboardType]) 3290 { 3291 //check if any torrent files can be added 3292 BOOL torrent = NO; 3293 NSArray * files = [pasteboard propertyListForType: NSFilenamesPboardType]; 3294 for (NSString * file in files) 3295 { 3296 if ([[[NSWorkspace sharedWorkspace] typeOfFile: file error: NULL] isEqualToString: @"org.bittorrent.torrent"] 3297 || [[file pathExtension] caseInsensitiveCompare: @"torrent"] == NSOrderedSame) 3298 { 3299 torrent = YES; 3300 tr_ctor * ctor = tr_ctorNew(fLib); 3301 tr_ctorSetMetainfoFromFile(ctor, [file UTF8String]); 3302 if (tr_torrentParse(ctor, NULL) == TR_PARSE_OK) 3303 { 3304 if (!fOverlayWindow) 3305 fOverlayWindow = [[DragOverlayWindow alloc] initWithLib: fLib forWindow: fWindow]; 3306 [fOverlayWindow setTorrents: files]; 3307 3308 return NSDragOperationCopy; 3309 } 3310 tr_ctorFree(ctor); 3311 } 3312 } 3313 3314 //create a torrent file if a single file 3315 if (!torrent && [files count] == 1) 3316 { 3317 if (!fOverlayWindow) 3318 fOverlayWindow = [[DragOverlayWindow alloc] initWithLib: fLib forWindow: fWindow]; 3319 [fOverlayWindow setFile: [[files objectAtIndex: 0] lastPathComponent]]; 3320 3321 return NSDragOperationCopy; 3322 } 3323 } 3324 else if ([[pasteboard types] containsObject: NSURLPboardType]) 3325 { 3326 if (!fOverlayWindow) 3327 fOverlayWindow = [[DragOverlayWindow alloc] initWithLib: fLib forWindow: fWindow]; 3328 [fOverlayWindow setURL: [[NSURL URLFromPasteboard: pasteboard] relativeString]]; 3329 3330 return NSDragOperationCopy; 3331 } 3332 else; 3333 3334 return NSDragOperationNone; 3335} 3336 3337- (void) draggingExited: (id <NSDraggingInfo>) info 3338{ 3339 if (fOverlayWindow) 3340 [fOverlayWindow fadeOut]; 3341} 3342 3343- (BOOL) performDragOperation: (id <NSDraggingInfo>) info 3344{ 3345 if (fOverlayWindow) 3346 [fOverlayWindow fadeOut]; 3347 3348 NSPasteboard * pasteboard = [info draggingPasteboard]; 3349 if ([[pasteboard types] containsObject: NSFilenamesPboardType]) 3350 { 3351 BOOL torrent = NO, accept = YES; 3352 3353 //create an array of files that can be opened 3354 NSArray * files = [pasteboard propertyListForType: NSFilenamesPboardType]; 3355 NSMutableArray * filesToOpen = [NSMutableArray arrayWithCapacity: [files count]]; 3356 for (NSString * file in files) 3357 { 3358 if ([[[NSWorkspace sharedWorkspace] typeOfFile: file error: NULL] isEqualToString: @"org.bittorrent.torrent"] 3359 || [[file pathExtension] caseInsensitiveCompare: @"torrent"] == NSOrderedSame) 3360 { 3361 torrent = YES; 3362 tr_ctor * ctor = tr_ctorNew(fLib); 3363 tr_ctorSetMetainfoFromFile(ctor, [file UTF8String]); 3364 if (tr_torrentParse(ctor, NULL) == TR_PARSE_OK) 3365 [filesToOpen addObject: file]; 3366 tr_ctorFree(ctor); 3367 } 3368 } 3369 3370 if ([filesToOpen count] > 0) 3371 [self application: NSApp openFiles: filesToOpen]; 3372 else 3373 { 3374 if (!torrent && [files count] == 1) 3375 [CreatorWindowController createTorrentFile: fLib forFile: [NSURL fileURLWithPath: [files objectAtIndex: 0]]]; 3376 else 3377 accept = NO; 3378 } 3379 3380 return accept; 3381 } 3382 else if ([[pasteboard types] containsObject: NSURLPboardType]) 3383 { 3384 NSURL * url; 3385 if ((url = [NSURL URLFromPasteboard: pasteboard])) 3386 { 3387 [self openURL: [url absoluteString]]; 3388 return YES; 3389 } 3390 } 3391 else; 3392 3393 return NO; 3394} 3395 3396- (void) toggleSmallView: (id) sender 3397{ 3398 BOOL makeSmall = ![fDefaults boolForKey: @"SmallView"]; 3399 [fDefaults setBool: makeSmall forKey: @"SmallView"]; 3400 3401 [fTableView setUsesAlternatingRowBackgroundColors: !makeSmall]; 3402 3403 [fTableView setRowHeight: makeSmall ? ROW_HEIGHT_SMALL : ROW_HEIGHT_REGULAR]; 3404 3405 if ([NSApp isOnLionOrBetter]) 3406 [fTableView beginUpdates]; 3407 [fTableView noteHeightOfRowsWithIndexesChanged: [NSIndexSet indexSetWithIndexesInRange: NSMakeRange(0, [fTableView numberOfRows])]]; 3408 if ([NSApp isOnLionOrBetter]) 3409 [fTableView endUpdates]; 3410 3411 //resize for larger min height if not set to auto size 3412 if (![fDefaults boolForKey: @"AutoSize"]) 3413 { 3414 const NSSize contentSize = [[fWindow contentView] frame].size; 3415 3416 NSSize contentMinSize = [fWindow contentMinSize]; 3417 contentMinSize.height = [self minWindowContentSizeAllowed]; 3418 [fWindow setContentMinSize: contentMinSize]; 3419 3420 //make sure the window already isn't too small 3421 if (!makeSmall && contentSize.height < contentMinSize.height) 3422 { 3423 NSRect frame = [fWindow frame]; 3424 CGFloat heightChange = contentMinSize.height - contentSize.height; 3425 frame.size.height += heightChange; 3426 frame.origin.y -= heightChange; 3427 3428 [fWindow setFrame: frame display: YES]; 3429 } 3430 } 3431 else 3432 [self setWindowSizeToFit]; 3433} 3434 3435- (void) togglePiecesBar: (id) sender 3436{ 3437 [fDefaults setBool: ![fDefaults boolForKey: @"PiecesBar"] forKey: @"PiecesBar"]; 3438 [fTableView togglePiecesBar]; 3439} 3440 3441- (void) toggleAvailabilityBar: (id) sender 3442{ 3443 [fDefaults setBool: ![fDefaults boolForKey: @"DisplayProgressBarAvailable"] forKey: @"DisplayProgressBarAvailable"]; 3444 [fTableView display]; 3445} 3446 3447#warning elliminate when 10.7-only 3448- (void) toggleStatusString: (id) sender 3449{ 3450 if ([fDefaults boolForKey: @"SmallView"]) 3451 [fDefaults setBool: ![fDefaults boolForKey: @"DisplaySmallStatusRegular"] forKey: @"DisplaySmallStatusRegular"]; 3452 else 3453 [fDefaults setBool: ![fDefaults boolForKey: @"DisplayStatusProgressSelected"] forKey: @"DisplayStatusProgressSelected"]; 3454 3455 [fTableView setNeedsDisplay: YES]; 3456} 3457 3458- (NSRect) windowFrameByAddingHeight: (CGFloat) height checkLimits: (BOOL) check 3459{ 3460 NSScrollView * scrollView = [fTableView enclosingScrollView]; 3461 3462 //convert pixels to points 3463 NSRect windowFrame = [fWindow frame]; 3464 NSSize windowSize = [scrollView convertSize: windowFrame.size fromView: nil]; 3465 windowSize.height += height; 3466 3467 if (check) 3468 { 3469 //we can't call minSize, since it might be set to the current size (auto size) 3470 const CGFloat minHeight = [self minWindowContentSizeAllowed] 3471 + (NSHeight([fWindow frame]) - NSHeight([[fWindow contentView] frame])); //contentView to window 3472 3473 if (windowSize.height <= minHeight) 3474 windowSize.height = minHeight; 3475 else 3476 { 3477 NSScreen * screen = [fWindow screen]; 3478 if (screen) 3479 { 3480 NSSize maxSize = [scrollView convertSize: [screen visibleFrame].size fromView: nil]; 3481 if (!fStatusBar) 3482 maxSize.height -= STATUS_BAR_HEIGHT; 3483 if (!fFilterBar) 3484 maxSize.height -= FILTER_BAR_HEIGHT; 3485 if (windowSize.height > maxSize.height) 3486 windowSize.height = maxSize.height; 3487 } 3488 } 3489 } 3490 3491 //convert points to pixels 3492 windowSize = [scrollView convertSize: windowSize toView: nil]; 3493 3494 windowFrame.origin.y -= (windowSize.height - windowFrame.size.height); 3495 windowFrame.size.height = windowSize.height; 3496 return windowFrame; 3497} 3498 3499- (void) toggleStatusBar: (id) sender 3500{ 3501 const BOOL show = fStatusBar == nil; 3502 [self showStatusBar: show animate: YES]; 3503 [fDefaults setBool: show forKey: @"StatusBar"]; 3504} 3505 3506//doesn't save shown state 3507- (void) showStatusBar: (BOOL) show animate: (BOOL) animate 3508{ 3509 const BOOL prevShown = fStatusBar != nil; 3510 if (show == prevShown) 3511 return; 3512 3513 if (show) 3514 { 3515 fStatusBar = [[StatusBarController alloc] initWithLib: fLib]; 3516 3517 NSView * contentView = [fWindow contentView]; 3518 const NSSize windowSize = [contentView convertSize: [fWindow frame].size fromView: nil]; 3519 3520 NSRect statusBarFrame = [[fStatusBar view] frame]; 3521 statusBarFrame.size.width = windowSize.width; 3522 [[fStatusBar view] setFrame: statusBarFrame]; 3523 3524 [contentView addSubview: [fStatusBar view]]; 3525 [[fStatusBar view] setFrameOrigin: NSMakePoint(0.0, NSMaxY([contentView frame]))]; 3526 } 3527 3528 CGFloat heightChange = [[fStatusBar view] frame].size.height; 3529 if (!show) 3530 heightChange *= -1; 3531 3532 //allow bar to show even if not enough room 3533 if (show && ![fDefaults boolForKey: @"AutoSize"]) 3534 { 3535 NSRect frame = [self windowFrameByAddingHeight: heightChange checkLimits: NO]; 3536 3537 NSScreen * screen = [fWindow screen]; 3538 if (screen) 3539 { 3540 CGFloat change = [screen visibleFrame].size.height - frame.size.height; 3541 if (change < 0.0) 3542 { 3543 frame = [fWindow frame]; 3544 frame.size.height += change; 3545 frame.origin.y -= change; 3546 [fWindow setFrame: frame display: NO animate: NO]; 3547 } 3548 } 3549 } 3550 3551 [self updateUI]; 3552 3553 NSScrollView * scrollView = [fTableView enclosingScrollView]; 3554 3555 //set views to not autoresize 3556 const NSUInteger statsMask = [[fStatusBar view] autoresizingMask]; 3557 [[fStatusBar view] setAutoresizingMask: NSViewNotSizable]; 3558 NSUInteger filterMask; 3559 if (fFilterBar) 3560 { 3561 filterMask = [[fFilterBar view] autoresizingMask]; 3562 [[fFilterBar view] setAutoresizingMask: NSViewNotSizable]; 3563 } 3564 const NSUInteger scrollMask = [scrollView autoresizingMask]; 3565 [scrollView setAutoresizingMask: NSViewNotSizable]; 3566 3567 NSRect frame = [self windowFrameByAddingHeight: heightChange checkLimits: NO]; 3568 [fWindow setFrame: frame display: YES animate: animate]; 3569 3570 //re-enable autoresize 3571 [[fStatusBar view] setAutoresizingMask: statsMask]; 3572 if (fFilterBar) 3573 [[fFilterBar view] setAutoresizingMask: filterMask]; 3574 [scrollView setAutoresizingMask: scrollMask]; 3575 3576 if (!show) 3577 { 3578 [[fStatusBar view] removeFromSuperviewWithoutNeedingDisplay]; 3579 [fStatusBar release]; 3580 fStatusBar = nil; 3581 } 3582 3583 if ([fDefaults boolForKey: @"AutoSize"]) 3584 [self setWindowMinMaxToCurrent]; 3585 else 3586 { 3587 //change min size 3588 NSSize minSize = [fWindow contentMinSize]; 3589 minSize.height += heightChange; 3590 [fWindow setContentMinSize: minSize]; 3591 } 3592} 3593 3594- (void) toggleFilterBar: (id) sender 3595{ 3596 const BOOL show = fFilterBar == nil; 3597 3598 [self showFilterBar: show animate: YES]; 3599 [fDefaults setBool: show forKey: @"FilterBar"]; 3600 [[fWindow toolbar] validateVisibleItems]; 3601 3602 //disable filtering when hiding 3603 if (!show) 3604 [fFilterBar reset: NO]; 3605 3606 [self applyFilter]; //do even if showing to ensure tooltips are updated 3607} 3608 3609//doesn't save shown state 3610- (void) showFilterBar: (BOOL) show animate: (BOOL) animate 3611{ 3612 const BOOL prevShown = fFilterBar != nil; 3613 if (show == prevShown) 3614 return; 3615 3616 if (show) 3617 { 3618 fFilterBar = [[FilterBarController alloc] init]; 3619 3620 NSView * contentView = [fWindow contentView]; 3621 const NSSize windowSize = [contentView convertSize: [fWindow frame].size fromView: nil]; 3622 3623 NSRect filterBarFrame = [[fFilterBar view] frame]; 3624 filterBarFrame.size.width = windowSize.width; 3625 [[fFilterBar view] setFrame: filterBarFrame]; 3626 3627 if (fStatusBar) 3628 [contentView addSubview: [fFilterBar view] positioned: NSWindowBelow relativeTo: [fStatusBar view]]; 3629 else 3630 [contentView addSubview: [fFilterBar view]]; 3631 const CGFloat originY = fStatusBar ? NSMinY([[fStatusBar view] frame]) : NSMaxY([contentView frame]); 3632 [[fFilterBar view] setFrameOrigin: NSMakePoint(0.0, originY)]; 3633 } 3634 else 3635 [fWindow makeFirstResponder: fTableView]; 3636 3637 CGFloat heightChange = NSHeight([[fFilterBar view] frame]); 3638 if (!show) 3639 heightChange *= -1; 3640 3641 //allow bar to show even if not enough room 3642 if (show && ![fDefaults boolForKey: @"AutoSize"]) 3643 { 3644 NSRect frame = [self windowFrameByAddingHeight: heightChange checkLimits: NO]; 3645 3646 NSScreen * screen = [fWindow screen]; 3647 if (screen) 3648 { 3649 CGFloat change = [screen visibleFrame].size.height - frame.size.height; 3650 if (change < 0.0) 3651 { 3652 frame = [fWindow frame]; 3653 frame.size.height += change; 3654 frame.origin.y -= change; 3655 [fWindow setFrame: frame display: NO animate: NO]; 3656 } 3657 } 3658 } 3659 3660 NSScrollView * scrollView = [fTableView enclosingScrollView]; 3661 3662 //set views to not autoresize 3663 const NSUInteger filterMask = [[fFilterBar view] autoresizingMask]; 3664 const NSUInteger scrollMask = [scrollView autoresizingMask]; 3665 [[fFilterBar view] setAutoresizingMask: NSViewNotSizable]; 3666 [scrollView setAutoresizingMask: NSViewNotSizable]; 3667 3668 const NSRect frame = [self windowFrameByAddingHeight: heightChange checkLimits: NO]; 3669 [fWindow setFrame: frame display: YES animate: animate]; 3670 3671 //re-enable autoresize 3672 [[fFilterBar view] setAutoresizingMask: filterMask]; 3673 [scrollView setAutoresizingMask: scrollMask]; 3674 3675 if (!show) 3676 { 3677 [[fFilterBar view] removeFromSuperviewWithoutNeedingDisplay]; 3678 [fFilterBar release]; 3679 fFilterBar = nil; 3680 } 3681 3682 if ([fDefaults boolForKey: @"AutoSize"]) 3683 [self setWindowMinMaxToCurrent]; 3684 else 3685 { 3686 //change min size 3687 NSSize minSize = [fWindow contentMinSize]; 3688 minSize.height += heightChange; 3689 [fWindow setContentMinSize: minSize]; 3690 } 3691} 3692 3693- (void) focusFilterField 3694{ 3695 if (!fFilterBar) 3696 [self toggleFilterBar: self]; 3697 [fFilterBar focusSearchField]; 3698} 3699 3700- (BOOL) acceptsPreviewPanelControl: (QLPreviewPanel *) panel 3701{ 3702 return !fQuitting; 3703} 3704 3705- (void) beginPreviewPanelControl: (QLPreviewPanel *) panel 3706{ 3707 fPreviewPanel = [panel retain]; 3708 [fPreviewPanel setDelegate: self]; 3709 [fPreviewPanel setDataSource: self]; 3710} 3711 3712- (void) endPreviewPanelControl: (QLPreviewPanel *) panel 3713{ 3714 [fPreviewPanel release]; 3715 fPreviewPanel = nil; 3716} 3717 3718- (NSArray *) quickLookableTorrents 3719{ 3720 NSArray * selectedTorrents = [fTableView selectedTorrents]; 3721 NSMutableArray * qlArray = [NSMutableArray arrayWithCapacity: [selectedTorrents count]]; 3722 3723 for (Torrent * torrent in selectedTorrents) 3724 if (([torrent isFolder] || [torrent isComplete]) && [torrent dataLocation]) 3725 [qlArray addObject: torrent]; 3726 3727 return qlArray; 3728} 3729 3730- (NSInteger) numberOfPreviewItemsInPreviewPanel: (QLPreviewPanel *) panel 3731{ 3732 if ([fInfoController canQuickLook]) 3733 return [[fInfoController quickLookURLs] count]; 3734 else 3735 return [[self quickLookableTorrents] count]; 3736} 3737 3738- (id <QLPreviewItem>) previewPanel: (QLPreviewPanel *) panel previewItemAtIndex: (NSInteger) index 3739{ 3740 if ([fInfoController canQuickLook]) 3741 return [[fInfoController quickLookURLs] objectAtIndex: index]; 3742 else 3743 return [[self quickLookableTorrents] objectAtIndex: index]; 3744} 3745 3746- (BOOL) previewPanel: (QLPreviewPanel *) panel handleEvent: (NSEvent *) event 3747{ 3748 /*if ([event type] == NSKeyDown) 3749 { 3750 [super keyDown: event]; 3751 return YES; 3752 }*/ 3753 3754 return NO; 3755} 3756 3757- (NSRect) previewPanel: (QLPreviewPanel *) panel sourceFrameOnScreenForPreviewItem: (id <QLPreviewItem>) item 3758{ 3759 if ([fInfoController canQuickLook]) 3760 return [fInfoController quickLookSourceFrameForPreviewItem: item]; 3761 else 3762 { 3763 if (![fWindow isVisible]) 3764 return NSZeroRect; 3765 3766 const NSInteger row = [fTableView rowForItem: item]; 3767 if (row == -1) 3768 return NSZeroRect; 3769 3770 NSRect frame = [fTableView iconRectForRow: row]; 3771 3772 if (!NSIntersectsRect([fTableView visibleRect], frame)) 3773 return NSZeroRect; 3774 3775 frame.origin = [fTableView convertPoint: frame.origin toView: nil]; 3776 frame.origin = [fWindow convertBaseToScreen: frame.origin]; 3777 frame.origin.y -= frame.size.height; 3778 return frame; 3779 } 3780} 3781 3782- (ButtonToolbarItem *) standardToolbarButtonWithIdentifier: (NSString *) ident 3783{ 3784 ButtonToolbarItem * item = [[ButtonToolbarItem alloc] initWithItemIdentifier: ident]; 3785 3786 NSButton * button = [[NSButton alloc] init]; 3787 [button setBezelStyle: NSTexturedRoundedBezelStyle]; 3788 [button setStringValue: @""]; 3789 3790 [item setView: button]; 3791 [button release]; 3792 3793 const NSSize buttonSize = NSMakeSize(36.0, 25.0); 3794 [item setMinSize: buttonSize]; 3795 [item setMaxSize: buttonSize]; 3796 3797 return [item autorelease]; 3798} 3799 3800- (NSToolbarItem *) toolbar: (NSToolbar *) toolbar itemForItemIdentifier: (NSString *) ident willBeInsertedIntoToolbar: (BOOL) flag 3801{ 3802 if ([ident isEqualToString: TOOLBAR_CREATE]) 3803 { 3804 ButtonToolbarItem * item = [self standardToolbarButtonWithIdentifier: ident]; 3805 3806 [item setLabel: NSLocalizedString(@"Create", "Create toolbar item -> label")]; 3807 [item setPaletteLabel: NSLocalizedString(@"Create Torrent File", "Create toolbar item -> palette label")]; 3808 [item setToolTip: NSLocalizedString(@"Create torrent file", "Create toolbar item -> tooltip")]; 3809 [item setImage: [NSImage imageNamed: @"ToolbarCreateTemplate"]]; 3810 [item setTarget: self]; 3811 [item setAction: @selector(createFile:)]; 3812 [item setAutovalidates: NO]; 3813 3814 return item; 3815 } 3816 else if ([ident isEqualToString: TOOLBAR_OPEN_FILE]) 3817 { 3818 ButtonToolbarItem * item = [self standardToolbarButtonWithIdentifier: ident]; 3819 3820 [item setLabel: NSLocalizedString(@"Open", "Open toolbar item -> label")]; 3821 [item setPaletteLabel: NSLocalizedString(@"Open Torrent Files", "Open toolbar item -> palette label")]; 3822 [item setToolTip: NSLocalizedString(@"Open torrent files", "Open toolbar item -> tooltip")]; 3823 [item setImage: [NSImage imageNamed: @"ToolbarOpenTemplate"]]; 3824 [item setTarget: self]; 3825 [item setAction: @selector(openShowSheet:)]; 3826 [item setAutovalidates: NO]; 3827 3828 return item; 3829 } 3830 else if ([ident isEqualToString: TOOLBAR_OPEN_WEB]) 3831 { 3832 ButtonToolbarItem * item = [self standardToolbarButtonWithIdentifier: ident]; 3833 3834 [item setLabel: NSLocalizedString(@"Open Address", "Open address toolbar item -> label")]; 3835 [item setPaletteLabel: NSLocalizedString(@"Open Torrent Address", "Open address toolbar item -> palette label")]; 3836 [item setToolTip: NSLocalizedString(@"Open torrent web address", "Open address toolbar item -> tooltip")]; 3837 [item setImage: [NSImage imageNamed: @"ToolbarOpenWebTemplate"]]; 3838 [item setTarget: self]; 3839 [item setAction: @selector(openURLShowSheet:)]; 3840 [item setAutovalidates: NO]; 3841 3842 return item; 3843 } 3844 else if ([ident isEqualToString: TOOLBAR_REMOVE]) 3845 { 3846 ButtonToolbarItem * item = [self standardToolbarButtonWithIdentifier: ident]; 3847 3848 [item setLabel: NSLocalizedString(@"Remove", "Remove toolbar item -> label")]; 3849 [item setPaletteLabel: NSLocalizedString(@"Remove Selected", "Remove toolbar item -> palette label")]; 3850 [item setToolTip: NSLocalizedString(@"Remove selected transfers", "Remove toolbar item -> tooltip")]; 3851 [item setImage: [NSImage imageNamed: @"ToolbarRemoveTemplate"]]; 3852 [item setTarget: self]; 3853 [item setAction: @selector(removeNoDelete:)]; 3854 [item setVisibilityPriority: NSToolbarItemVisibilityPriorityHigh]; 3855 3856 return item; 3857 } 3858 else if ([ident isEqualToString: TOOLBAR_INFO]) 3859 { 3860 ButtonToolbarItem * item = [self standardToolbarButtonWithIdentifier: ident]; 3861 [[(NSButton *)[item view] cell] setShowsStateBy: NSContentsCellMask]; //blue when enabled 3862 3863 [item setLabel: NSLocalizedString(@"Inspector", "Inspector toolbar item -> label")]; 3864 [item setPaletteLabel: NSLocalizedString(@"Toggle Inspector", "Inspector toolbar item -> palette label")]; 3865 [item setToolTip: NSLocalizedString(@"Toggle the torrent inspector", "Inspector toolbar item -> tooltip")]; 3866 [item setImage: [NSImage imageNamed: @"ToolbarInfoTemplate"]]; 3867 [item setTarget: self]; 3868 [item setAction: @selector(showInfo:)]; 3869 3870 return item; 3871 } 3872 else if ([ident isEqualToString: TOOLBAR_PAUSE_RESUME_ALL]) 3873 { 3874 GroupToolbarItem * groupItem = [[GroupToolbarItem alloc] initWithItemIdentifier: ident]; 3875 3876 NSSegmentedControl * segmentedControl = [[NSSegmentedControl alloc] initWithFrame: NSZeroRect]; 3877 [segmentedControl setCell: [[[ToolbarSegmentedCell alloc] init] autorelease]]; 3878 [groupItem setView: segmentedControl]; 3879 NSSegmentedCell * segmentedCell = (NSSegmentedCell *)[segmentedControl cell]; 3880 3881 [segmentedControl setSegmentCount: 2]; 3882 [segmentedCell setTrackingMode: NSSegmentSwitchTrackingMomentary]; 3883 3884 const NSSize groupSize = NSMakeSize(72.0, 25.0); 3885 [groupItem setMinSize: groupSize]; 3886 [groupItem setMaxSize: groupSize]; 3887 3888 [groupItem setLabel: NSLocalizedString(@"Apply All", "All toolbar item -> label")]; 3889 [groupItem setPaletteLabel: NSLocalizedString(@"Pause / Resume All", "All toolbar item -> palette label")]; 3890 [groupItem setTarget: self]; 3891 [groupItem setAction: @selector(allToolbarClicked:)]; 3892 3893 [groupItem setIdentifiers: [NSArray arrayWithObjects: TOOLBAR_PAUSE_ALL, TOOLBAR_RESUME_ALL, nil]]; 3894 3895 [segmentedCell setTag: TOOLBAR_PAUSE_TAG forSegment: TOOLBAR_PAUSE_TAG]; 3896 [segmentedControl setImage: [NSImage imageNamed: @"ToolbarPauseAllTemplate"] forSegment: TOOLBAR_PAUSE_TAG]; 3897 [segmentedCell setToolTip: NSLocalizedString(@"Pause all transfers", 3898 "All toolbar item -> tooltip") forSegment: TOOLBAR_PAUSE_TAG]; 3899 3900 [segmentedCell setTag: TOOLBAR_RESUME_TAG forSegment: TOOLBAR_RESUME_TAG]; 3901 [segmentedControl setImage: [NSImage imageNamed: @"ToolbarResumeAllTemplate"] forSegment: TOOLBAR_RESUME_TAG]; 3902 [segmentedCell setToolTip: NSLocalizedString(@"Resume all transfers", 3903 "All toolbar item -> tooltip") forSegment: TOOLBAR_RESUME_TAG]; 3904 3905 [groupItem createMenu: [NSArray arrayWithObjects: NSLocalizedString(@"Pause All", "All toolbar item -> label"), 3906 NSLocalizedString(@"Resume All", "All toolbar item -> label"), nil]]; 3907 3908 [segmentedControl release]; 3909 3910 [groupItem setVisibilityPriority: NSToolbarItemVisibilityPriorityHigh]; 3911 3912 return [groupItem autorelease]; 3913 } 3914 else if ([ident isEqualToString: TOOLBAR_PAUSE_RESUME_SELECTED]) 3915 { 3916 GroupToolbarItem * groupItem = [[GroupToolbarItem alloc] initWithItemIdentifier: ident]; 3917 3918 NSSegmentedControl * segmentedControl = [[NSSegmentedControl alloc] initWithFrame: NSZeroRect]; 3919 [segmentedControl setCell: [[[ToolbarSegmentedCell alloc] init] autorelease]]; 3920 [groupItem setView: segmentedControl]; 3921 NSSegmentedCell * segmentedCell = (NSSegmentedCell *)[segmentedControl cell]; 3922 3923 [segmentedControl setSegmentCount: 2]; 3924 [segmentedCell setTrackingMode: NSSegmentSwitchTrackingMomentary]; 3925 3926 const NSSize groupSize = NSMakeSize(72.0, 25.0); 3927 [groupItem setMinSize: groupSize]; 3928 [groupItem setMaxSize: groupSize]; 3929 3930 [groupItem setLabel: NSLocalizedString(@"Apply Selected", "Selected toolbar item -> label")]; 3931 [groupItem setPaletteLabel: NSLocalizedString(@"Pause / Resume Selected", "Selected toolbar item -> palette label")]; 3932 [groupItem setTarget: self]; 3933 [groupItem setAction: @selector(selectedToolbarClicked:)]; 3934 3935 [groupItem setIdentifiers: [NSArray arrayWithObjects: TOOLBAR_PAUSE_SELECTED, TOOLBAR_RESUME_SELECTED, nil]]; 3936 3937 [segmentedCell setTag: TOOLBAR_PAUSE_TAG forSegment: TOOLBAR_PAUSE_TAG]; 3938 [segmentedControl setImage: [NSImage imageNamed: @"ToolbarPauseSelectedTemplate"] forSegment: TOOLBAR_PAUSE_TAG]; 3939 [segmentedCell setToolTip: NSLocalizedString(@"Pause selected transfers", 3940 "Selected toolbar item -> tooltip") forSegment: TOOLBAR_PAUSE_TAG]; 3941 3942 [segmentedCell setTag: TOOLBAR_RESUME_TAG forSegment: TOOLBAR_RESUME_TAG]; 3943 [segmentedControl setImage: [NSImage imageNamed: @"ToolbarResumeSelectedTemplate"] forSegment: TOOLBAR_RESUME_TAG]; 3944 [segmentedCell setToolTip: NSLocalizedString(@"Resume selected transfers", 3945 "Selected toolbar item -> tooltip") forSegment: TOOLBAR_RESUME_TAG]; 3946 3947 [groupItem createMenu: [NSArray arrayWithObjects: NSLocalizedString(@"Pause Selected", "Selected toolbar item -> label"), 3948 NSLocalizedString(@"Resume Selected", "Selected toolbar item -> label"), nil]]; 3949 3950 [segmentedControl release]; 3951 3952 [groupItem setVisibilityPriority: NSToolbarItemVisibilityPriorityHigh]; 3953 3954 return [groupItem autorelease]; 3955 } 3956 else if ([ident isEqualToString: TOOLBAR_FILTER]) 3957 { 3958 ButtonToolbarItem * item = [self standardToolbarButtonWithIdentifier: ident]; 3959 [[(NSButton *)[item view] cell] setShowsStateBy: NSContentsCellMask]; //blue when enabled 3960 3961 [item setLabel: NSLocalizedString(@"Filter", "Filter toolbar item -> label")]; 3962 [item setPaletteLabel: NSLocalizedString(@"Toggle Filter", "Filter toolbar item -> palette label")]; 3963 [item setToolTip: NSLocalizedString(@"Toggle the filter bar", "Filter toolbar item -> tooltip")]; 3964 [item setImage: [NSImage imageNamed: @"ToolbarFilterTemplate"]]; 3965 [item setTarget: self]; 3966 [item setAction: @selector(toggleFilterBar:)]; 3967 3968 return item; 3969 } 3970 else if ([ident isEqualToString: TOOLBAR_QUICKLOOK]) 3971 { 3972 ButtonToolbarItem * item = [self standardToolbarButtonWithIdentifier: ident]; 3973 [[(NSButton *)[item view] cell] setShowsStateBy: NSContentsCellMask]; //blue when enabled 3974 3975 [item setLabel: NSLocalizedString(@"Quick Look", "QuickLook toolbar item -> label")]; 3976 [item setPaletteLabel: NSLocalizedString(@"Quick Look", "QuickLook toolbar item -> palette label")]; 3977 [item setToolTip: NSLocalizedString(@"Quick Look", "QuickLook toolbar item -> tooltip")]; 3978 [item setImage: [NSImage imageNamed: NSImageNameQuickLookTemplate]]; 3979 [item setTarget: self]; 3980 [item setAction: @selector(toggleQuickLook:)]; 3981 [item setVisibilityPriority: NSToolbarItemVisibilityPriorityLow]; 3982 3983 return item; 3984 } 3985 else 3986 return nil; 3987} 3988 3989- (void) allToolbarClicked: (id) sender 3990{ 3991 NSInteger tagValue = [sender isKindOfClass: [NSSegmentedControl class]] 3992 ? [(NSSegmentedCell *)[sender cell] tagForSegment: [sender selectedSegment]] : [(NSControl *)sender tag]; 3993 switch (tagValue) 3994 { 3995 case TOOLBAR_PAUSE_TAG: 3996 [self stopAllTorrents: sender]; 3997 break; 3998 case TOOLBAR_RESUME_TAG: 3999 [self resumeAllTorrents: sender]; 4000 break; 4001 } 4002} 4003 4004- (void) selectedToolbarClicked: (id) sender 4005{ 4006 NSInteger tagValue = [sender isKindOfClass: [NSSegmentedControl class]] 4007 ? [(NSSegmentedCell *)[sender cell] tagForSegment: [sender selectedSegment]] : [(NSControl *)sender tag]; 4008 switch (tagValue) 4009 { 4010 case TOOLBAR_PAUSE_TAG: 4011 [self stopSelectedTorrents: sender]; 4012 break; 4013 case TOOLBAR_RESUME_TAG: 4014 [self resumeSelectedTorrents: sender]; 4015 break; 4016 } 4017} 4018 4019- (NSArray *) toolbarAllowedItemIdentifiers: (NSToolbar *) toolbar 4020{ 4021 return [NSArray arrayWithObjects: 4022 TOOLBAR_CREATE, TOOLBAR_OPEN_FILE, TOOLBAR_OPEN_WEB, TOOLBAR_REMOVE, 4023 TOOLBAR_PAUSE_RESUME_SELECTED, TOOLBAR_PAUSE_RESUME_ALL, 4024 TOOLBAR_QUICKLOOK, TOOLBAR_FILTER, TOOLBAR_INFO, 4025 NSToolbarSeparatorItemIdentifier, 4026 NSToolbarSpaceItemIdentifier, 4027 NSToolbarFlexibleSpaceItemIdentifier, 4028 NSToolbarCustomizeToolbarItemIdentifier, nil]; 4029} 4030 4031- (NSArray *) toolbarDefaultItemIdentifiers: (NSToolbar *) toolbar 4032{ 4033 return [NSArray arrayWithObjects: 4034 TOOLBAR_CREATE, TOOLBAR_OPEN_FILE, TOOLBAR_REMOVE, NSToolbarSpaceItemIdentifier, 4035 TOOLBAR_PAUSE_RESUME_ALL, NSToolbarFlexibleSpaceItemIdentifier, 4036 TOOLBAR_QUICKLOOK, TOOLBAR_FILTER, TOOLBAR_INFO, nil]; 4037} 4038 4039- (BOOL) validateToolbarItem: (NSToolbarItem *) toolbarItem 4040{ 4041 NSString * ident = [toolbarItem itemIdentifier]; 4042 4043 //enable remove item 4044 if ([ident isEqualToString: TOOLBAR_REMOVE]) 4045 return [fTableView numberOfSelectedRows] > 0; 4046 4047 //enable pause all item 4048 if ([ident isEqualToString: TOOLBAR_PAUSE_ALL]) 4049 { 4050 for (Torrent * torrent in fTorrents) 4051 if ([torrent isActive] || [torrent waitingToStart]) 4052 return YES; 4053 return NO; 4054 } 4055 4056 //enable resume all item 4057 if ([ident isEqualToString: TOOLBAR_RESUME_ALL]) 4058 { 4059 for (Torrent * torrent in fTorrents) 4060 if (![torrent isActive] && ![torrent waitingToStart] && ![torrent isFinishedSeeding]) 4061 return YES; 4062 return NO; 4063 } 4064 4065 //enable pause item 4066 if ([ident isEqualToString: TOOLBAR_PAUSE_SELECTED]) 4067 { 4068 for (Torrent * torrent in [fTableView selectedTorrents]) 4069 if ([torrent isActive] || [torrent waitingToStart]) 4070 return YES; 4071 return NO; 4072 } 4073 4074 //enable resume item 4075 if ([ident isEqualToString: TOOLBAR_RESUME_SELECTED]) 4076 { 4077 for (Torrent * torrent in [fTableView selectedTorrents]) 4078 if (![torrent isActive] && ![torrent waitingToStart]) 4079 return YES; 4080 return NO; 4081 } 4082 4083 //set info item 4084 if ([ident isEqualToString: TOOLBAR_INFO]) 4085 { 4086 [(NSButton *)[toolbarItem view] setState: [[fInfoController window] isVisible]]; 4087 return YES; 4088 } 4089 4090 //set filter item 4091 if ([ident isEqualToString: TOOLBAR_FILTER]) 4092 { 4093 [(NSButton *)[toolbarItem view] setState: fFilterBar != nil]; 4094 return YES; 4095 } 4096 4097 //set quick look item 4098 if ([ident isEqualToString: TOOLBAR_QUICKLOOK]) 4099 { 4100 [(NSButton *)[toolbarItem view] setState: [QLPreviewPanel sharedPreviewPanelExists] && [[QLPreviewPanel sharedPreviewPanel] isVisible]]; 4101 return YES; 4102 } 4103 4104 return YES; 4105} 4106 4107- (BOOL) validateMenuItem: (NSMenuItem *) menuItem 4108{ 4109 SEL action = [menuItem action]; 4110 4111 if (action == @selector(toggleSpeedLimit:)) 4112 { 4113 [menuItem setState: [fDefaults boolForKey: @"SpeedLimit"] ? NSOnState : NSOffState]; 4114 return YES; 4115 } 4116 4117 //only enable some items if it is in a context menu or the window is useable 4118 BOOL canUseTable = [fWindow isKeyWindow] || [[menuItem menu] supermenu] != [NSApp mainMenu]; 4119 4120 //enable open items 4121 if (action == @selector(openShowSheet:) || action == @selector(openURLShowSheet:)) 4122 return [fWindow attachedSheet] == nil; 4123 4124 //enable sort options 4125 if (action == @selector(setSort:)) 4126 { 4127 NSString * sortType; 4128 switch ([menuItem tag]) 4129 { 4130 case SORT_ORDER_TAG: 4131 sortType = SORT_ORDER; 4132 break; 4133 case SORT_DATE_TAG: 4134 sortType = SORT_DATE; 4135 break; 4136 case SORT_NAME_TAG: 4137 sortType = SORT_NAME; 4138 break; 4139 case SORT_PROGRESS_TAG: 4140 sortType = SORT_PROGRESS; 4141 break; 4142 case SORT_STATE_TAG: 4143 sortType = SORT_STATE; 4144 break; 4145 case SORT_TRACKER_TAG: 4146 sortType = SORT_TRACKER; 4147 break; 4148 case SORT_ACTIVITY_TAG: 4149 sortType = SORT_ACTIVITY; 4150 break; 4151 case SORT_SIZE_TAG: 4152 sortType = SORT_SIZE; 4153 break; 4154 default: 4155 NSAssert1(NO, @"Unknown sort tag received: %ld", [menuItem tag]); 4156 } 4157 4158 [menuItem setState: [sortType isEqualToString: [fDefaults stringForKey: @"Sort"]] ? NSOnState : NSOffState]; 4159 return [fWindow isVisible]; 4160 } 4161 4162 if (action == @selector(setGroup:)) 4163 { 4164 BOOL checked = NO; 4165 4166 NSInteger index = [menuItem tag]; 4167 for (Torrent * torrent in [fTableView selectedTorrents]) 4168 if (index == [torrent groupValue]) 4169 { 4170 checked = YES; 4171 break; 4172 } 4173 4174 [menuItem setState: checked ? NSOnState : NSOffState]; 4175 return canUseTable && [fTableView numberOfSelectedRows] > 0; 4176 } 4177 4178 if (action == @selector(toggleSmallView:)) 4179 { 4180 [menuItem setState: [fDefaults boolForKey: @"SmallView"] ? NSOnState : NSOffState]; 4181 return [fWindow isVisible]; 4182 } 4183 4184 if (action == @selector(togglePiecesBar:)) 4185 { 4186 [menuItem setState: [fDefaults boolForKey: @"PiecesBar"] ? NSOnState : NSOffState]; 4187 return [fWindow isVisible]; 4188 } 4189 4190 #warning remove when menu is removed (10.7-only) 4191 if (action == @selector(toggleStatusString:)) 4192 { 4193 if ([fDefaults boolForKey: @"SmallView"]) 4194 { 4195 [menuItem setTitle: NSLocalizedString(@"Remaining Time", "Action menu -> status string toggle")]; 4196 [menuItem setState: ![fDefaults boolForKey: @"DisplaySmallStatusRegular"] ? NSOnState : NSOffState]; 4197 } 4198 else 4199 { 4200 [menuItem setTitle: NSLocalizedString(@"Status of Selected Files", "Action menu -> status string toggle")]; 4201 [menuItem setState: [fDefaults boolForKey: @"DisplayStatusProgressSelected"] ? NSOnState : NSOffState]; 4202 } 4203 4204 return [fWindow isVisible]; 4205 } 4206 4207 if (action == @selector(toggleAvailabilityBar:)) 4208 { 4209 [menuItem setState: [fDefaults boolForKey: @"DisplayProgressBarAvailable"] ? NSOnState : NSOffState]; 4210 return [fWindow isVisible]; 4211 } 4212 4213 if (action == @selector(setLimitGlobalEnabled:)) 4214 { 4215 BOOL upload = [menuItem menu] == fUploadMenu; 4216 BOOL limit = menuItem == (upload ? fUploadLimitItem : fDownloadLimitItem); 4217 if (limit) 4218 [menuItem setTitle: [NSString stringWithFormat: NSLocalizedString(@"Limit (%d KB/s)", 4219 "Action menu -> upload/download limit"), 4220 [fDefaults integerForKey: upload ? @"UploadLimit" : @"DownloadLimit"]]]; 4221 4222 [menuItem setState: [fDefaults boolForKey: upload ? @"CheckUpload" : @"CheckDownload"] ? limit : !limit]; 4223 return YES; 4224 } 4225 4226 if (action == @selector(setRatioGlobalEnabled:)) 4227 { 4228 BOOL check = menuItem == fCheckRatioItem; 4229 if (check) 4230 [menuItem setTitle: [NSString localizedStringWithFormat: NSLocalizedString(@"Stop at Ratio (%.2f)", 4231 "Action menu -> ratio stop"), [fDefaults floatForKey: @"RatioLimit"]]]; 4232 4233 [menuItem setState: [fDefaults boolForKey: @"RatioCheck"] ? check : !check]; 4234 return YES; 4235 } 4236 4237 //enable show info 4238 if (action == @selector(showInfo:)) 4239 { 4240 NSString * title = [[fInfoController window] isVisible] ? NSLocalizedString(@"Hide Inspector", "View menu -> Inspector") 4241 : NSLocalizedString(@"Show Inspector", "View menu -> Inspector"); 4242 [menuItem setTitle: title]; 4243 4244 return YES; 4245 } 4246 4247 //enable prev/next inspector tab 4248 if (action == @selector(setInfoTab:)) 4249 return [[fInfoController window] isVisible]; 4250 4251 //enable toggle status bar 4252 if (action == @selector(toggleStatusBar:)) 4253 { 4254 NSString * title = !fStatusBar ? NSLocalizedString(@"Show Status Bar", "View menu -> Status Bar") 4255 : NSLocalizedString(@"Hide Status Bar", "View menu -> Status Bar"); 4256 [menuItem setTitle: title]; 4257 4258 return [fWindow isVisible]; 4259 } 4260 4261 //enable toggle filter bar 4262 if (action == @selector(toggleFilterBar:)) 4263 { 4264 NSString * title = !fFilterBar ? NSLocalizedString(@"Show Filter Bar", "View menu -> Filter Bar") 4265 : NSLocalizedString(@"Hide Filter Bar", "View menu -> Filter Bar"); 4266 [menuItem setTitle: title]; 4267 4268 return [fWindow isVisible]; 4269 } 4270 4271 //enable prev/next filter button 4272 if (action == @selector(switchFilter:)) 4273 return [fWindow isVisible] && fFilterBar; 4274 4275 //enable reveal in finder 4276 if (action == @selector(revealFile:)) 4277 return canUseTable && [fTableView numberOfSelectedRows] > 0; 4278 4279 //enable remove items 4280 if (action == @selector(removeNoDelete:) || action == @selector(removeDeleteData:)) 4281 { 4282 BOOL warning = NO; 4283 4284 for (Torrent * torrent in [fTableView selectedTorrents]) 4285 { 4286 if ([torrent isActive]) 4287 { 4288 if ([fDefaults boolForKey: @"CheckRemoveDownloading"] ? ![torrent isSeeding] : YES) 4289 { 4290 warning = YES; 4291 break; 4292 } 4293 } 4294 } 4295 4296 //append or remove ellipsis when needed 4297 NSString * title = [menuItem title], * ellipsis = [NSString ellipsis]; 4298 if (warning && [fDefaults boolForKey: @"CheckRemove"]) 4299 { 4300 if (![title hasSuffix: ellipsis]) 4301 [menuItem setTitle: [title stringByAppendingEllipsis]]; 4302 } 4303 else 4304 { 4305 if ([title hasSuffix: ellipsis]) 4306 [menuItem setTitle: [title substringToIndex: [title rangeOfString: ellipsis].location]]; 4307 } 4308 4309 return canUseTable && [fTableView numberOfSelectedRows] > 0; 4310 } 4311 4312 //remove all completed transfers item 4313 if (action == @selector(clearCompleted:)) 4314 { 4315 //append or remove ellipsis when needed 4316 NSString * title = [menuItem title], * ellipsis = [NSString ellipsis]; 4317 if ([fDefaults boolForKey: @"WarningRemoveCompleted"]) 4318 { 4319 if (![title hasSuffix: ellipsis]) 4320 [menuItem setTitle: [title stringByAppendingEllipsis]]; 4321 } 4322 else 4323 { 4324 if ([title hasSuffix: ellipsis]) 4325 [menuItem setTitle: [title substringToIndex: [title rangeOfString: ellipsis].location]]; 4326 } 4327 4328 for (Torrent * torrent in fTorrents) 4329 if ([torrent isFinishedSeeding]) 4330 return YES; 4331 return NO; 4332 } 4333 4334 //enable pause all item 4335 if (action == @selector(stopAllTorrents:)) 4336 { 4337 for (Torrent * torrent in fTorrents) 4338 if ([torrent isActive] || [torrent waitingToStart]) 4339 return YES; 4340 return NO; 4341 } 4342 4343 //enable resume all item 4344 if (action == @selector(resumeAllTorrents:)) 4345 { 4346 for (Torrent * torrent in fTorrents) 4347 if (![torrent isActive] && ![torrent waitingToStart] && ![torrent isFinishedSeeding]) 4348 return YES; 4349 return NO; 4350 } 4351 4352 //enable resume all waiting item 4353 if (action == @selector(resumeWaitingTorrents:)) 4354 { 4355 if (![fDefaults boolForKey: @"Queue"] && ![fDefaults boolForKey: @"QueueSeed"]) 4356 return NO; 4357 4358 for (Torrent * torrent in fTorrents) 4359 if ([torrent waitingToStart]) 4360 return YES; 4361 return NO; 4362 } 4363 4364 //enable resume selected waiting item 4365 if (action == @selector(resumeSelectedTorrentsNoWait:)) 4366 { 4367 if (!canUseTable) 4368 return NO; 4369 4370 for (Torrent * torrent in [fTableView selectedTorrents]) 4371 if (![torrent isActive]) 4372 return YES; 4373 return NO; 4374 } 4375 4376 //enable pause item 4377 if (action == @selector(stopSelectedTorrents:)) 4378 { 4379 if (!canUseTable) 4380 return NO; 4381 4382 for (Torrent * torrent in [fTableView selectedTorrents]) 4383 if ([torrent isActive] || [torrent waitingToStart]) 4384 return YES; 4385 return NO; 4386 } 4387 4388 //enable resume item 4389 if (action == @selector(resumeSelectedTorrents:)) 4390 { 4391 if (!canUseTable) 4392 return NO; 4393 4394 for (Torrent * torrent in [fTableView selectedTorrents]) 4395 if (![torrent isActive] && ![torrent waitingToStart]) 4396 return YES; 4397 return NO; 4398 } 4399 4400 //enable manual announce item 4401 if (action == @selector(announceSelectedTorrents:)) 4402 { 4403 if (!canUseTable) 4404 return NO; 4405 4406 for (Torrent * torrent in [fTableView selectedTorrents]) 4407 if ([torrent canManualAnnounce]) 4408 return YES; 4409 return NO; 4410 } 4411 4412 //enable reset cache item 4413 if (action == @selector(verifySelectedTorrents:)) 4414 { 4415 if (!canUseTable) 4416 return NO; 4417 4418 for (Torrent * torrent in [fTableView selectedTorrents]) 4419 if (![torrent isMagnet]) 4420 return YES; 4421 return NO; 4422 } 4423 4424 //enable move torrent file item 4425 if (action == @selector(moveDataFilesSelected:)) 4426 return canUseTable && [fTableView numberOfSelectedRows] > 0; 4427 4428 //enable copy torrent file item 4429 if (action == @selector(copyTorrentFiles:)) 4430 { 4431 if (!canUseTable) 4432 return NO; 4433 4434 for (Torrent * torrent in [fTableView selectedTorrents]) 4435 if (![torrent isMagnet]) 4436 return YES; 4437 return NO; 4438 } 4439 4440 //enable copy torrent file item 4441 if (action == @selector(copyMagnetLinks:)) 4442 return canUseTable && [fTableView numberOfSelectedRows] > 0; 4443 4444 //enable reverse sort item 4445 if (action == @selector(setSortReverse:)) 4446 { 4447 const BOOL isReverse = [menuItem tag] == SORT_DESC_TAG; 4448 [menuItem setState: (isReverse == [fDefaults boolForKey: @"SortReverse"]) ? NSOnState : NSOffState]; 4449 return ![[fDefaults stringForKey: @"Sort"] isEqualToString: SORT_ORDER]; 4450 } 4451 4452 //enable group sort item 4453 if (action == @selector(setSortByGroup:)) 4454 { 4455 [menuItem setState: [fDefaults boolForKey: @"SortByGroup"] ? NSOnState : NSOffState]; 4456 return YES; 4457 } 4458 4459 if (action == @selector(toggleQuickLook:)) 4460 { 4461 const BOOL visible =[QLPreviewPanel sharedPreviewPanelExists] && [[QLPreviewPanel sharedPreviewPanel] isVisible]; 4462 //text consistent with Finder 4463 NSString * title = !visible ? NSLocalizedString(@"Quick Look", "View menu -> Quick Look") 4464 : NSLocalizedString(@"Close Quick Look", "View menu -> Quick Look"); 4465 [menuItem setTitle: title]; 4466 4467 return YES; 4468 } 4469 4470 return YES; 4471} 4472 4473- (void) sleepCallback: (natural_t) messageType argument: (void *) messageArgument 4474{ 4475 switch (messageType) 4476 { 4477 case kIOMessageSystemWillSleep: 4478 //if there are any running transfers, wait 15 seconds for them to stop 4479 for (Torrent * torrent in fTorrents) 4480 if ([torrent isActive]) 4481 { 4482 //stop all transfers (since some are active) before going to sleep and remember to resume when we wake up 4483 for (Torrent * torrent in fTorrents) 4484 [torrent sleep]; 4485 sleep(15); 4486 break; 4487 } 4488 4489 IOAllowPowerChange(fRootPort, (long) messageArgument); 4490 break; 4491 4492 case kIOMessageCanSystemSleep: 4493 if ([fDefaults boolForKey: @"SleepPrevent"]) 4494 { 4495 //prevent idle sleep unless no torrents are active 4496 for (Torrent * torrent in fTorrents) 4497 if ([torrent isActive] && ![torrent isStalled] && ![torrent isError]) 4498 { 4499 IOCancelPowerChange(fRootPort, (long) messageArgument); 4500 return; 4501 } 4502 } 4503 4504 IOAllowPowerChange(fRootPort, (long) messageArgument); 4505 break; 4506 4507 case kIOMessageSystemHasPoweredOn: 4508 //resume sleeping transfers after we wake up 4509 for (Torrent * torrent in fTorrents) 4510 [torrent wakeUp]; 4511 break; 4512 } 4513} 4514 4515- (NSMenu *) applicationDockMenu: (NSApplication *) sender 4516{ 4517 if (fQuitting) 4518 return nil; 4519 4520 NSUInteger seeding = 0, downloading = 0; 4521 for (Torrent * torrent in fTorrents) 4522 { 4523 if ([torrent isSeeding]) 4524 seeding++; 4525 else if ([torrent isActive]) 4526 downloading++; 4527 else; 4528 } 4529 4530 NSMenu * menu = [[NSMenu alloc] init]; 4531 4532 if (seeding > 0) 4533 { 4534 NSString * title = [NSString stringWithFormat: NSLocalizedString(@"%d Seeding", "Dock item - Seeding"), seeding]; 4535 [menu addItemWithTitle: title action: nil keyEquivalent: @""]; 4536 } 4537 4538 if (downloading > 0) 4539 { 4540 NSString * title = [NSString stringWithFormat: NSLocalizedString(@"%d Downloading", "Dock item - Downloading"), downloading]; 4541 [menu addItemWithTitle: title action: nil keyEquivalent: @""]; 4542 } 4543 4544 if (seeding > 0 || downloading > 0) 4545 [menu addItem: [NSMenuItem separatorItem]]; 4546 4547 [menu addItemWithTitle: NSLocalizedString(@"Pause All", "Dock item") action: @selector(stopAllTorrents:) keyEquivalent: @""]; 4548 [menu addItemWithTitle: NSLocalizedString(@"Resume All", "Dock item") action: @selector(resumeAllTorrents:) keyEquivalent: @""]; 4549 [menu addItem: [NSMenuItem separatorItem]]; 4550 [menu addItemWithTitle: NSLocalizedString(@"Speed Limit", "Dock item") action: @selector(toggleSpeedLimit:) keyEquivalent: @""]; 4551 4552 return [menu autorelease]; 4553} 4554 4555- (NSRect) windowWillUseStandardFrame: (NSWindow *) window defaultFrame: (NSRect) defaultFrame 4556{ 4557 //if auto size is enabled, the current frame shouldn't need to change 4558 NSRect frame = [fDefaults boolForKey: @"AutoSize"] ? [window frame] : [self sizedWindowFrame]; 4559 4560 frame.size.width = [fDefaults boolForKey: @"SmallView"] ? [fWindow minSize].width : WINDOW_REGULAR_WIDTH; 4561 return frame; 4562} 4563 4564- (void) setWindowSizeToFit 4565{ 4566 if ([fDefaults boolForKey: @"AutoSize"]) 4567 { 4568 NSScrollView * scrollView = [fTableView enclosingScrollView]; 4569 4570 [scrollView setHasVerticalScroller: NO]; 4571 [fWindow setFrame: [self sizedWindowFrame] display: YES animate: YES]; 4572 [scrollView setHasVerticalScroller: YES]; 4573 4574 [self setWindowMinMaxToCurrent]; 4575 } 4576} 4577 4578- (NSRect) sizedWindowFrame 4579{ 4580 NSUInteger groups = ([fDisplayedTorrents count] > 0 && ![[fDisplayedTorrents objectAtIndex: 0] isKindOfClass: [Torrent class]]) 4581 ? [fDisplayedTorrents count] : 0; 4582 4583 CGFloat heightChange = (GROUP_SEPARATOR_HEIGHT + [fTableView intercellSpacing].height) * groups 4584 + ([fTableView rowHeight] + [fTableView intercellSpacing].height) * ([fTableView numberOfRows] - groups) 4585 - NSHeight([[fTableView enclosingScrollView] frame]); 4586 4587 return [self windowFrameByAddingHeight: heightChange checkLimits: YES]; 4588} 4589 4590- (void) updateForAutoSize 4591{ 4592 if ([fDefaults boolForKey: @"AutoSize"]) 4593 [self setWindowSizeToFit]; 4594 else 4595 { 4596 NSSize contentMinSize = [fWindow contentMinSize]; 4597 contentMinSize.height = [self minWindowContentSizeAllowed]; 4598 4599 [fWindow setContentMinSize: contentMinSize]; 4600 4601 NSSize contentMaxSize = [fWindow contentMaxSize]; 4602 contentMaxSize.height = FLT_MAX; 4603 [fWindow setContentMaxSize: contentMaxSize]; 4604 } 4605} 4606 4607- (void) setWindowMinMaxToCurrent 4608{ 4609 const CGFloat height = NSHeight([[fWindow contentView] frame]); 4610 4611 NSSize minSize = [fWindow contentMinSize], 4612 maxSize = [fWindow contentMaxSize]; 4613 minSize.height = height; 4614 maxSize.height = height; 4615 4616 [fWindow setContentMinSize: minSize]; 4617 [fWindow setContentMaxSize: maxSize]; 4618} 4619 4620- (CGFloat) minWindowContentSizeAllowed 4621{ 4622 CGFloat contentMinHeight = NSHeight([[fWindow contentView] frame]) - NSHeight([[fTableView enclosingScrollView] frame]) 4623 + [fTableView rowHeight] + [fTableView intercellSpacing].height; 4624 return contentMinHeight; 4625} 4626 4627- (void) updateForExpandCollape 4628{ 4629 [self setWindowSizeToFit]; 4630 [self setBottomCountText: YES]; 4631} 4632 4633- (void) showMainWindow: (id) sender 4634{ 4635 [fWindow makeKeyAndOrderFront: nil]; 4636} 4637 4638- (void) windowDidBecomeMain: (NSNotification *) notification 4639{ 4640 [fBadger clearCompleted]; 4641 [self updateUI]; 4642} 4643 4644- (void) applicationWillUnhide: (NSNotification *) notification 4645{ 4646 [self updateUI]; 4647} 4648 4649- (void) toggleQuickLook: (id) sender 4650{ 4651 if ([[QLPreviewPanel sharedPreviewPanel] isVisible]) 4652 [[QLPreviewPanel sharedPreviewPanel] orderOut: nil]; 4653 else 4654 [[QLPreviewPanel sharedPreviewPanel] makeKeyAndOrderFront: nil]; 4655} 4656 4657- (void) linkHomepage: (id) sender 4658{ 4659 [[NSWorkspace sharedWorkspace] openURL: [NSURL URLWithString: WEBSITE_URL]]; 4660} 4661 4662- (void) linkForums: (id) sender 4663{ 4664 [[NSWorkspace sharedWorkspace] openURL: [NSURL URLWithString: FORUM_URL]]; 4665} 4666 4667- (void) linkTrac: (id) sender 4668{ 4669 [[NSWorkspace sharedWorkspace] openURL: [NSURL URLWithString: TRAC_URL]]; 4670} 4671 4672- (void) linkDonate: (id) sender 4673{ 4674 [[NSWorkspace sharedWorkspace] openURL: [NSURL URLWithString: DONATE_URL]]; 4675} 4676 4677- (void) updaterWillRelaunchApplication: (SUUpdater *) updater 4678{ 4679 fQuitRequested = YES; 4680} 4681 4682- (NSDictionary *) registrationDictionaryForGrowl 4683{ 4684 NSArray * notifications = @[GROWL_DOWNLOAD_COMPLETE, GROWL_SEEDING_COMPLETE, GROWL_AUTO_ADD, GROWL_AUTO_SPEED_LIMIT]; 4685 4686 return @{GROWL_NOTIFICATIONS_ALL : notifications, 4687 GROWL_NOTIFICATIONS_DEFAULT : notifications }; 4688} 4689 4690- (void) growlNotificationWasClicked: (id) clickContext 4691{ 4692 if (![clickContext isKindOfClass: [NSDictionary class]]) 4693 return; 4694 4695 NSString * type = [clickContext objectForKey: @"Type"], * location; 4696 if (([type isEqualToString: GROWL_DOWNLOAD_COMPLETE] || [type isEqualToString: GROWL_SEEDING_COMPLETE]) 4697 && (location = [clickContext objectForKey: @"Location"])) 4698 { 4699 NSURL * file = [NSURL fileURLWithPath: location]; 4700 [[NSWorkspace sharedWorkspace] activateFileViewerSelectingURLs: [NSArray arrayWithObject: file]]; 4701 } 4702} 4703 4704- (void) rpcCallback: (tr_rpc_callback_type) type forTorrentStruct: (struct tr_torrent *) torrentStruct 4705{ 4706 @autoreleasepool 4707 { 4708 //get the torrent 4709 Torrent * torrent = nil; 4710 if (torrentStruct != NULL && (type != TR_RPC_TORRENT_ADDED && type != TR_RPC_SESSION_CHANGED && type != TR_RPC_SESSION_CLOSE)) 4711 { 4712 for (torrent in fTorrents) 4713 if (torrentStruct == [torrent torrentStruct]) 4714 { 4715 [torrent retain]; 4716 break; 4717 } 4718 4719 if (!torrent) 4720 { 4721 NSLog(@"No torrent found matching the given torrent struct from the RPC callback!"); 4722 return; 4723 } 4724 } 4725 4726 switch (type) 4727 { 4728 case TR_RPC_TORRENT_ADDED: 4729 [self performSelectorOnMainThread: @selector(rpcAddTorrentStruct:) withObject: 4730 [[NSValue valueWithPointer: torrentStruct] retain] waitUntilDone: NO]; 4731 break; 4732 4733 case TR_RPC_TORRENT_STARTED: 4734 case TR_RPC_TORRENT_STOPPED: 4735 [self performSelectorOnMainThread: @selector(rpcStartedStoppedTorrent:) withObject: torrent waitUntilDone: NO]; 4736 break; 4737 4738 case TR_RPC_TORRENT_REMOVING: 4739 [self performSelectorOnMainThread: @selector(rpcRemoveTorrent:) withObject: torrent waitUntilDone: NO]; 4740 break; 4741 4742 case TR_RPC_TORRENT_TRASHING: 4743 [self performSelectorOnMainThread: @selector(rpcRemoveTorrentDeleteData:) withObject: torrent waitUntilDone: NO]; 4744 break; 4745 4746 case TR_RPC_TORRENT_CHANGED: 4747 [self performSelectorOnMainThread: @selector(rpcChangedTorrent:) withObject: torrent waitUntilDone: NO]; 4748 break; 4749 4750 case TR_RPC_TORRENT_MOVED: 4751 [self performSelectorOnMainThread: @selector(rpcMovedTorrent:) withObject: torrent waitUntilDone: NO]; 4752 break; 4753 4754 case TR_RPC_SESSION_QUEUE_POSITIONS_CHANGED: 4755 [self performSelectorOnMainThread: @selector(rpcUpdateQueue) withObject: nil waitUntilDone: NO]; 4756 break; 4757 4758 case TR_RPC_SESSION_CHANGED: 4759 [fPrefsController performSelectorOnMainThread: @selector(rpcUpdatePrefs) withObject: nil waitUntilDone: NO]; 4760 break; 4761 4762 case TR_RPC_SESSION_CLOSE: 4763 fQuitRequested = YES; 4764 [NSApp performSelectorOnMainThread: @selector(terminate:) withObject: self waitUntilDone: NO]; 4765 break; 4766 4767 default: 4768 NSAssert1(NO, @"Unknown RPC command received: %d", type); 4769 [torrent release]; 4770 } 4771 } 4772} 4773 4774- (void) rpcAddTorrentStruct: (NSValue *) torrentStructPtr 4775{ 4776 tr_torrent * torrentStruct = (tr_torrent *)[torrentStructPtr pointerValue]; 4777 [torrentStructPtr release]; 4778 4779 NSString * location = nil; 4780 if (tr_torrentGetDownloadDir(torrentStruct) != NULL) 4781 location = [NSString stringWithUTF8String: tr_torrentGetDownloadDir(torrentStruct)]; 4782 4783 Torrent * torrent = [[Torrent alloc] initWithTorrentStruct: torrentStruct location: location lib: fLib]; 4784 4785 //change the location if the group calls for it (this has to wait until after the torrent is created) 4786 if ([[GroupsController groups] usesCustomDownloadLocationForIndex: [torrent groupValue]]) 4787 { 4788 location = [[GroupsController groups] customDownloadLocationForIndex: [torrent groupValue]]; 4789 [torrent changeDownloadFolderBeforeUsing: location]; 4790 } 4791 4792 [torrent update]; 4793 [fTorrents addObject: torrent]; 4794 [torrent release]; 4795 4796 if (!fAddingTransfers) 4797 fAddingTransfers = [[NSMutableSet alloc] init]; 4798 [fAddingTransfers addObject: torrent]; 4799 4800 [self fullUpdateUI]; 4801} 4802 4803- (void) rpcRemoveTorrent: (Torrent *) torrent 4804{ 4805 [self confirmRemoveTorrents: [[NSArray arrayWithObject: torrent] retain] deleteData: NO]; 4806 [torrent release]; 4807} 4808 4809- (void) rpcRemoveTorrentDeleteData: (Torrent *) torrent 4810{ 4811 [self confirmRemoveTorrents: [[NSArray arrayWithObject: torrent] retain] deleteData: YES]; 4812 [torrent release]; 4813} 4814 4815- (void) rpcStartedStoppedTorrent: (Torrent *) torrent 4816{ 4817 [torrent update]; 4818 [torrent release]; 4819 4820 [self updateUI]; 4821 [self applyFilter]; 4822 [self updateTorrentHistory]; 4823} 4824 4825- (void) rpcChangedTorrent: (Torrent *) torrent 4826{ 4827 [torrent update]; 4828 4829 if ([[fTableView selectedTorrents] containsObject: torrent]) 4830 { 4831 [fInfoController updateInfoStats]; //this will reload the file table 4832 [fInfoController updateOptions]; 4833 } 4834 4835 [torrent release]; 4836} 4837 4838- (void) rpcMovedTorrent: (Torrent *) torrent 4839{ 4840 [torrent update]; 4841 [torrent updateTimeMachineExclude]; 4842 4843 if ([[fTableView selectedTorrents] containsObject: torrent]) 4844 [fInfoController updateInfoStats]; 4845 4846 [torrent release]; 4847} 4848 4849- (void) rpcUpdateQueue 4850{ 4851 for (Torrent * torrent in fTorrents) 4852 [torrent update]; 4853 4854 NSArray * selectedValues = [fTableView selectedValues]; 4855 4856 NSSortDescriptor * descriptor = [NSSortDescriptor sortDescriptorWithKey: @"queuePosition" ascending: YES]; 4857 NSArray * descriptors = [NSArray arrayWithObject: descriptor]; 4858 4859 [fTorrents sortUsingDescriptors: descriptors]; 4860 4861 [self fullUpdateUI]; 4862 4863 [fTableView selectValues: selectedValues]; 4864} 4865 4866@end 4867