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