macos: View-based FileOutlineView (#7760)

Signed-off-by: Dzmitry Neviadomski <nevack.d@gmail.com>
This commit is contained in:
Dzmitry Neviadomski
2025-11-09 20:21:42 +03:00
committed by GitHub
parent 909fdad807
commit d1985b05c6
21 changed files with 877 additions and 634 deletions

View File

@@ -309,7 +309,7 @@ typedef NS_ENUM(NSUInteger, PopupPriority) {
{
//check buttons
//keep synced with identical code in InfoFileViewController.m
NSInteger const filesCheckState = [self.torrent
NSControlStateValue const filesCheckState = [self.torrent
checkForFiles:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, self.torrent.fileCount)]];
self.fCheckAllButton.enabled = filesCheckState != NSControlStateValueOn; //if anything is unchecked
self.fUncheckAllButton.enabled = !self.torrent.allDownloaded; //if there are any checked files that aren't finished
@@ -352,7 +352,7 @@ typedef NS_ENUM(NSUInteger, PopupPriority) {
{
[self.torrent update];
[self.fFileController refresh];
[self.fFileController reloadVisibleRows];
[self updateCheckButtons:nil]; //call in case button state changed by checking

View File

@@ -75,14 +75,16 @@ target_sources(${TR_NAME}-mac
ExpandedPathToPathTransformer.mm
FileListNode.h
FileListNode.mm
FileNameCell.h
FileNameCell.mm
FileCheckCellView.h
FileCheckCellView.mm
FileNameCellView.h
FileNameCellView.mm
FileOutlineController.h
FileOutlineController.mm
FileOutlineView.h
FileOutlineView.mm
FilePriorityCell.h
FilePriorityCell.mm
FilePriorityCellView.h
FilePriorityCellView.mm
FileRenameSheetController.h
FileRenameSheetController.mm
FilterBarController.h

View File

@@ -0,0 +1,13 @@
// This file Copyright © Transmission authors and contributors.
// It may be used under the MIT (SPDX: MIT) license.
// License text can be found in the licenses/ folder.
#import <AppKit/AppKit.h>
@class FileListNode;
@interface FileCheckCellView : NSTableCellView
@property(nonatomic, weak) FileListNode* node;
@end

113
macosx/FileCheckCellView.mm Normal file
View File

@@ -0,0 +1,113 @@
// This file Copyright © Transmission authors and contributors.
// It may be used under the MIT (SPDX: MIT) license.
// License text can be found in the licenses/ folder.
#import "FileCheckCellView.h"
#import "FileListNode.h"
#import "Torrent.h"
@interface FileCheckCellView ()
@property(nonatomic, weak) NSButton* checkButton;
@end
@implementation FileCheckCellView
- (instancetype)initWithFrame:(NSRect)frameRect
{
if ((self = [super initWithFrame:frameRect]))
{
// Create checkbox button
NSButton* checkButton = [[NSButton alloc] initWithFrame:NSZeroRect];
checkButton.translatesAutoresizingMaskIntoConstraints = NO;
[checkButton setButtonType:NSButtonTypeSwitch];
checkButton.title = @"";
checkButton.allowsMixedState = YES;
checkButton.target = self;
checkButton.action = @selector(checkButtonClicked:);
[self addSubview:checkButton];
_checkButton = checkButton;
// Setup constraints
[NSLayoutConstraint activateConstraints:@[
[checkButton.centerXAnchor constraintEqualToAnchor:self.centerXAnchor],
[checkButton.centerYAnchor constraintEqualToAnchor:self.centerYAnchor],
]];
}
return self;
}
- (void)setNode:(FileListNode*)node
{
_node = node;
[self updateDisplay];
}
- (void)updateDisplay
{
if (!self.node)
{
return;
}
FileListNode* node = self.node;
Torrent* torrent = node.torrent;
// Update checkbox state
self.checkButton.state = [torrent checkForFiles:node.indexes];
self.checkButton.enabled = [torrent canChangeDownloadCheckForFiles:node.indexes];
// Update tooltip
[self updateTooltip];
}
- (void)updateTooltip
{
if (!self.node)
{
return;
}
NSString* tooltip = nil;
switch (self.checkButton.state)
{
case NSControlStateValueOff:
tooltip = NSLocalizedString(@"Don't Download", "files tab -> tooltip");
break;
case NSControlStateValueOn:
tooltip = NSLocalizedString(@"Download", "files tab -> tooltip");
break;
case NSControlStateValueMixed:
tooltip = NSLocalizedString(@"Download Some", "files tab -> tooltip");
break;
}
self.checkButton.toolTip = tooltip;
}
- (void)checkButtonClicked:(NSButton*)sender
{
if (!self.node)
{
return;
}
FileListNode* node = self.node;
Torrent* torrent = node.torrent;
NSIndexSet* indexSet;
if (NSEvent.modifierFlags & NSEventModifierFlagOption)
{
indexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, torrent.fileCount)];
}
else
{
indexSet = node.indexes;
}
[torrent setFileCheckState:sender.state != NSControlStateValueOff ? NSControlStateValueOn : NSControlStateValueOff
forIndexes:indexSet];
// Notify that we need to refresh
[NSNotificationCenter.defaultCenter postNotificationName:@"UpdateUI" object:nil];
}
@end

View File

@@ -1,17 +0,0 @@
// This file Copyright © Transmission authors and contributors.
// It may be used under the MIT (SPDX: MIT) license.
// License text can be found in the licenses/ folder.
#import <AppKit/AppKit.h>
typedef NS_ENUM(NSInteger, AttributesStyle) {
AttributesStyleNormal,
AttributesStyleEmphasized,
AttributesStyleDisabled,
};
@interface FileNameCell : NSActionCell
- (NSRect)imageRectForBounds:(NSRect)bounds;
@end

View File

@@ -1,218 +0,0 @@
// This file Copyright © Transmission authors and contributors.
// It may be used under the MIT (SPDX: MIT) license.
// License text can be found in the licenses/ folder.
#include <libtransmission/transmission.h>
#include <libtransmission/utils.h>
#import "FileNameCell.h"
#import "FileOutlineView.h"
#import "Torrent.h"
#import "FileListNode.h"
#import "NSStringAdditions.h"
static CGFloat const kPaddingHorizontal = 2.0;
static CGFloat const kImageFolderSize = 16.0;
static CGFloat const kImageIconSize = 32.0;
static CGFloat const kPaddingBetweenImageAndTitle = 4.0;
static CGFloat const kPaddingAboveTitleFile = 2.0;
static CGFloat const kPaddingBelowStatusFile = 2.0;
static CGFloat const kPaddingBetweenNameAndFolderStatus = 4.0;
static CGFloat const kPaddingExpansionFrame = 2.0;
static NSMutableParagraphStyle* sParagraphStyle()
{
NSMutableParagraphStyle* paragraphStyle = [NSParagraphStyle.defaultParagraphStyle mutableCopy];
paragraphStyle.lineBreakMode = NSLineBreakByTruncatingMiddle;
return paragraphStyle;
}
static NSMutableParagraphStyle* sStatusParagraphStyle()
{
NSMutableParagraphStyle* paragraphStyle = [NSParagraphStyle.defaultParagraphStyle mutableCopy];
paragraphStyle.lineBreakMode = NSLineBreakByTruncatingTail;
return paragraphStyle;
}
static NSDictionary<NSAttributedStringKey, id>* const kTitleAttributes = @{
NSFontAttributeName : [NSFont messageFontOfSize:12.0],
NSParagraphStyleAttributeName : sParagraphStyle(),
NSForegroundColorAttributeName : NSColor.controlTextColor
};
static NSDictionary<NSAttributedStringKey, id>* const kStatusAttributes = @{
NSFontAttributeName : [NSFont messageFontOfSize:9.0],
NSParagraphStyleAttributeName : sStatusParagraphStyle(),
NSForegroundColorAttributeName : NSColor.secondaryLabelColor
};
static NSDictionary<NSAttributedStringKey, id>* const kTitleEmphasizedAttributes = @{
NSFontAttributeName : [NSFont messageFontOfSize:12.0],
NSParagraphStyleAttributeName : sParagraphStyle(),
NSForegroundColorAttributeName : NSColor.whiteColor
};
static NSDictionary<NSAttributedStringKey, id>* const kStatusEmphasizedAttributes = @{
NSFontAttributeName : [NSFont messageFontOfSize:9.0],
NSParagraphStyleAttributeName : sStatusParagraphStyle(),
NSForegroundColorAttributeName : NSColor.whiteColor
};
static NSDictionary<NSAttributedStringKey, id>* const kTitleDisabledAttributes = @{
NSFontAttributeName : [NSFont messageFontOfSize:12.0],
NSParagraphStyleAttributeName : sParagraphStyle(),
NSForegroundColorAttributeName : NSColor.disabledControlTextColor
};
static NSDictionary<NSAttributedStringKey, id>* const kStatusDisabledAttributes = @{
NSFontAttributeName : [NSFont messageFontOfSize:9.0],
NSParagraphStyleAttributeName : sStatusParagraphStyle(),
NSForegroundColorAttributeName : NSColor.disabledControlTextColor
};
@implementation FileNameCell
- (NSImage*)image
{
FileListNode* node = (FileListNode*)self.objectValue;
return node.icon;
}
- (NSRect)imageRectForBounds:(NSRect)bounds
{
NSRect result = bounds;
result.origin.x += kPaddingHorizontal;
CGFloat const IMAGE_SIZE = ((FileListNode*)self.objectValue).isFolder ? kImageFolderSize : kImageIconSize;
result.origin.y += (result.size.height - IMAGE_SIZE) * 0.5;
result.size = NSMakeSize(IMAGE_SIZE, IMAGE_SIZE);
return result;
}
- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView*)controlView
{
//icon
[self.image drawInRect:[self imageRectForBounds:cellFrame] fromRect:NSZeroRect operation:NSCompositingOperationSourceOver
fraction:1.0
respectFlipped:YES
hints:nil];
FileListNode* node = self.objectValue;
AttributesStyle style;
if (self.backgroundStyle == NSBackgroundStyleEmphasized)
{
style = AttributesStyleEmphasized;
}
else if ([node.torrent checkForFiles:node.indexes] == NSControlStateValueOff)
{
style = AttributesStyleDisabled;
}
else
{
style = AttributesStyleNormal;
}
//title
NSAttributedString* titleString = [self attributedTitleWithStyle:style];
NSRect titleRect = [self rectForTitleWithStringSize:[titleString size] inBounds:cellFrame];
[titleString drawInRect:titleRect];
//status
NSAttributedString* statusString = [self attributedStatusWithStyle:style];
NSRect statusRect = [self rectForStatusWithString:statusString withTitleRect:titleRect inBounds:cellFrame];
[statusString drawInRect:statusRect];
}
- (NSRect)expansionFrameWithFrame:(NSRect)cellFrame inView:(NSView*)view
{
NSAttributedString* titleString = [self attributedTitleWithStyle:AttributesStyleNormal];
NSRect realRect = [self rectForTitleWithStringSize:[titleString size] inBounds:cellFrame];
if ([titleString size].width > NSWidth(realRect) &&
NSMouseInRect([view convertPoint:view.window.mouseLocationOutsideOfEventStream fromView:nil], realRect, view.flipped))
{
realRect.size.width = [titleString size].width;
return NSInsetRect(realRect, -kPaddingExpansionFrame, -kPaddingExpansionFrame);
}
return NSZeroRect;
}
- (void)drawWithExpansionFrame:(NSRect)cellFrame inView:(NSView*)view
{
cellFrame.origin.x += kPaddingExpansionFrame;
cellFrame.origin.y += kPaddingExpansionFrame;
NSAttributedString* titleString = [self attributedTitleWithStyle:AttributesStyleNormal];
[titleString drawInRect:cellFrame];
}
#pragma mark - Private
- (NSRect)rectForTitleWithStringSize:(NSSize)stringSize inBounds:(NSRect)bounds
{
NSSize const titleSize = stringSize;
//no right padding, so that there's not too much space between this and the priority image
NSRect result;
if (!((FileListNode*)self.objectValue).isFolder)
{
result.origin.x = NSMinX(bounds) + kPaddingHorizontal + kImageIconSize + kPaddingBetweenImageAndTitle;
result.origin.y = NSMinY(bounds) + kPaddingAboveTitleFile;
result.size.width = NSMaxX(bounds) - NSMinX(result);
}
else
{
result.origin.x = NSMinX(bounds) + kPaddingHorizontal + kImageFolderSize + kPaddingBetweenImageAndTitle;
result.origin.y = NSMidY(bounds) - titleSize.height * 0.5;
result.size.width = MIN(titleSize.width, NSMaxX(bounds) - NSMinX(result));
}
result.size.height = titleSize.height;
return result;
}
- (NSRect)rectForStatusWithString:(NSAttributedString*)string withTitleRect:(NSRect)titleRect inBounds:(NSRect)bounds
{
NSSize const statusSize = [string size];
NSRect result;
if (!((FileListNode*)self.objectValue).isFolder)
{
result.origin.x = NSMinX(titleRect);
result.origin.y = NSMaxY(bounds) - kPaddingBelowStatusFile - statusSize.height;
result.size.width = NSWidth(titleRect);
}
else
{
result.origin.x = NSMaxX(titleRect) + kPaddingBetweenNameAndFolderStatus;
result.origin.y = NSMaxY(titleRect) - statusSize.height - 1.0;
result.size.width = NSMaxX(bounds) - NSMaxX(titleRect);
}
result.size.height = statusSize.height;
return result;
}
- (NSAttributedString*)attributedTitleWithStyle:(AttributesStyle)style
{
NSString* title = ((FileListNode*)self.objectValue).name;
return [[NSAttributedString alloc] initWithString:title attributes:style == AttributesStyleEmphasized ? kTitleEmphasizedAttributes :
style == AttributesStyleDisabled ? kTitleDisabledAttributes :
kTitleAttributes];
}
- (NSAttributedString*)attributedStatusWithStyle:(AttributesStyle)style
{
FileListNode* node = (FileListNode*)self.objectValue;
Torrent* torrent = node.torrent;
CGFloat const progress = [torrent fileProgress:node];
NSString* percentString = [NSString percentString:progress longDecimals:YES];
NSString* status = [NSString stringWithFormat:NSLocalizedString(@"%@ of %@", "Inspector -> Files tab -> file status string"),
percentString,
[NSString stringForFileSize:node.size]];
return [[NSAttributedString alloc] initWithString:status attributes:style == AttributesStyleEmphasized ? kStatusEmphasizedAttributes :
style == AttributesStyleDisabled ? kStatusDisabledAttributes :
kStatusAttributes];
}
@end

13
macosx/FileNameCellView.h Normal file
View File

@@ -0,0 +1,13 @@
// This file Copyright © Transmission authors and contributors.
// It may be used under the MIT (SPDX: MIT) license.
// License text can be found in the licenses/ folder.
#import <AppKit/AppKit.h>
@class FileListNode;
@interface FileNameCellView : NSTableCellView
@property(nonatomic, weak) FileListNode* node;
@end

228
macosx/FileNameCellView.mm Normal file
View File

@@ -0,0 +1,228 @@
// This file Copyright © Transmission authors and contributors.
// It may be used under the MIT (SPDX: MIT) license.
// License text can be found in the licenses/ folder.
#include <libtransmission/transmission.h>
#include <libtransmission/utils.h>
#import "FileNameCellView.h"
#import "FileListNode.h"
#import "Torrent.h"
#import "NSStringAdditions.h"
static CGFloat const kPaddingHorizontal = 2.0;
static CGFloat const kImageFolderSize = 16.0;
static CGFloat const kImageIconSize = 32.0;
static CGFloat const kPaddingBetweenImageAndTitle = 4.0;
static CGFloat const kPaddingAboveTitleFile = 2.0;
static CGFloat const kPaddingBelowStatusFile = 2.0;
static CGFloat const kPaddingBetweenNameAndFolderStatus = 4.0;
@interface FileNameCellView ()
@property(nonatomic, weak) NSImageView* iconView;
@property(nonatomic, weak) NSTextField* nameField;
@property(nonatomic, weak) NSTextField* statusField;
@property(nonatomic, strong) NSArray<NSLayoutConstraint*>* dynamicConstraints;
@end
@implementation FileNameCellView
- (instancetype)initWithFrame:(NSRect)frameRect
{
if ((self = [super initWithFrame:frameRect]))
{
// Create icon view
NSImageView* iconView = [[NSImageView alloc] initWithFrame:NSZeroRect];
iconView.translatesAutoresizingMaskIntoConstraints = NO;
iconView.imageScaling = NSImageScaleProportionallyDown;
[self addSubview:iconView];
_iconView = iconView;
// Create name field
NSTextField* nameField = [[NSTextField alloc] initWithFrame:NSZeroRect];
nameField.translatesAutoresizingMaskIntoConstraints = NO;
nameField.editable = NO;
nameField.selectable = NO;
nameField.bordered = NO;
nameField.backgroundColor = NSColor.clearColor;
nameField.font = [NSFont messageFontOfSize:12.0];
nameField.lineBreakMode = NSLineBreakByTruncatingMiddle;
[self addSubview:nameField];
_nameField = nameField;
self.textField = nameField;
// Create status field
NSTextField* statusField = [[NSTextField alloc] initWithFrame:NSZeroRect];
statusField.translatesAutoresizingMaskIntoConstraints = NO;
statusField.editable = NO;
statusField.selectable = NO;
statusField.bordered = NO;
statusField.backgroundColor = NSColor.clearColor;
statusField.font = [NSFont messageFontOfSize:9.0];
statusField.textColor = NSColor.secondaryLabelColor;
statusField.lineBreakMode = NSLineBreakByTruncatingTail;
[self addSubview:statusField];
_statusField = statusField;
// Setup constraints
[self setupConstraints];
}
return self;
}
- (void)setupConstraints
{
NSImageView* iconView = self.iconView;
NSTextField* nameField = self.nameField;
// Fixed constraints that don't change
[NSLayoutConstraint activateConstraints:@[
// Icon view constraints
[iconView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor constant:kPaddingHorizontal],
[iconView.centerYAnchor constraintEqualToAnchor:self.centerYAnchor],
[iconView.widthAnchor constraintEqualToConstant:kImageIconSize],
[iconView.heightAnchor constraintEqualToConstant:kImageIconSize],
// Name field leading constraint
[nameField.leadingAnchor constraintEqualToAnchor:iconView.trailingAnchor constant:kPaddingBetweenImageAndTitle],
]];
self.dynamicConstraints = @[];
}
- (void)setNode:(FileListNode*)node
{
_node = node;
[self updateDisplay];
}
- (void)updateDisplay
{
if (!self.node)
{
return;
}
FileListNode* node = self.node;
// Update icon
self.iconView.image = node.icon;
// Update icon size constraints based on folder/file
CGFloat const imageSize = node.isFolder ? kImageFolderSize : kImageIconSize;
for (NSLayoutConstraint* constraint in self.iconView.constraints)
{
if (constraint.firstAttribute == NSLayoutAttributeWidth || constraint.firstAttribute == NSLayoutAttributeHeight)
{
constraint.constant = imageSize;
}
}
// Update name
self.nameField.stringValue = node.name;
// Update status
Torrent* torrent = node.torrent;
CGFloat const progress = [torrent fileProgress:node];
NSString* percentString = [NSString percentString:progress longDecimals:YES];
NSString* status = [NSString stringWithFormat:NSLocalizedString(@"%@ of %@", "Inspector -> Files tab -> file status string"),
percentString,
[NSString stringForFileSize:node.size]];
self.statusField.stringValue = status;
// Update layout constraints based on folder vs file
[NSLayoutConstraint deactivateConstraints:self.dynamicConstraints];
NSTextField* nameField = self.nameField;
NSTextField* statusField = self.statusField;
if (node.isFolder)
{
// For folders, status appears next to name, both centered
self.statusField.hidden = NO;
self.dynamicConstraints = @[
[nameField.centerYAnchor constraintEqualToAnchor:self.centerYAnchor],
[nameField.trailingAnchor constraintLessThanOrEqualToAnchor:statusField.leadingAnchor
constant:-kPaddingBetweenNameAndFolderStatus],
[statusField.leadingAnchor constraintEqualToAnchor:nameField.trailingAnchor constant:kPaddingBetweenNameAndFolderStatus],
[statusField.centerYAnchor constraintEqualToAnchor:self.centerYAnchor],
[statusField.trailingAnchor constraintLessThanOrEqualToAnchor:self.trailingAnchor],
];
}
else
{
// For files, status appears below name
self.statusField.hidden = NO;
self.dynamicConstraints = @[
[nameField.topAnchor constraintEqualToAnchor:self.topAnchor constant:kPaddingAboveTitleFile],
[nameField.trailingAnchor constraintLessThanOrEqualToAnchor:self.trailingAnchor],
[statusField.leadingAnchor constraintEqualToAnchor:nameField.leadingAnchor],
[statusField.trailingAnchor constraintEqualToAnchor:self.trailingAnchor],
[statusField.bottomAnchor constraintEqualToAnchor:self.bottomAnchor constant:-kPaddingBelowStatusFile],
];
}
[NSLayoutConstraint activateConstraints:self.dynamicConstraints];
// Update colors based on background style and check state
[self updateColors];
// Update tooltip
[self updateTooltip];
}
- (void)updateTooltip
{
if (!self.node)
{
return;
}
FileListNode* node = self.node;
Torrent* torrent = node.torrent;
NSString* path = [torrent fileLocation:node];
if (!path)
{
path = [node.path stringByAppendingPathComponent:node.name];
}
self.toolTip = path;
}
- (void)setBackgroundStyle:(NSBackgroundStyle)backgroundStyle
{
[super setBackgroundStyle:backgroundStyle];
[self updateColors];
}
- (void)updateColors
{
if (!self.node)
{
return;
}
FileListNode* node = self.node;
Torrent* torrent = node.torrent;
if (self.backgroundStyle == NSBackgroundStyleEmphasized)
{
self.nameField.textColor = NSColor.whiteColor;
self.statusField.textColor = NSColor.whiteColor;
}
else if ([torrent checkForFiles:node.indexes] == NSControlStateValueOff)
{
self.nameField.textColor = NSColor.disabledControlTextColor;
self.statusField.textColor = NSColor.disabledControlTextColor;
}
else
{
self.nameField.textColor = NSColor.controlTextColor;
self.statusField.textColor = NSColor.secondaryLabelColor;
}
}
@end

View File

@@ -13,7 +13,7 @@
@property(nonatomic) Torrent* torrent;
@property(nonatomic) NSString* filterText;
- (void)refresh;
- (void)reloadVisibleRows;
- (void)setCheck:(id)sender;
- (void)setOnlySelectedCheck:(id)sender;

View File

@@ -6,7 +6,9 @@
#import "Torrent.h"
#import "FileListNode.h"
#import "FileOutlineView.h"
#import "FilePriorityCell.h"
#import "FileNameCellView.h"
#import "FilePriorityCellView.h"
#import "FileCheckCellView.h"
#import "FileRenameSheetController.h"
#import "NSMutableArrayAdditions.h"
#import "NSStringAdditions.h"
@@ -24,7 +26,7 @@ typedef NS_ENUM(NSUInteger, FilePriorityMenuTag) { //
FilePriorityMenuTagLow
};
@interface FileOutlineController ()
@interface FileOutlineController ()<NSOutlineViewDelegate, NSOutlineViewDataSource, NSMenuItemValidation>
@property(nonatomic) NSMutableArray<FileListNode*>* fFileList;
@@ -186,18 +188,18 @@ typedef NS_ENUM(NSUInteger, FilePriorityMenuTag) { //
_filterText = text;
}
- (void)refresh
- (void)reloadVisibleRows
{
self.fOutline.needsDisplay = YES;
NSRect visibleRect = self.fOutline.visibleRect;
NSRange range = [self.fOutline rowsInRect:visibleRect];
NSIndexSet* rowIndexes = [NSIndexSet indexSetWithIndexesInRange:range];
NSIndexSet* columnIndexes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, self.fOutline.numberOfColumns)];
[self.fOutline reloadDataForRowIndexes:rowIndexes columnIndexes:columnIndexes];
}
- (void)outlineViewSelectionDidChange:(NSNotification*)notification
{
if ([QLPreviewPanel sharedPreviewPanelExists] && [QLPreviewPanel sharedPreviewPanel].visible)
{
[[QLPreviewPanel sharedPreviewPanel] reloadData];
}
}
#pragma mark - NSOutlineViewDataSource
- (NSInteger)outlineView:(NSOutlineView*)outlineView numberOfChildrenOfItem:(id)item
{
@@ -222,61 +224,51 @@ typedef NS_ENUM(NSUInteger, FilePriorityMenuTag) { //
return (item ? ((FileListNode*)item).children : self.fFileList)[index];
}
- (id)outlineView:(NSOutlineView*)outlineView objectValueForTableColumn:(NSTableColumn*)tableColumn byItem:(id)item
{
if ([tableColumn.identifier isEqualToString:@"Check"])
{
return @([self.torrent checkForFiles:((FileListNode*)item).indexes]);
}
else
{
return item;
}
}
#pragma mark - NSOutlineViewDelegate
- (void)outlineView:(NSOutlineView*)outlineView
willDisplayCell:(id)cell
forTableColumn:(NSTableColumn*)tableColumn
item:(id)item
- (NSView*)outlineView:(NSOutlineView*)outlineView viewForTableColumn:(NSTableColumn*)tableColumn item:(id)item
{
NSString* identifier = tableColumn.identifier;
if ([identifier isEqualToString:@"Check"])
FileListNode* node = (FileListNode*)item;
if ([identifier isEqualToString:@"Name"])
{
[cell setEnabled:[self.torrent canChangeDownloadCheckForFiles:((FileListNode*)item).indexes]];
FileNameCellView* cellView = [outlineView makeViewWithIdentifier:@"NameCell" owner:self];
if (!cellView)
{
cellView = [[FileNameCellView alloc] initWithFrame:NSZeroRect];
cellView.identifier = @"NameCell";
}
cellView.node = node;
return cellView;
}
else if ([identifier isEqualToString:@"Priority"])
{
[cell setRepresentedObject:item];
FilePriorityCellView* cellView = [outlineView makeViewWithIdentifier:@"PriorityCell" owner:self];
if (!cellView)
{
cellView = [[FilePriorityCellView alloc] initWithFrame:NSZeroRect];
cellView.identifier = @"PriorityCell";
}
cellView.node = node;
NSInteger hoveredRow = self.fOutline.hoveredRow;
((FilePriorityCell*)cell).hovered = hoveredRow != -1 && hoveredRow == [self.fOutline rowForItem:item];
return cellView;
}
}
- (void)outlineView:(NSOutlineView*)outlineView
setObjectValue:(id)object
forTableColumn:(NSTableColumn*)tableColumn
byItem:(id)item
{
NSString* identifier = tableColumn.identifier;
if ([identifier isEqualToString:@"Check"])
else if ([identifier isEqualToString:@"Check"])
{
NSIndexSet* indexSet;
if (NSEvent.modifierFlags & NSEventModifierFlagOption)
FileCheckCellView* cellView = [outlineView makeViewWithIdentifier:@"CheckCell" owner:self];
if (!cellView)
{
indexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, self.torrent.fileCount)];
}
else
{
indexSet = ((FileListNode*)item).indexes;
cellView = [[FileCheckCellView alloc] initWithFrame:NSZeroRect];
cellView.identifier = @"CheckCell";
}
cellView.node = node;
[self.torrent setFileCheckState:[object intValue] != NSControlStateValueOff ? NSControlStateValueOn : NSControlStateValueOff
forIndexes:indexSet];
self.fOutline.needsDisplay = YES;
[NSNotificationCenter.defaultCenter postNotificationName:@"UpdateUI" object:nil];
return cellView;
}
return nil;
}
- (NSString*)outlineView:(NSOutlineView*)outlineView typeSelectStringForTableColumn:(NSTableColumn*)tableColumn item:(id)item
@@ -284,60 +276,13 @@ typedef NS_ENUM(NSUInteger, FilePriorityMenuTag) { //
return ((FileListNode*)item).name;
}
- (NSString*)outlineView:(NSOutlineView*)outlineView
toolTipForCell:(NSCell*)cell
rect:(NSRectPointer)rect
tableColumn:(NSTableColumn*)tableColumn
item:(id)item
mouseLocation:(NSPoint)mouseLocation
- (void)outlineViewSelectionDidChange:(NSNotification*)notification
{
NSString* ident = tableColumn.identifier;
if ([ident isEqualToString:@"Name"])
[self reloadVisibleRows];
if ([QLPreviewPanel sharedPreviewPanelExists] && [QLPreviewPanel sharedPreviewPanel].visible)
{
NSString* path = [self.torrent fileLocation:item];
if (!path)
{
FileListNode* node = (FileListNode*)item;
path = [node.path stringByAppendingPathComponent:node.name];
}
return path;
[[QLPreviewPanel sharedPreviewPanel] reloadData];
}
else if ([ident isEqualToString:@"Check"])
{
switch (cell.state)
{
case NSControlStateValueOff:
return NSLocalizedString(@"Don't Download", "files tab -> tooltip");
case NSControlStateValueOn:
return NSLocalizedString(@"Download", "files tab -> tooltip");
case NSControlStateValueMixed:
return NSLocalizedString(@"Download Some", "files tab -> tooltip");
}
}
else if ([ident isEqualToString:@"Priority"])
{
NSSet* priorities = [self.torrent filePrioritiesForIndexes:((FileListNode*)item).indexes];
switch (priorities.count)
{
case 0:
return NSLocalizedString(@"Priority Not Available", "files tab -> tooltip");
case 1:
switch ([[priorities anyObject] intValue])
{
case TR_PRI_LOW:
return NSLocalizedString(@"Low Priority", "files tab -> tooltip");
case TR_PRI_HIGH:
return NSLocalizedString(@"High Priority", "files tab -> tooltip");
case TR_PRI_NORMAL:
return NSLocalizedString(@"Normal Priority", "files tab -> tooltip");
}
break;
default:
return NSLocalizedString(@"Multiple Priorities", "files tab -> tooltip");
}
}
return nil;
}
- (CGFloat)outlineView:(NSOutlineView*)outlineView heightOfRowByItem:(id)item
@@ -352,9 +297,11 @@ typedef NS_ENUM(NSUInteger, FilePriorityMenuTag) { //
}
}
#pragma mark - Actions
- (void)setCheck:(id)sender
{
NSInteger state = [sender tag] == FileCheckMenuTagUncheck ? NSControlStateValueOff : NSControlStateValueOn;
NSControlStateValue state = [sender tag] == FileCheckMenuTagUncheck ? NSControlStateValueOff : NSControlStateValueOn;
NSIndexSet* indexSet = self.fOutline.selectedRowIndexes;
NSMutableIndexSet* itemIndexes = [NSMutableIndexSet indexSet];
@@ -365,7 +312,8 @@ typedef NS_ENUM(NSUInteger, FilePriorityMenuTag) { //
}
[self.torrent setFileCheckState:state forIndexes:itemIndexes];
self.fOutline.needsDisplay = YES;
[self reloadVisibleRows];
}
- (void)setOnlySelectedCheck:(id)sender
@@ -384,21 +332,23 @@ typedef NS_ENUM(NSUInteger, FilePriorityMenuTag) { //
[remainingItemIndexes removeIndexes:itemIndexes];
[self.torrent setFileCheckState:NSControlStateValueOff forIndexes:remainingItemIndexes];
self.fOutline.needsDisplay = YES;
[self reloadVisibleRows];
}
- (void)checkAll
{
NSIndexSet* indexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, self.torrent.fileCount)];
[self.torrent setFileCheckState:NSControlStateValueOn forIndexes:indexSet];
self.fOutline.needsDisplay = YES;
[self reloadVisibleRows];
}
- (void)uncheckAll
{
NSIndexSet* indexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, self.torrent.fileCount)];
[self.torrent setFileCheckState:NSControlStateValueOff forIndexes:indexSet];
self.fOutline.needsDisplay = YES;
[self reloadVisibleRows];
}
- (void)setPriority:(id)sender
@@ -429,7 +379,8 @@ typedef NS_ENUM(NSUInteger, FilePriorityMenuTag) { //
}
[self.torrent setFilePriority:priority forIndexes:itemIndexes];
self.fOutline.needsDisplay = YES;
[self reloadVisibleRows];
}
- (void)revealFile:(id)sender
@@ -480,6 +431,8 @@ typedef NS_ENUM(NSUInteger, FilePriorityMenuTag) { //
}
}
#pragma mark - NSMenuItemValidation
#warning make real view controller (Leopard-only) so that Command-R will work
- (BOOL)validateMenuItem:(NSMenuItem*)menuItem
{
@@ -518,7 +471,7 @@ typedef NS_ENUM(NSUInteger, FilePriorityMenuTag) { //
[itemIndexes addIndexes:node.indexes];
}
NSInteger state = (menuItem.tag == FileCheckMenuTagCheck) ? NSControlStateValueOn : NSControlStateValueOff;
NSControlStateValue state = (menuItem.tag == FileCheckMenuTagCheck) ? NSControlStateValueOn : NSControlStateValueOff;
return [self.torrent checkForFiles:itemIndexes] != state && [self.torrent canChangeDownloadCheckForFiles:itemIndexes];
}

