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