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 "TorrentTableView.h" 24#import "Controller.h" 25#import "FileListNode.h" 26#import "InfoOptionsViewController.h" 27#import "NSApplicationAdditions.h" 28#import "NSStringAdditions.h" 29#import "Torrent.h" 30#import "TorrentCell.h" 31#import "TorrentGroup.h" 32 33#define MAX_GROUP 999999 34 35//eliminate when Lion-only 36#define ACTION_MENU_GLOBAL_TAG 101 37#define ACTION_MENU_UNLIMITED_TAG 102 38#define ACTION_MENU_LIMIT_TAG 103 39 40#define ACTION_MENU_PRIORITY_HIGH_TAG 101 41#define ACTION_MENU_PRIORITY_NORMAL_TAG 102 42#define ACTION_MENU_PRIORITY_LOW_TAG 103 43 44#define TOGGLE_PROGRESS_SECONDS 0.175 45 46@interface TorrentTableView (Private) 47 48- (BOOL) pointInGroupStatusRect: (NSPoint) point; 49 50- (void) setGroupStatusColumns; 51 52@end 53 54@implementation TorrentTableView 55 56- (id) initWithCoder: (NSCoder *) decoder 57{ 58 if ((self = [super initWithCoder: decoder])) 59 { 60 fDefaults = [NSUserDefaults standardUserDefaults]; 61 62 fTorrentCell = [[TorrentCell alloc] init]; 63 64 NSData * groupData = [fDefaults dataForKey: @"CollapsedGroups"]; 65 if (groupData) 66 fCollapsedGroups = [[NSUnarchiver unarchiveObjectWithData: groupData] mutableCopy]; 67 else 68 fCollapsedGroups = [[NSMutableIndexSet alloc] init]; 69 70 fMouseRow = -1; 71 fMouseControlRow = -1; 72 fMouseRevealRow = -1; 73 fMouseActionRow = -1; 74 75 fActionPopoverShown = NO; 76 77 [self setDelegate: self]; 78 79 fPiecesBarPercent = [fDefaults boolForKey: @"PiecesBar"] ? 1.0 : 0.0; 80 } 81 82 return self; 83} 84 85- (void) dealloc 86{ 87 [[NSNotificationCenter defaultCenter] removeObserver: self]; 88} 89 90- (void) awakeFromNib 91{ 92 //set group columns to show ratio, needs to be in awakeFromNib to size columns correctly 93 [self setGroupStatusColumns]; 94 95 [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(setNeedsDisplay) name: @"RefreshTorrentTable" object: nil]; 96} 97 98- (BOOL) isGroupCollapsed: (NSInteger) value 99{ 100 if (value == -1) 101 value = MAX_GROUP; 102 103 return [fCollapsedGroups containsIndex: value]; 104} 105 106- (void) removeCollapsedGroup: (NSInteger) value 107{ 108 if (value == -1) 109 value = MAX_GROUP; 110 111 [fCollapsedGroups removeIndex: value]; 112} 113 114- (void) removeAllCollapsedGroups 115{ 116 [fCollapsedGroups removeAllIndexes]; 117} 118 119- (void) saveCollapsedGroups 120{ 121 [fDefaults setObject: [NSArchiver archivedDataWithRootObject: fCollapsedGroups] forKey: @"CollapsedGroups"]; 122} 123 124- (BOOL) outlineView: (NSOutlineView *) outlineView isGroupItem: (id) item 125{ 126 return ![item isKindOfClass: [Torrent class]]; 127} 128 129- (CGFloat) outlineView: (NSOutlineView *) outlineView heightOfRowByItem: (id) item 130{ 131 return [item isKindOfClass: [Torrent class]] ? [self rowHeight] : GROUP_SEPARATOR_HEIGHT; 132} 133 134- (NSCell *) outlineView: (NSOutlineView *) outlineView dataCellForTableColumn: (NSTableColumn *) tableColumn item: (id) item 135{ 136 const BOOL group = ![item isKindOfClass: [Torrent class]]; 137 if (!tableColumn) 138 return !group ? fTorrentCell : nil; 139 else 140 return group ? [tableColumn dataCellForRow: [self rowForItem: item]] : nil; 141} 142 143- (void) outlineView: (NSOutlineView *) outlineView willDisplayCell: (id) cell forTableColumn: (NSTableColumn *) tableColumn 144 item: (id) item 145{ 146 if ([item isKindOfClass: [Torrent class]]) 147 { 148 if (!tableColumn) 149 { 150 [cell setRepresentedObject: item]; 151 152 const NSInteger row = [self rowForItem: item]; 153 [cell setHover: row == fMouseRow]; 154 [cell setControlHover: row == fMouseControlRow]; 155 [cell setRevealHover: row == fMouseRevealRow]; 156 [cell setActionHover: row == fMouseActionRow]; 157 } 158 } 159} 160 161- (NSRect) frameOfCellAtColumn: (NSInteger) column row: (NSInteger) row 162{ 163 if (column == -1) 164 return [self rectOfRow: row]; 165 else 166 { 167 NSRect rect = [super frameOfCellAtColumn: column row: row]; 168 169 //adjust placement for proper vertical alignment 170 if (column == [self columnWithIdentifier: @"Group"]) 171 rect.size.height -= 1.0f; 172 173 return rect; 174 } 175} 176 177- (NSString *) outlineView: (NSOutlineView *) outlineView typeSelectStringForTableColumn: (NSTableColumn *) tableColumn item: (id) item 178{ 179 if ([item isKindOfClass: [Torrent class]]) 180 return [(Torrent *)item name]; 181 else 182 return [[self dataSource] outlineView:outlineView objectValueForTableColumn:[self tableColumnWithIdentifier:@"Group"] byItem:item]; 183} 184 185- (NSString *) outlineView: (NSOutlineView *) outlineView toolTipForCell: (NSCell *) cell rect: (NSRectPointer) rect tableColumn: (NSTableColumn *) column item: (id) item mouseLocation: (NSPoint) mouseLocation 186{ 187 NSString * ident = [column identifier]; 188 if ([ident isEqualToString: @"DL"] || [ident isEqualToString: @"DL Image"]) 189 return NSLocalizedString(@"Download speed", "Torrent table -> group row -> tooltip"); 190 else if ([ident isEqualToString: @"UL"] || [ident isEqualToString: @"UL Image"]) 191 return [fDefaults boolForKey: @"DisplayGroupRowRatio"] ? NSLocalizedString(@"Ratio", "Torrent table -> group row -> tooltip") 192 : NSLocalizedString(@"Upload speed", "Torrent table -> group row -> tooltip"); 193 else if (ident) 194 { 195 NSUInteger count = [[item torrents] count]; 196 if (count == 1) 197 return NSLocalizedString(@"1 transfer", "Torrent table -> group row -> tooltip"); 198 else 199 return [NSString stringWithFormat: NSLocalizedString(@"%@ transfers", "Torrent table -> group row -> tooltip"), 200 [NSString formattedUInteger: count]]; 201 } 202 else 203 return nil; 204} 205 206- (void) updateTrackingAreas 207{ 208 [super updateTrackingAreas]; 209 [self removeTrackingAreas]; 210 211 const NSRange rows = [self rowsInRect: [self visibleRect]]; 212 if (rows.length == 0) 213 return; 214 215 NSPoint mouseLocation = [self convertPoint: [[self window] mouseLocationOutsideOfEventStream] fromView: nil]; 216 for (NSUInteger row = rows.location; row < NSMaxRange(rows); row++) 217 { 218 if (![[self itemAtRow: row] isKindOfClass: [Torrent class]]) 219 continue; 220 221 NSDictionary * userInfo = @{@"Row": @(row)}; 222 TorrentCell * cell = (TorrentCell *)[self preparedCellAtColumn: -1 row: row]; 223 [cell addTrackingAreasForView: self inRect: [self rectOfRow: row] withUserInfo: userInfo mouseLocation: mouseLocation]; 224 } 225} 226 227- (void) removeTrackingAreas 228{ 229 fMouseRow = -1; 230 fMouseControlRow = -1; 231 fMouseRevealRow = -1; 232 fMouseActionRow = -1; 233 234 for (NSTrackingArea * area in [self trackingAreas]) 235 { 236 if ([area owner] == self && [area userInfo][@"Row"]) 237 [self removeTrackingArea: area]; 238 } 239} 240 241- (void) setRowHover: (NSInteger) row 242{ 243 NSAssert([fDefaults boolForKey: @"SmallView"], @"cannot set a hover row when not in compact view"); 244 245 fMouseRow = row; 246 if (row >= 0) 247 [self setNeedsDisplayInRect: [self rectOfRow: row]]; 248} 249 250- (void) setControlButtonHover: (NSInteger) row 251{ 252 fMouseControlRow = row; 253 if (row >= 0) 254 [self setNeedsDisplayInRect: [self rectOfRow: row]]; 255} 256 257- (void) setRevealButtonHover: (NSInteger) row 258{ 259 fMouseRevealRow = row; 260 if (row >= 0) 261 [self setNeedsDisplayInRect: [self rectOfRow: row]]; 262} 263 264- (void) setActionButtonHover: (NSInteger) row 265{ 266 fMouseActionRow = row; 267 if (row >= 0) 268 [self setNeedsDisplayInRect: [self rectOfRow: row]]; 269} 270 271- (void) mouseEntered: (NSEvent *) event 272{ 273 NSDictionary * dict = (NSDictionary *)[event userData]; 274 275 NSNumber * row; 276 if ((row = dict[@"Row"])) 277 { 278 NSInteger rowVal = [row integerValue]; 279 NSString * type = dict[@"Type"]; 280 if ([type isEqualToString: @"Action"]) 281 fMouseActionRow = rowVal; 282 else if ([type isEqualToString: @"Control"]) 283 fMouseControlRow = rowVal; 284 else if ([type isEqualToString: @"Reveal"]) 285 fMouseRevealRow = rowVal; 286 else 287 { 288 fMouseRow = rowVal; 289 if (![fDefaults boolForKey: @"SmallView"]) 290 return; 291 } 292 293 [self setNeedsDisplayInRect: [self rectOfRow: rowVal]]; 294 } 295} 296 297- (void) mouseExited: (NSEvent *) event 298{ 299 NSDictionary * dict = (NSDictionary *)[event userData]; 300 301 NSNumber * row; 302 if ((row = dict[@"Row"])) 303 { 304 NSString * type = dict[@"Type"]; 305 if ([type isEqualToString: @"Action"]) 306 fMouseActionRow = -1; 307 else if ([type isEqualToString: @"Control"]) 308 fMouseControlRow = -1; 309 else if ([type isEqualToString: @"Reveal"]) 310 fMouseRevealRow = -1; 311 else 312 { 313 fMouseRow = -1; 314 if (![fDefaults boolForKey: @"SmallView"]) 315 return; 316 } 317 318 [self setNeedsDisplayInRect: [self rectOfRow: [row integerValue]]]; 319 } 320} 321 322- (void) outlineViewSelectionIsChanging: (NSNotification *) notification 323{ 324 #warning elliminate when view-based? 325 //if pushing a button, don't change the selected rows 326 if (fSelectedValues) 327 [self selectValues: fSelectedValues]; 328} 329 330- (void) outlineViewItemDidExpand: (NSNotification *) notification 331{ 332 NSInteger value = [[notification userInfo][@"NSObject"] groupIndex]; 333 if (value < 0) 334 value = MAX_GROUP; 335 336 if ([fCollapsedGroups containsIndex: value]) 337 { 338 [fCollapsedGroups removeIndex: value]; 339 [[NSNotificationCenter defaultCenter] postNotificationName: @"OutlineExpandCollapse" object: self]; 340 } 341} 342 343- (void) outlineViewItemDidCollapse: (NSNotification *) notification 344{ 345 NSInteger value = [[notification userInfo][@"NSObject"] groupIndex]; 346 if (value < 0) 347 value = MAX_GROUP; 348 349 [fCollapsedGroups addIndex: value]; 350 [[NSNotificationCenter defaultCenter] postNotificationName: @"OutlineExpandCollapse" object: self]; 351} 352 353- (void) mouseDown: (NSEvent *) event 354{ 355 NSPoint point = [self convertPoint: [event locationInWindow] fromView: nil]; 356 const NSInteger row = [self rowAtPoint: point]; 357 358 //check to toggle group status before anything else 359 if ([self pointInGroupStatusRect: point]) 360 { 361 [fDefaults setBool: ![fDefaults boolForKey: @"DisplayGroupRowRatio"] forKey: @"DisplayGroupRowRatio"]; 362 [self setGroupStatusColumns]; 363 364 return; 365 } 366 367 const BOOL pushed = row != -1 && (fMouseActionRow == row || fMouseRevealRow == row || fMouseControlRow == row); 368 369 //if pushing a button, don't change the selected rows 370 if (pushed) 371 fSelectedValues = [self selectedValues]; 372 373 [super mouseDown: event]; 374 375 fSelectedValues = nil; 376 377 //avoid weird behavior when showing menu by doing this after mouse down 378 if (row != -1 && fMouseActionRow == row) 379 { 380 #warning maybe make appear on mouse down 381 [self displayTorrentActionPopoverForEvent: event]; 382 } 383 else if (!pushed && [event clickCount] == 2) //double click 384 { 385 id item = nil; 386 if (row != -1) 387 item = [self itemAtRow: row]; 388 389 if (!item || [item isKindOfClass: [Torrent class]]) 390 [fController showInfo: nil]; 391 else 392 { 393 if ([self isItemExpanded: item]) 394 [self collapseItem: item]; 395 else 396 [self expandItem: item]; 397 } 398 } 399 else; 400} 401 402- (void) selectValues: (NSArray *) values 403{ 404 NSMutableIndexSet * indexSet = [NSMutableIndexSet indexSet]; 405 406 for (id item in values) 407 { 408 if ([item isKindOfClass: [Torrent class]]) 409 { 410 const NSInteger index = [self rowForItem: item]; 411 if (index != -1) 412 [indexSet addIndex: index]; 413 } 414 else 415 { 416 const NSInteger group = [item groupIndex]; 417 for (NSInteger i = 0; i < [self numberOfRows]; i++) 418 { 419 id tableItem = [self itemAtRow: i]; 420 if ([tableItem isKindOfClass: [TorrentGroup class]] && group == [tableItem groupIndex]) 421 { 422 [indexSet addIndex: i]; 423 break; 424 } 425 } 426 } 427 } 428 429 [self selectRowIndexes: indexSet byExtendingSelection: NO]; 430} 431 432- (NSArray *) selectedValues 433{ 434 NSIndexSet * selectedIndexes = [self selectedRowIndexes]; 435 NSMutableArray * values = [NSMutableArray arrayWithCapacity: [selectedIndexes count]]; 436 437 for (NSUInteger i = [selectedIndexes firstIndex]; i != NSNotFound; i = [selectedIndexes indexGreaterThanIndex: i]) 438 [values addObject: [self itemAtRow: i]]; 439 440 return values; 441} 442 443- (NSArray *) selectedTorrents 444{ 445 NSIndexSet * selectedIndexes = [self selectedRowIndexes]; 446 NSMutableArray * torrents = [NSMutableArray arrayWithCapacity: [selectedIndexes count]]; //take a shot at guessing capacity 447 448 for (NSUInteger i = [selectedIndexes firstIndex]; i != NSNotFound; i = [selectedIndexes indexGreaterThanIndex: i]) 449 { 450 id item = [self itemAtRow: i]; 451 if ([item isKindOfClass: [Torrent class]]) 452 [torrents addObject: item]; 453 else 454 { 455 NSArray * groupTorrents = [item torrents]; 456 [torrents addObjectsFromArray: groupTorrents]; 457 if ([self isItemExpanded: item]) 458 i +=[groupTorrents count]; 459 } 460 } 461 462 return torrents; 463} 464 465- (NSMenu *) menuForEvent: (NSEvent *) event 466{ 467 NSInteger row = [self rowAtPoint: [self convertPoint: [event locationInWindow] fromView: nil]]; 468 if (row >= 0) 469 { 470 if (![self isRowSelected: row]) 471 [self selectRowIndexes: [NSIndexSet indexSetWithIndex: row] byExtendingSelection: NO]; 472 return fContextRow; 473 } 474 else 475 { 476 [self deselectAll: self]; 477 return fContextNoRow; 478 } 479} 480 481//make sure that the pause buttons become orange when holding down the option key 482- (void) flagsChanged: (NSEvent *) event 483{ 484 [self display]; 485 [super flagsChanged: event]; 486} 487 488//option-command-f will focus the filter bar's search field 489- (void) keyDown: (NSEvent *) event 490{ 491 const unichar firstChar = [[event charactersIgnoringModifiers] characterAtIndex: 0]; 492 493 if (firstChar == 'f' && [event modifierFlags] & NSAlternateKeyMask && [event modifierFlags] & NSCommandKeyMask) 494 [fController focusFilterField]; 495 else if (firstChar == ' ') 496 [fController toggleQuickLook: nil]; 497 else if ([event keyCode] == 53) //esc key 498 [self deselectAll: nil]; 499 else 500 [super keyDown: event]; 501} 502 503- (NSRect) iconRectForRow: (NSInteger) row 504{ 505 return [fTorrentCell iconRectForBounds: [self rectOfRow: row]]; 506} 507 508- (void) paste: (id) sender 509{ 510 NSURL * url; 511 if ((url = [NSURL URLFromPasteboard: [NSPasteboard generalPasteboard]])) 512 [fController openURL: [url absoluteString]]; 513 else 514 { 515 NSArray * items = [[NSPasteboard generalPasteboard] readObjectsForClasses: @[[NSString class]] options: nil]; 516 if (items) 517 { 518 NSDataDetector * detector = [NSDataDetector dataDetectorWithTypes: NSTextCheckingTypeLink error: nil]; 519 for (__strong NSString * pbItem in items) 520 { 521 pbItem = [pbItem stringByTrimmingCharactersInSet: [NSCharacterSet whitespaceAndNewlineCharacterSet]]; 522 if ([pbItem rangeOfString: @"magnet:" options: (NSAnchoredSearch | NSCaseInsensitiveSearch)].location != NSNotFound) 523 [fController openURL: pbItem]; 524 else 525 { 526 #warning only accept full text? 527 for (NSTextCheckingResult * result in [detector matchesInString: pbItem options: 0 range: NSMakeRange(0, [pbItem length])]) 528 [fController openURL: [[result URL] absoluteString]]; 529 } 530 } 531 } 532 } 533} 534 535- (BOOL) validateMenuItem: (NSMenuItem *) menuItem 536{ 537 SEL action = [menuItem action]; 538 539 if (action == @selector(paste:)) 540 { 541 if ([[[NSPasteboard generalPasteboard] types] containsObject: NSURLPboardType]) 542 return YES; 543 544 NSArray * items = [[NSPasteboard generalPasteboard] readObjectsForClasses: @[[NSString class]] options: nil]; 545 if (items) 546 { 547 NSDataDetector * detector = [NSDataDetector dataDetectorWithTypes: NSTextCheckingTypeLink error: nil]; 548 for (__strong NSString * pbItem in items) 549 { 550 pbItem = [pbItem stringByTrimmingCharactersInSet: [NSCharacterSet whitespaceAndNewlineCharacterSet]]; 551 if (([pbItem rangeOfString: @"magnet:" options: (NSAnchoredSearch | NSCaseInsensitiveSearch)].location != NSNotFound) 552 || [detector firstMatchInString: pbItem options: 0 range: NSMakeRange(0, [pbItem length])]) 553 return YES; 554 } 555 } 556 557 return NO; 558 } 559 560 return YES; 561} 562 563- (void) toggleControlForTorrent: (Torrent *) torrent 564{ 565 if ([torrent isActive]) 566 [fController stopTorrents: @[torrent]]; 567 else 568 { 569 if ([NSEvent modifierFlags] & NSAlternateKeyMask) 570 [fController resumeTorrentsNoWait: @[torrent]]; 571 else if ([torrent waitingToStart]) 572 [fController stopTorrents: @[torrent]]; 573 else 574 [fController resumeTorrents: @[torrent]]; 575 } 576} 577 578- (void) displayTorrentActionPopoverForEvent: (NSEvent *) event 579{ 580 const NSInteger row = [self rowAtPoint: [self convertPoint: [event locationInWindow] fromView: nil]]; 581 if (row < 0) 582 return; 583 584 const NSRect rect = [fTorrentCell iconRectForBounds: [self rectOfRow: row]]; 585 586 if (fActionPopoverShown) 587 return; 588 589 Torrent * torrent = [self itemAtRow: row]; 590 591 NSPopover * popover = [[NSPopover alloc] init]; 592 [popover setBehavior: NSPopoverBehaviorTransient]; 593 InfoOptionsViewController * infoViewController = [[InfoOptionsViewController alloc] init]; 594 [popover setContentViewController: infoViewController]; 595 [popover setDelegate: self]; 596 597 [popover showRelativeToRect: rect ofView: self preferredEdge: NSMaxYEdge]; 598 [infoViewController setInfoForTorrents: @[torrent]]; 599 [infoViewController updateInfo]; 600 601} 602 603//don't show multiple popovers when clicking the gear button repeatedly 604- (void) popoverWillShow: (NSNotification *) notification 605{ 606 fActionPopoverShown = YES; 607} 608 609- (void) popoverWillClose: (NSNotification *) notification 610{ 611 fActionPopoverShown = NO; 612} 613 614//eliminate when Lion-only, along with all the menu item instance variables 615- (void) menuNeedsUpdate: (NSMenu *) menu 616{ 617 //this method seems to be called when it shouldn't be 618 if (!fMenuTorrent || ![menu supermenu]) 619 return; 620 621 if (menu == fUploadMenu || menu == fDownloadMenu) 622 { 623 NSMenuItem * item; 624 if ([menu numberOfItems] == 3) 625 { 626 const NSInteger speedLimitActionValue[] = { 0, 5, 10, 20, 30, 40, 50, 75, 100, 150, 200, 250, 500, 627 750, 1000, 1500, 2000, -1 }; 628 629 for (NSInteger i = 0; speedLimitActionValue[i] != -1; i++) 630 { 631 item = [[NSMenuItem alloc] initWithTitle: [NSString stringWithFormat: NSLocalizedString(@"%d KB/s", 632 "Action menu -> upload/download limit"), speedLimitActionValue[i]] action: @selector(setQuickLimit:) 633 keyEquivalent: @""]; 634 [item setTarget: self]; 635 [item setRepresentedObject: @(speedLimitActionValue[i])]; 636 [menu addItem: item]; 637 } 638 } 639 640 const BOOL upload = menu == fUploadMenu; 641 const BOOL limit = [fMenuTorrent usesSpeedLimit: upload]; 642 643 item = [menu itemWithTag: ACTION_MENU_LIMIT_TAG]; 644 [item setState: limit ? NSOnState : NSOffState]; 645 [item setTitle: [NSString stringWithFormat: NSLocalizedString(@"Limit (%d KB/s)", 646 "torrent action menu -> upload/download limit"), [fMenuTorrent speedLimit: upload]]]; 647 648 item = [menu itemWithTag: ACTION_MENU_UNLIMITED_TAG]; 649 [item setState: !limit ? NSOnState : NSOffState]; 650 } 651 else if (menu == fRatioMenu) 652 { 653 NSMenuItem * item; 654 if ([menu numberOfItems] == 4) 655 { 656 const float ratioLimitActionValue[] = { 0.25, 0.5, 0.75, 1.0, 1.5, 2.0, 3.0, -1.0 }; 657 658 for (NSInteger i = 0; ratioLimitActionValue[i] != -1.0; i++) 659 { 660 item = [[NSMenuItem alloc] initWithTitle: [NSString localizedStringWithFormat: @"%.2f", ratioLimitActionValue[i]] 661 action: @selector(setQuickRatio:) keyEquivalent: @""]; 662 [item setTarget: self]; 663 [item setRepresentedObject: @(ratioLimitActionValue[i])]; 664 [menu addItem: item]; 665 } 666 } 667 668 const tr_ratiolimit mode = [fMenuTorrent ratioSetting]; 669 670 item = [menu itemWithTag: ACTION_MENU_LIMIT_TAG]; 671 [item setState: mode == TR_RATIOLIMIT_SINGLE ? NSOnState : NSOffState]; 672 [item setTitle: [NSString localizedStringWithFormat: NSLocalizedString(@"Stop at Ratio (%.2f)", 673 "torrent action menu -> ratio stop"), [fMenuTorrent ratioLimit]]]; 674 675 item = [menu itemWithTag: ACTION_MENU_UNLIMITED_TAG]; 676 [item setState: mode == TR_RATIOLIMIT_UNLIMITED ? NSOnState : NSOffState]; 677 678 item = [menu itemWithTag: ACTION_MENU_GLOBAL_TAG]; 679 [item setState: mode == TR_RATIOLIMIT_GLOBAL ? NSOnState : NSOffState]; 680 } 681 else if (menu == fPriorityMenu) 682 { 683 const tr_priority_t priority = [fMenuTorrent priority]; 684 685 NSMenuItem * item = [menu itemWithTag: ACTION_MENU_PRIORITY_HIGH_TAG]; 686 [item setState: priority == TR_PRI_HIGH ? NSOnState : NSOffState]; 687 688 item = [menu itemWithTag: ACTION_MENU_PRIORITY_NORMAL_TAG]; 689 [item setState: priority == TR_PRI_NORMAL ? NSOnState : NSOffState]; 690 691 item = [menu itemWithTag: ACTION_MENU_PRIORITY_LOW_TAG]; 692 [item setState: priority == TR_PRI_LOW ? NSOnState : NSOffState]; 693 } 694} 695 696//the following methods might not be needed when Lion-only 697- (void) setQuickLimitMode: (id) sender 698{ 699 const BOOL limit = [sender tag] == ACTION_MENU_LIMIT_TAG; 700 [fMenuTorrent setUseSpeedLimit: limit upload: [sender menu] == fUploadMenu]; 701 702 [[NSNotificationCenter defaultCenter] postNotificationName: @"UpdateOptions" object: nil]; 703} 704 705- (void) setQuickLimit: (id) sender 706{ 707 const BOOL upload = [sender menu] == fUploadMenu; 708 [fMenuTorrent setUseSpeedLimit: YES upload: upload]; 709 [fMenuTorrent setSpeedLimit: [[sender representedObject] intValue] upload: upload]; 710 711 [[NSNotificationCenter defaultCenter] postNotificationName: @"UpdateOptions" object: nil]; 712} 713 714- (void) setGlobalLimit: (id) sender 715{ 716 [fMenuTorrent setUseGlobalSpeedLimit: [(NSButton *)sender state] != NSOnState]; 717 718 [[NSNotificationCenter defaultCenter] postNotificationName: @"UpdateOptions" object: nil]; 719} 720 721- (void) setQuickRatioMode: (id) sender 722{ 723 tr_ratiolimit mode; 724 switch ([sender tag]) 725 { 726 case ACTION_MENU_UNLIMITED_TAG: 727 mode = TR_RATIOLIMIT_UNLIMITED; 728 break; 729 case ACTION_MENU_LIMIT_TAG: 730 mode = TR_RATIOLIMIT_SINGLE; 731 break; 732 case ACTION_MENU_GLOBAL_TAG: 733 mode = TR_RATIOLIMIT_GLOBAL; 734 break; 735 default: 736 return; 737 } 738 739 [fMenuTorrent setRatioSetting: mode]; 740 741 [[NSNotificationCenter defaultCenter] postNotificationName: @"UpdateOptions" object: nil]; 742} 743 744- (void) setQuickRatio: (id) sender 745{ 746 [fMenuTorrent setRatioSetting: TR_RATIOLIMIT_SINGLE]; 747 [fMenuTorrent setRatioLimit: [[sender representedObject] floatValue]]; 748 749 [[NSNotificationCenter defaultCenter] postNotificationName: @"UpdateOptions" object: nil]; 750} 751 752- (void) setPriority: (id) sender 753{ 754 tr_priority_t priority; 755 switch ([sender tag]) 756 { 757 case ACTION_MENU_PRIORITY_HIGH_TAG: 758 priority = TR_PRI_HIGH; 759 break; 760 case ACTION_MENU_PRIORITY_NORMAL_TAG: 761 priority = TR_PRI_NORMAL; 762 break; 763 case ACTION_MENU_PRIORITY_LOW_TAG: 764 priority = TR_PRI_LOW; 765 break; 766 default: 767 NSAssert1(NO, @"Unknown priority: %ld", [sender tag]); 768 priority = TR_PRI_NORMAL; 769 } 770 771 [fMenuTorrent setPriority: priority]; 772 773 [[NSNotificationCenter defaultCenter] postNotificationName: @"UpdateUI" object: nil]; 774} 775 776- (void) togglePiecesBar 777{ 778 NSMutableArray * progressMarks = [NSMutableArray arrayWithCapacity: 16]; 779 for (NSAnimationProgress i = 0.0625; i <= 1.0; i += 0.0625) 780 [progressMarks addObject: @(i)]; 781 782 //this stops a previous animation 783 fPiecesBarAnimation = [[NSAnimation alloc] initWithDuration: TOGGLE_PROGRESS_SECONDS animationCurve: NSAnimationEaseIn]; 784 [fPiecesBarAnimation setAnimationBlockingMode: NSAnimationNonblocking]; 785 [fPiecesBarAnimation setProgressMarks: progressMarks]; 786 [fPiecesBarAnimation setDelegate: self]; 787 788 [fPiecesBarAnimation startAnimation]; 789} 790 791- (void) animationDidEnd: (NSAnimation *) animation 792{ 793 if (animation == fPiecesBarAnimation) 794 { 795 fPiecesBarAnimation = nil; 796 } 797} 798 799- (void) animation: (NSAnimation *) animation didReachProgressMark: (NSAnimationProgress) progress 800{ 801 if (animation == fPiecesBarAnimation) 802 { 803 if ([fDefaults boolForKey: @"PiecesBar"]) 804 fPiecesBarPercent = progress; 805 else 806 fPiecesBarPercent = 1.0 - progress; 807 808 [self setNeedsDisplay: YES]; 809 } 810} 811 812- (CGFloat) piecesBarPercent 813{ 814 return fPiecesBarPercent; 815} 816 817- (void) selectAndScrollToRow: (NSInteger) row 818{ 819 NSParameterAssert(row >= 0); 820 NSParameterAssert(row < [self numberOfRows]); 821 822 [self selectRowIndexes: [NSIndexSet indexSetWithIndex: row] byExtendingSelection: NO]; 823 824 const NSRect rowRect = [self rectOfRow: row]; 825 const NSRect viewRect = [[self superview] frame]; 826 827 NSPoint scrollOrigin = rowRect.origin; 828 scrollOrigin.y += (rowRect.size.height - viewRect.size.height) / 2; 829 if (scrollOrigin.y < 0) 830 scrollOrigin.y = 0; 831 832 [[[self superview] animator] setBoundsOrigin: scrollOrigin]; 833} 834 835@end 836 837@implementation TorrentTableView (Private) 838 839- (BOOL) pointInGroupStatusRect: (NSPoint) point 840{ 841 NSInteger row = [self rowAtPoint: point]; 842 if (row < 0 || [[self itemAtRow: row] isKindOfClass: [Torrent class]]) 843 return NO; 844 845 NSString * ident = [[self tableColumns][[self columnAtPoint: point]] identifier]; 846 return [ident isEqualToString: @"UL"] || [ident isEqualToString: @"UL Image"] 847 || [ident isEqualToString: @"DL"] || [ident isEqualToString: @"DL Image"]; 848} 849 850- (void) setGroupStatusColumns 851{ 852 const BOOL ratio = [fDefaults boolForKey: @"DisplayGroupRowRatio"]; 853 854 [[self tableColumnWithIdentifier: @"DL"] setHidden: ratio]; 855 [[self tableColumnWithIdentifier: @"DL Image"] setHidden: ratio]; 856} 857 858@end 859