View File

@@ -6,8 +6,6 @@
@interface FileOutlineView : NSOutlineView
@property(nonatomic, readonly) NSInteger hoveredRow;
- (NSRect)iconRectForRow:(NSInteger)row;
@end

View File

@@ -4,15 +4,13 @@
#import "InfoWindowController.h"
#import "FileListNode.h"
#import "FileNameCell.h"
#import "FileNameCellView.h"
#import "FileOutlineView.h"
#import "FilePriorityCell.h"
#import "FilePriorityCellView.h"
#import "Torrent.h"
@interface FileOutlineView ()
@property(nonatomic) NSInteger hoveredRow;
@end
@implementation FileOutlineView
@@ -20,16 +18,9 @@
- (void)awakeFromNib
{
[super awakeFromNib];
FileNameCell* nameCell = [[FileNameCell alloc] init];
[self tableColumnWithIdentifier:@"Name"].dataCell = nameCell;
FilePriorityCell* priorityCell = [[FilePriorityCell alloc] init];
[self tableColumnWithIdentifier:@"Priority"].dataCell = priorityCell;
self.autoresizesOutlineColumn = NO;
self.indentationPerLevel = 14.0;
self.hoveredRow = -1;
}
- (void)mouseDown:(NSEvent*)event
@@ -59,61 +50,22 @@
- (NSRect)iconRectForRow:(NSInteger)row
{
FileNameCell* cell = (FileNameCell*)[self preparedCellAtColumn:[self columnWithIdentifier:@"Name"] row:row];
NSRect iconRect = [cell imageRectForBounds:[self rectOfRow:row]];
NSView* view = [self viewAtColumn:[self columnWithIdentifier:@"Name"] row:row makeIfNecessary:NO];
if (![view isKindOfClass:[FileNameCellView class]])
{
return NSZeroRect;
}
FileNameCellView* cellView = (FileNameCellView*)view;
NSImageView* iconView = [cellView valueForKey:@"iconView"];
if (!iconView)
{
return NSZeroRect;
}
NSRect iconRect = [self convertRect:iconView.frame fromView:cellView];
iconRect.origin.x += self.indentationPerLevel * (CGFloat)([self levelForRow:row] + 1);
return iconRect;
}
- (void)updateTrackingAreas
{
[super updateTrackingAreas];
for (NSTrackingArea* area in self.trackingAreas)
{
if (area.owner == self && area.userInfo[@"Row"])
{
[self removeTrackingArea:area];
}
}
NSRange visibleRows = [self rowsInRect:self.visibleRect];
if (visibleRows.length == 0)
{
return;
}
NSPoint mouseLocation = [self convertPoint:self.window.mouseLocationOutsideOfEventStream fromView:nil];
for (NSInteger row = visibleRows.location, col = [self columnWithIdentifier:@"Priority"]; (NSUInteger)row < NSMaxRange(visibleRows); row++)
{
FilePriorityCell* cell = (FilePriorityCell*)[self preparedCellAtColumn:col row:row];
NSDictionary* userInfo = @{ @"Row" : @(row) };
[cell addTrackingAreasForView:self inRect:[self frameOfCellAtColumn:col row:row] withUserInfo:userInfo
mouseLocation:mouseLocation];
}
}
- (void)mouseEntered:(NSEvent*)event
{
NSNumber* row;
if ((row = ((NSDictionary*)event.userData)[@"Row"]))
{
self.hoveredRow = row.intValue;
[self setNeedsDisplayInRect:[self frameOfCellAtColumn:[self columnWithIdentifier:@"Priority"] row:self.hoveredRow]];
}
}
- (void)mouseExited:(NSEvent*)event
{
NSNumber* row;
if ((row = ((NSDictionary*)event.userData)[@"Row"]))
{
[self setNeedsDisplayInRect:[self frameOfCellAtColumn:[self columnWithIdentifier:@"Priority"] row:row.intValue]];
self.hoveredRow = -1;
}
}
@end

View File

@@ -1,16 +0,0 @@
// This file Copyright © Transmission authors and contributors.
// It may be used under the MIT (SPDX: MIT) license.
// License text can be found in the licenses/ folder.
#import <AppKit/AppKit.h>
@interface FilePriorityCell : NSSegmentedCell
@property(nonatomic) BOOL hovered;
- (void)addTrackingAreasForView:(NSView*)controlView
inRect:(NSRect)cellFrame
withUserInfo:(NSDictionary*)userInfo
mouseLocation:(NSPoint)mouseLocation;
@end

View File

@@ -1,171 +0,0 @@
// This file Copyright © Transmission authors and contributors.
// It may be used under the MIT (SPDX: MIT) license.
// License text can be found in the licenses/ folder.
#import "FilePriorityCell.h"
#import "FileOutlineView.h"
#import "FileListNode.h"
#import "NSImageAdditions.h"
#import "Torrent.h"
static CGFloat const kImageOverlap = 1.0;
@implementation FilePriorityCell
- (instancetype)init
{
if ((self = [super init]))
{
self.trackingMode = NSSegmentSwitchTrackingSelectAny;
self.controlSize = NSControlSizeMini;
self.segmentCount = 3;
for (NSInteger i = 0; i < self.segmentCount; i++)
{
[self setLabel:@"" forSegment:i];
[self setWidth:9.0f forSegment:i]; //9 is minimum size to get proper look
}
[self setImage:[NSImage imageNamed:@"PriorityControlLow"] forSegment:0];
[self setImage:[NSImage imageNamed:@"PriorityControlNormal"] forSegment:1];
[self setImage:[NSImage imageNamed:@"PriorityControlHigh"] forSegment:2];
_hovered = NO;
}
return self;
}
- (id)copyWithZone:(NSZone*)zone
{
FilePriorityCell* copy = [super copyWithZone:zone];
[copy setRepresentedObject:self.representedObject];
return copy;
}
- (void)setSelected:(BOOL)flag forSegment:(NSInteger)segment
{
[super setSelected:flag forSegment:segment];
//only for when clicking manually
tr_priority_t priority;
switch (segment)
{
case 0:
priority = TR_PRI_LOW;
break;
case 1:
priority = TR_PRI_NORMAL;
break;
case 2:
priority = TR_PRI_HIGH;
break;
default:
NSAssert1(NO, @"Unknown segment: %ld", segment);
return;
}
FileListNode* node = self.representedObject;
Torrent* torrent = node.torrent;
[torrent setFilePriority:priority forIndexes:node.indexes];
FileOutlineView* controlView = (FileOutlineView*)self.controlView;
controlView.needsDisplay = YES;
}
- (void)addTrackingAreasForView:(NSView*)controlView
inRect:(NSRect)cellFrame
withUserInfo:(NSDictionary*)userInfo
mouseLocation:(NSPoint)mouseLocation
{
NSTrackingAreaOptions options = NSTrackingEnabledDuringMouseDrag | NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways;
if (NSMouseInRect(mouseLocation, cellFrame, controlView.flipped))
{
options |= NSTrackingAssumeInside;
[controlView setNeedsDisplayInRect:cellFrame];
}
NSTrackingArea* area = [[NSTrackingArea alloc] initWithRect:cellFrame options:options owner:controlView userInfo:userInfo];
[controlView addTrackingArea:area];
}
- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView*)controlView
{
FileListNode* node = self.representedObject;
Torrent* torrent = node.torrent;
NSSet* priorities = [torrent filePrioritiesForIndexes:node.indexes];
NSUInteger const count = priorities.count;
if (self.hovered && count > 0)
{
[super setSelected:[priorities containsObject:@(TR_PRI_LOW)] forSegment:0];
[super setSelected:[priorities containsObject:@(TR_PRI_NORMAL)] forSegment:1];
[super setSelected:[priorities containsObject:@(TR_PRI_HIGH)] forSegment:2];
[super drawWithFrame:cellFrame inView:controlView];
}
else
{
NSMutableArray* images = [NSMutableArray arrayWithCapacity:MAX(count, 1u)];
CGFloat totalWidth;
if (count == 0)
{
//if ([self backgroundStyle] != NSBackgroundStyleEmphasized)
{
NSImage* image = [[NSImage imageNamed:@"PriorityNormalTemplate"] imageWithColor:NSColor.lightGrayColor];
[images addObject:image];
totalWidth = image.size.width;
}
}
else
{
NSColor* priorityColor = self.backgroundStyle == NSBackgroundStyleEmphasized ? NSColor.whiteColor : NSColor.darkGrayColor;
totalWidth = 0.0;
if ([priorities containsObject:@(TR_PRI_LOW)])
{
NSImage* image = [[NSImage imageNamed:@"PriorityLowTemplate"] imageWithColor:priorityColor];
[images addObject:image];
totalWidth += image.size.width;
}
if ([priorities containsObject:@(TR_PRI_NORMAL)])
{
NSImage* image = [[NSImage imageNamed:@"PriorityNormalTemplate"] imageWithColor:priorityColor];
[images addObject:image];
totalWidth += image.size.width;
}
if ([priorities containsObject:@(TR_PRI_HIGH)])
{
NSImage* image = [[NSImage imageNamed:@"PriorityHighTemplate"] imageWithColor:priorityColor];
[images addObject:image];
totalWidth += image.size.width;
}
}
if (count > 1)
{
totalWidth -= kImageOverlap * (count - 1);
}
CGFloat currentWidth = floor(NSMidX(cellFrame) - totalWidth * 0.5);
for (NSImage* image in images)
{
NSSize const imageSize = image.size;
NSRect const imageRect = NSMakeRect(
currentWidth,
floor(NSMidY(cellFrame) - imageSize.height * 0.5),
imageSize.width,
imageSize.height);
[image drawInRect:imageRect fromRect:NSZeroRect operation:NSCompositingOperationSourceOver fraction:1.0
respectFlipped:YES
hints:nil];
currentWidth += imageSize.width - kImageOverlap;
}
}
}
@end

View File

@@ -0,0 +1,14 @@
// This file Copyright © Transmission authors and contributors.
// It may be used under the MIT (SPDX: MIT) license.
// License text can be found in the licenses/ folder.
#import <AppKit/AppKit.h>
@class FileListNode;
@interface FilePriorityCellView : NSTableCellView
@property(nonatomic, weak) FileListNode* node;
@property(nonatomic) BOOL hovered;
@end

View File

@@ -0,0 +1,313 @@
// This file Copyright © Transmission authors and contributors.
// It may be used under the MIT (SPDX: MIT) license.
// License text can be found in the licenses/ folder.
#import "FilePriorityCellView.h"
#import "FileListNode.h"
#import "NSImageAdditions.h"
#import "Torrent.h"
static CGFloat const kImageOverlap = 1.0;
@interface FilePriorityCellView ()
@property(nonatomic, weak) NSSegmentedControl* segmentedControl;
@property(nonatomic, weak) NSView* iconsContainerView;
@property(nonatomic, strong) NSTrackingArea* trackingArea;
@end
@implementation FilePriorityCellView
- (instancetype)initWithFrame:(NSRect)frameRect
{
if ((self = [super initWithFrame:frameRect]))
{
// Create segmented control for hover state
NSSegmentedControl* segmentedControl = [[NSSegmentedControl alloc] initWithFrame:NSZeroRect];
segmentedControl.translatesAutoresizingMaskIntoConstraints = NO;
segmentedControl.trackingMode = NSSegmentSwitchTrackingSelectAny;
segmentedControl.controlSize = NSControlSizeMini;
segmentedControl.segmentCount = 3;
for (NSInteger i = 0; i < segmentedControl.segmentCount; i++)
{
[segmentedControl setLabel:@"" forSegment:i];
[segmentedControl setWidth:9.0f forSegment:i];
}
[segmentedControl setImage:[NSImage imageNamed:@"PriorityControlLow"] forSegment:0];
[segmentedControl setImage:[NSImage imageNamed:@"PriorityControlNormal"] forSegment:1];
[segmentedControl setImage:[NSImage imageNamed:@"PriorityControlHigh"] forSegment:2];
segmentedControl.target = self;
segmentedControl.action = @selector(segmentedControlClicked:);
segmentedControl.hidden = YES;
[self addSubview:segmentedControl];
_segmentedControl = segmentedControl;
// Create container view for priority icons
NSView* iconsContainerView = [[NSView alloc] initWithFrame:NSZeroRect];
iconsContainerView.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:iconsContainerView];
_iconsContainerView = iconsContainerView;
// Setup constraints
[NSLayoutConstraint activateConstraints:@[
[segmentedControl.centerXAnchor constraintEqualToAnchor:self.centerXAnchor],
[segmentedControl.centerYAnchor constraintEqualToAnchor:self.centerYAnchor],
[iconsContainerView.centerXAnchor constraintEqualToAnchor:self.centerXAnchor],
[iconsContainerView.centerYAnchor constraintEqualToAnchor:self.centerYAnchor],
[iconsContainerView.widthAnchor constraintLessThanOrEqualToAnchor:self.widthAnchor],
[iconsContainerView.heightAnchor constraintLessThanOrEqualToAnchor:self.heightAnchor],
]];
_hovered = NO;
}
return self;
}
- (void)setNode:(FileListNode*)node
{
_node = node;
[self updateDisplay];
}
- (void)setHovered:(BOOL)hovered
{
_hovered = hovered;
[self updateDisplay];
}
- (void)updateDisplay
{
if (!self.node)
{
return;
}
FileListNode* node = self.node;
Torrent* torrent = node.torrent;
NSSet* priorities = [torrent filePrioritiesForIndexes:node.indexes];
NSUInteger const count = priorities.count;
if (self.hovered && count > 0)
{
// Show segmented control
self.segmentedControl.hidden = NO;
self.iconsContainerView.hidden = YES;
[self.segmentedControl setSelected:[priorities containsObject:@(TR_PRI_LOW)] forSegment:0];
[self.segmentedControl setSelected:[priorities containsObject:@(TR_PRI_NORMAL)] forSegment:1];
[self.segmentedControl setSelected:[priorities containsObject:@(TR_PRI_HIGH)] forSegment:2];
}
else
{
// Show static priority icons
self.segmentedControl.hidden = YES;
self.iconsContainerView.hidden = NO;
[self updatePriorityIcons:priorities];
}
// Update tooltip
[self updateTooltip];
}
- (void)updatePriorityIcons:(NSSet*)priorities
{
// Remove all existing image views
for (NSView* subview in self.iconsContainerView.subviews)
{
[subview removeFromSuperview];
}
NSUInteger const count = priorities.count;
NSMutableArray* images = [NSMutableArray arrayWithCapacity:MAX(count, 1u)];
if (count == 0)
{
NSImage* image = [[NSImage imageNamed:@"PriorityNormalTemplate"] imageWithColor:NSColor.lightGrayColor];
[images addObject:image];
}
else
{
NSColor* priorityColor = self.backgroundStyle == NSBackgroundStyleEmphasized ? NSColor.whiteColor : NSColor.darkGrayColor;
if ([priorities containsObject:@(TR_PRI_LOW)])
{
NSImage* image = [[NSImage imageNamed:@"PriorityLowTemplate"] imageWithColor:priorityColor];
[images addObject:image];
}
if ([priorities containsObject:@(TR_PRI_NORMAL)])
{
NSImage* image = [[NSImage imageNamed:@"PriorityNormalTemplate"] imageWithColor:priorityColor];
[images addObject:image];
}
if ([priorities containsObject:@(TR_PRI_HIGH)])
{
NSImage* image = [[NSImage imageNamed:@"PriorityHighTemplate"] imageWithColor:priorityColor];
[images addObject:image];
}
}
NSView* previousView = nil;
for (NSImage* image in images)
{
NSImageView* imageView = [[NSImageView alloc] initWithFrame:NSZeroRect];
imageView.translatesAutoresizingMaskIntoConstraints = NO;
imageView.image = image;
[self.iconsContainerView addSubview:imageView];
NSSize const imageSize = image.size;
[NSLayoutConstraint activateConstraints:@[
[imageView.widthAnchor constraintEqualToConstant:imageSize.width],
[imageView.heightAnchor constraintEqualToConstant:imageSize.height],
[imageView.centerYAnchor constraintEqualToAnchor:self.iconsContainerView.centerYAnchor],
]];
if (previousView == nil)
{
[imageView.leadingAnchor constraintEqualToAnchor:self.iconsContainerView.leadingAnchor].active = YES;
}
else
{
[imageView.leadingAnchor constraintEqualToAnchor:previousView.trailingAnchor constant:-kImageOverlap].active = YES;
}
previousView = imageView;
}
if (previousView)
{
[previousView.trailingAnchor constraintEqualToAnchor:self.iconsContainerView.trailingAnchor].active = YES;
}
}
- (void)segmentedControlClicked:(NSSegmentedControl*)sender
{
NSInteger segment = sender.selectedSegment;
if (segment == -1)
{
return;
}
tr_priority_t priority;
switch (segment)
{
case 0:
priority = TR_PRI_LOW;
break;
case 1:
priority = TR_PRI_NORMAL;
break;
case 2:
priority = TR_PRI_HIGH;
break;
default:
NSAssert1(NO, @"Unknown segment: %ld", segment);
return;
}
FileListNode* node = self.node;
Torrent* torrent = node.torrent;
[torrent setFilePriority:priority forIndexes:node.indexes];
// Notify that we need to refresh
[NSNotificationCenter.defaultCenter postNotificationName:@"UpdateUI" object:nil];
}
- (void)setBackgroundStyle:(NSBackgroundStyle)backgroundStyle
{
[super setBackgroundStyle:backgroundStyle];
[self updateDisplay];
}
- (void)updateTrackingAreas
{
[super updateTrackingAreas];
if (self.trackingArea)
{
[self removeTrackingArea:self.trackingArea];
}
NSTrackingAreaOptions options = NSTrackingMouseEnteredAndExited | NSTrackingActiveInActiveApp;
// Check if mouse is currently inside the bounds
NSPoint mouseLocation = [self.window mouseLocationOutsideOfEventStream];
NSPoint localPoint = [self convertPoint:mouseLocation fromView:nil];
if (NSPointInRect(localPoint, self.bounds))
{
options |= NSTrackingAssumeInside;
if (!self.hovered)
{
self.hovered = YES;
}
}
else
{
// Mouse is not inside, reset hovered state
if (self.hovered)
{
self.hovered = NO;
}
}
self.trackingArea = [[NSTrackingArea alloc] initWithRect:self.bounds options:options owner:self userInfo:nil];
[self addTrackingArea:self.trackingArea];
}
- (void)mouseEntered:(NSEvent*)event
{
self.hovered = YES;
}
- (void)mouseExited:(NSEvent*)event
{
self.hovered = NO;
}
- (void)updateTooltip
{
if (!self.node)
{
return;
}
FileListNode* node = self.node;
Torrent* torrent = node.torrent;
NSSet* priorities = [torrent filePrioritiesForIndexes:node.indexes];
NSString* tooltip = nil;
switch (priorities.count)
{
case 0:
tooltip = NSLocalizedString(@"Priority Not Available", "files tab -> tooltip");
break;
case 1:
switch ([[priorities anyObject] intValue])
{
case TR_PRI_LOW:
tooltip = NSLocalizedString(@"Low Priority", "files tab -> tooltip");
break;
case TR_PRI_HIGH:
tooltip = NSLocalizedString(@"High Priority", "files tab -> tooltip");
break;
case TR_PRI_NORMAL:
tooltip = NSLocalizedString(@"Normal Priority", "files tab -> tooltip");
break;
}
break;
default:
tooltip = NSLocalizedString(@"Multiple Priorities", "files tab -> tooltip");
break;
}
self.toolTip = tooltip;
}
@end

View File

@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="20037" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="24412" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="20037"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="24412"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
@@ -20,8 +20,8 @@
<customView clipsToBounds="YES" translatesAutoresizingMaskIntoConstraints="NO" id="2" userLabel="Files">
<rect key="frame" x="0.0" y="0.0" width="340" height="365"/>
<subviews>
<searchField wantsLayer="YES" verticalHuggingPriority="750" allowsCharacterPickerTouchBarItem="YES" translatesAutoresizingMaskIntoConstraints="NO" id="3">
<rect key="frame" x="12" y="12" width="110" height="19"/>
<searchField wantsLayer="YES" focusRingType="none" verticalHuggingPriority="750" allowsCharacterPickerTouchBarItem="YES" translatesAutoresizingMaskIntoConstraints="NO" id="3">
<rect key="frame" x="12" y="12" width="110" height="20"/>
<constraints>
<constraint firstAttribute="width" constant="110" id="sKS-V5-H9b"/>
</constraints>
@@ -35,19 +35,19 @@
</connections>
</searchField>
<scrollView horizontalLineScroll="36" horizontalPageScroll="10" verticalLineScroll="36" verticalPageScroll="10" hasHorizontalScroller="NO" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="4">
<rect key="frame" x="12" y="39" width="316" height="314"/>
<rect key="frame" x="12" y="40" width="316" height="313"/>
<clipView key="contentView" id="l96-jk-uz9">
<rect key="frame" x="1" y="1" width="314" height="312"/>
<rect key="frame" x="1" y="1" width="301" height="311"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<outlineView verticalHuggingPriority="750" allowsExpansionToolTips="YES" columnReordering="NO" columnResizing="NO" autosaveColumns="NO" rowHeight="34" indentationPerLevel="16" autoresizesOutlineColumn="YES" outlineTableColumn="10" id="7" customClass="FileOutlineView">
<rect key="frame" x="0.0" y="0.0" width="314" height="312"/>
<outlineView verticalHuggingPriority="750" allowsExpansionToolTips="YES" alternatingRowBackgroundColors="YES" columnReordering="NO" columnResizing="NO" autosaveColumns="NO" rowHeight="34" rowSizeStyle="automatic" viewBased="YES" indentationPerLevel="16" autoresizesOutlineColumn="YES" outlineTableColumn="10" id="7" customClass="FileOutlineView">
<rect key="frame" x="0.0" y="0.0" width="301" height="311"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<size key="intercellSpacing" width="3" height="2"/>
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
<color key="gridColor" name="gridColor" catalog="System" colorSpace="catalog"/>
<tableColumns>
<tableColumn identifier="Name" editable="NO" width="231" minWidth="38.599119999999999" maxWidth="1000" id="10">
<tableColumn identifier="Name" editable="NO" width="218" minWidth="38.599119999999999" maxWidth="1000" id="10">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="left" title="Name">
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" white="0.33333299" alpha="1" colorSpace="calibratedWhite"/>
@@ -58,6 +58,26 @@
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES"/>
<prototypeCellViews>
<tableCellView id="ddm-cs-nIR">
<rect key="frame" x="1" y="1" width="223" height="34"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="HIK-di-9tP">
<rect key="frame" x="0.0" y="9" width="223" height="16"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES" flexibleMaxY="YES"/>
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="Table View Cell" id="D7f-0h-h8A">
<font key="font" usesAppearanceFont="YES"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<connections>
<outlet property="textField" destination="HIK-di-9tP" id="bHT-uH-6AK"/>
</connections>
</tableCellView>
</prototypeCellViews>
</tableColumn>
<tableColumn identifier="Priority" editable="NO" width="34" minWidth="10" maxWidth="1000" id="8">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="left" title="Rank">
@@ -69,6 +89,26 @@
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<prototypeCellViews>
<tableCellView id="4RH-Kf-WF8">
<rect key="frame" x="227" y="1" width="34" height="34"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="DCs-8X-Mcb">
<rect key="frame" x="0.0" y="9" width="34" height="16"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES" flexibleMaxY="YES"/>
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="Table View Cell" id="Doc-xO-gx2">
<font key="font" usesAppearanceFont="YES"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<connections>
<outlet property="textField" destination="DCs-8X-Mcb" id="ad1-R3-pcd"/>
</connections>
</tableCellView>
</prototypeCellViews>
</tableColumn>
<tableColumn identifier="Check" editable="NO" width="31" minWidth="10" maxWidth="1000" id="9">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="left" title="DL">
@@ -79,6 +119,26 @@
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="cellTitle"/>
</buttonCell>
<prototypeCellViews>
<tableCellView id="l8k-SJ-e7E">
<rect key="frame" x="264" y="1" width="35" height="34"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="CkN-iw-rOX">
<rect key="frame" x="0.0" y="9" width="35" height="16"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES" flexibleMaxY="YES"/>
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="Table View Cell" id="a2Y-Cf-rdu">
<font key="font" usesAppearanceFont="YES"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<connections>
<outlet property="textField" destination="CkN-iw-rOX" id="Thn-jw-ARn"/>
</connections>
</tableCellView>
</prototypeCellViews>
</tableColumn>
</tableColumns>
<connections>
@@ -98,12 +158,12 @@
<autoresizingMask key="autoresizingMask"/>
</scroller>
<scroller key="verticalScroller" wantsLayer="YES" verticalHuggingPriority="750" controlSize="small" horizontal="NO" id="6">
<rect key="frame" x="301" y="1" width="14" height="312"/>
<rect key="frame" x="302" y="1" width="13" height="311"/>
<autoresizingMask key="autoresizingMask"/>
</scroller>
</scrollView>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="25">
<rect key="frame" x="286" y="12" width="42" height="17"/>
<rect key="frame" x="280" y="12" width="48" height="20"/>
<buttonCell key="cell" type="roundRect" title="None" bezelStyle="roundedRect" alignment="center" controlSize="small" state="on" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="26">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="smallSystem"/>
@@ -113,7 +173,7 @@
</connections>
</button>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="27">
<rect key="frame" x="236" y="12" width="42" height="17"/>
<rect key="frame" x="224" y="12" width="48" height="20"/>
<buttonCell key="cell" type="roundRect" title="All" bezelStyle="roundedRect" alignment="center" controlSize="small" state="on" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="28">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="smallSystem"/>

View File

@@ -88,13 +88,13 @@
if (self.fTorrents.count == 1)
{
[self.fFileController refresh];
[self.fFileController reloadVisibleRows];
#warning use TorrentFileCheckChange notification as well
Torrent* torrent = self.fTorrents[0];
if (torrent.folder)
{
NSInteger const filesCheckState = [torrent
NSControlStateValue const filesCheckState = [torrent
checkForFiles:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, torrent.fileCount)]];
self.fCheckAllButton.enabled = filesCheckState != NSControlStateValueOn; //if anything is unchecked
self.fUncheckAllButton.enabled = !torrent.allDownloaded; //if there are any checked files that aren't finished

View File

@@ -195,8 +195,8 @@ extern NSString* const kTorrentDidChangeGroupNotification;
- (CGFloat)fileProgress:(FileListNode*)node;
- (BOOL)canChangeDownloadCheckForFile:(NSUInteger)index;
- (BOOL)canChangeDownloadCheckForFiles:(NSIndexSet*)indexSet;
- (NSInteger)checkForFiles:(NSIndexSet*)indexSet;
- (void)setFileCheckState:(NSInteger)state forIndexes:(NSIndexSet*)indexSet;
- (NSControlStateValue)checkForFiles:(NSIndexSet*)indexSet;
- (void)setFileCheckState:(NSControlStateValue)state forIndexes:(NSIndexSet*)indexSet;
- (void)setFilePriority:(tr_priority_t)priority forIndexes:(NSIndexSet*)indexSet;
- (BOOL)hasFilePriority:(tr_priority_t)priority forIndexes:(NSIndexSet*)indexSet;
- (NSSet*)filePrioritiesForIndexes:(NSIndexSet*)indexSet;

View File

@@ -1547,7 +1547,7 @@ bool trashDataFile(char const* filename, void* /*user_data*/, tr_error* error)
return canChange;
}
- (NSInteger)checkForFiles:(NSIndexSet*)indexSet
- (NSControlStateValue)checkForFiles:(NSIndexSet*)indexSet
{
BOOL onState = NO, offState = NO;
for (NSUInteger index = indexSet.firstIndex; index != NSNotFound; index = [indexSet indexGreaterThanIndex:index])
@@ -1570,7 +1570,7 @@ bool trashDataFile(char const* filename, void* /*user_data*/, tr_error* error)
return onState ? NSControlStateValueOn : NSControlStateValueOff;
}
- (void)setFileCheckState:(NSInteger)state forIndexes:(NSIndexSet*)indexSet
- (void)setFileCheckState:(NSControlStateValue)state forIndexes:(NSIndexSet*)indexSet
{
NSUInteger count = indexSet.count;
tr_file_index_t* files = static_cast<tr_file_index_t*>(malloc(count * sizeof(tr_file_index_t)));