refactor: view-based TorrentTableView in macOS client (#5147)

Converted TorrentTableView from older style cell based table to more modern view based
* floating group rows are now used for an improved groups experience
* individual group indicators are hidden when _Use Groups_ is selected to minimize visual clutter (see #3328 )
* removed negated `usesAlternatingRowBackgroundColors` flag for minimal view in Controller.mm (personal preference - easy to restore)
This commit is contained in:
SweetPPro 2023-06-27 21:40:44 +02:00 committed by GitHub
parent 7fdfabe184
commit 635268854b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 1504 additions and 1487 deletions

View File

@ -21,7 +21,13 @@
3C7A11980D0B2EE300B5701F /* getgateway.h in Headers */ = {isa = PBXBuildFile; fileRef = 3C7A11920D0B2EE300B5701F /* getgateway.h */; };
3C7A11990D0B2EE300B5701F /* natpmp.c in Sources */ = {isa = PBXBuildFile; fileRef = 3C7A11930D0B2EE300B5701F /* natpmp.c */; };
3C7A119A0D0B2EE300B5701F /* natpmp.h in Headers */ = {isa = PBXBuildFile; fileRef = 3C7A11940D0B2EE300B5701F /* natpmp.h */; };
454BB0562941E8D800F99F38 /* GroupTextCell.mm in Sources */ = {isa = PBXBuildFile; fileRef = 454BB0542941E8D800F99F38 /* GroupTextCell.mm */; };
4521532B29AF891F009331B0 /* GroupCell.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4521532929AF891E009331B0 /* GroupCell.mm */; };
4521532E29AF9D61009331B0 /* TorrentCell.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4521532D29AF9D61009331B0 /* TorrentCell.mm */; };
4534164229B0EA8600F544C9 /* SmallTorrentCell.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4534164129B0EA8600F544C9 /* SmallTorrentCell.mm */; };
45489C5429AF6FB20098A812 /* TorrentCellActionButton.mm in Sources */ = {isa = PBXBuildFile; fileRef = 45489C5029AF6FB10098A812 /* TorrentCellActionButton.mm */; };
45489C5629AF6FB20098A812 /* TorrentCellRevealButton.mm in Sources */ = {isa = PBXBuildFile; fileRef = 45489C5229AF6FB10098A812 /* TorrentCellRevealButton.mm */; };
45489C5729AF6FB20098A812 /* TorrentCellControlButton.mm in Sources */ = {isa = PBXBuildFile; fileRef = 45489C5329AF6FB10098A812 /* TorrentCellControlButton.mm */; };
4559D21629B32623004EFDF0 /* ProgressBarView.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4559D21529B32623004EFDF0 /* ProgressBarView.mm */; };
457AF8EB28604AFC00BCF74F /* Toolbar.mm in Sources */ = {isa = PBXBuildFile; fileRef = 457AF8EA28604AFC00BCF74F /* Toolbar.mm */; };
45A7D3292843B54D00F0C32A /* GroupPopUpButtonCell.mm in Sources */ = {isa = PBXBuildFile; fileRef = 45A7D3282843B54D00F0C32A /* GroupPopUpButtonCell.mm */; };
45A7D32C2843B55F00F0C32A /* PriorityPopUpButtonCell.mm in Sources */ = {isa = PBXBuildFile; fileRef = 45A7D32B2843B55F00F0C32A /* PriorityPopUpButtonCell.mm */; };
@ -46,7 +52,6 @@
4D80185910BBC0B0008A4AF2 /* magnet-metainfo.cc in Sources */ = {isa = PBXBuildFile; fileRef = 4D80185710BBC0B0008A4AF2 /* magnet-metainfo.cc */; };
4D80185A10BBC0B0008A4AF2 /* magnet-metainfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 4D80185810BBC0B0008A4AF2 /* magnet-metainfo.h */; };
4D9A2BF009E16D21002D0FF9 /* libtransmission.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4D18389709DEC0030047D688 /* libtransmission.a */; };
4DCCBB3E09C3D71100D3CABF /* TorrentCell.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4DCCBB3C09C3D71100D3CABF /* TorrentCell.mm */; };
4DE5CC9D0980656F00BE280E /* NSStringAdditions.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4DE5CC9C0980656F00BE280E /* NSStringAdditions.mm */; };
4DE5CCA70980735700BE280E /* Badger.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4DE5CCA60980735700BE280E /* Badger.mm */; };
4DE5CCCB0981D9BE00BE280E /* Defaults.plist in Resources */ = {isa = PBXBuildFile; fileRef = 4DE5CCCA0981D9BE00BE280E /* Defaults.plist */; };
@ -642,8 +647,21 @@
3C7A11920D0B2EE300B5701F /* getgateway.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = getgateway.h; sourceTree = "<group>"; };
3C7A11930D0B2EE300B5701F /* natpmp.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = natpmp.c; sourceTree = "<group>"; };
3C7A11940D0B2EE300B5701F /* natpmp.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = natpmp.h; sourceTree = "<group>"; };
454BB0542941E8D800F99F38 /* GroupTextCell.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = GroupTextCell.mm; sourceTree = "<group>"; };
454BB0552941E8D800F99F38 /* GroupTextCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GroupTextCell.h; sourceTree = "<group>"; };
4521532929AF891E009331B0 /* GroupCell.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = GroupCell.mm; sourceTree = "<group>"; };
4521532A29AF891F009331B0 /* GroupCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GroupCell.h; sourceTree = "<group>"; };
4521532C29AF9D60009331B0 /* TorrentCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TorrentCell.h; sourceTree = "<group>"; };
4521532D29AF9D61009331B0 /* TorrentCell.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = TorrentCell.mm; sourceTree = "<group>"; };
4534164029B0EA8500F544C9 /* SmallTorrentCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SmallTorrentCell.h; sourceTree = "<group>"; };
4534164129B0EA8600F544C9 /* SmallTorrentCell.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = SmallTorrentCell.mm; sourceTree = "<group>"; };
45489C4529AE70F00098A812 /* libc++.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = "libc++.tbd"; path = "usr/lib/libc++.tbd"; sourceTree = SDKROOT; };
45489C4729AE79FC0098A812 /* TorrentCellActionButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TorrentCellActionButton.h; sourceTree = "<group>"; };
45489C4A29AF10D00098A812 /* TorrentCellRevealButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TorrentCellRevealButton.h; sourceTree = "<group>"; };
45489C4B29AF10D10098A812 /* TorrentCellControlButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TorrentCellControlButton.h; sourceTree = "<group>"; };
45489C5029AF6FB10098A812 /* TorrentCellActionButton.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = TorrentCellActionButton.mm; sourceTree = "<group>"; };
45489C5229AF6FB10098A812 /* TorrentCellRevealButton.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = TorrentCellRevealButton.mm; sourceTree = "<group>"; };
45489C5329AF6FB10098A812 /* TorrentCellControlButton.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = TorrentCellControlButton.mm; sourceTree = "<group>"; };
4559D21429B32622004EFDF0 /* ProgressBarView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ProgressBarView.h; sourceTree = "<group>"; };
4559D21529B32623004EFDF0 /* ProgressBarView.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ProgressBarView.mm; sourceTree = "<group>"; };
455C0939287767270003A078 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/PrefsWindow.strings; sourceTree = "<group>"; };
455C093A287767290003A078 /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/PrefsWindow.strings; sourceTree = "<group>"; };
455C093B2877672C0003A078 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/PrefsWindow.strings; sourceTree = "<group>"; };
@ -771,8 +789,6 @@
4D8017E910BBC073008A4AF2 /* torrent-magnet.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "torrent-magnet.h"; sourceTree = "<group>"; };
4D80185710BBC0B0008A4AF2 /* magnet-metainfo.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = "magnet-metainfo.cc"; sourceTree = "<group>"; };
4D80185810BBC0B0008A4AF2 /* magnet-metainfo.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "magnet-metainfo.h"; sourceTree = "<group>"; };
4DCCBB3C09C3D71100D3CABF /* TorrentCell.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = TorrentCell.mm; sourceTree = "<group>"; };
4DCCBB3D09C3D71100D3CABF /* TorrentCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TorrentCell.h; sourceTree = "<group>"; };
4DDBB71909E16BAE00284745 /* transmissioncli */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = transmissioncli; sourceTree = BUILT_PRODUCTS_DIR; };
4DE5CC9B0980656F00BE280E /* NSStringAdditions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NSStringAdditions.h; sourceTree = "<group>"; };
4DE5CC9C0980656F00BE280E /* NSStringAdditions.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = NSStringAdditions.mm; sourceTree = "<group>"; };
@ -1457,10 +1473,20 @@
A27F0F320E19AD9800B2DB97 /* TorrentGroup.mm */,
4D364D9E091FBB2C00377D12 /* TorrentTableView.h */,
4D364D9F091FBB2C00377D12 /* TorrentTableView.mm */,
4DCCBB3D09C3D71100D3CABF /* TorrentCell.h */,
4DCCBB3C09C3D71100D3CABF /* TorrentCell.mm */,
454BB0552941E8D800F99F38 /* GroupTextCell.h */,
454BB0542941E8D800F99F38 /* GroupTextCell.mm */,
4521532C29AF9D60009331B0 /* TorrentCell.h */,
4521532D29AF9D61009331B0 /* TorrentCell.mm */,
4534164029B0EA8500F544C9 /* SmallTorrentCell.h */,
4534164129B0EA8600F544C9 /* SmallTorrentCell.mm */,
4521532A29AF891F009331B0 /* GroupCell.h */,
4521532929AF891E009331B0 /* GroupCell.mm */,
4559D21429B32622004EFDF0 /* ProgressBarView.h */,
4559D21529B32623004EFDF0 /* ProgressBarView.mm */,
45489C4729AE79FC0098A812 /* TorrentCellActionButton.h */,
45489C5029AF6FB10098A812 /* TorrentCellActionButton.mm */,
45489C4B29AF10D10098A812 /* TorrentCellControlButton.h */,
45489C5329AF6FB10098A812 /* TorrentCellControlButton.mm */,
45489C4A29AF10D00098A812 /* TorrentCellRevealButton.h */,
45489C5229AF6FB10098A812 /* TorrentCellRevealButton.mm */,
A21A9BE0106D86A800F1C3C1 /* TrackerNode.h */,
A21A9BE1106D86A800F1C3C1 /* TrackerNode.mm */,
A2725B6C0DE5C4F5003445E7 /* FileListNode.h */,
@ -2797,7 +2823,7 @@
29B97313FDCFA39411CA2CEA /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 1400;
LastUpgradeCheck = 1420;
ORGANIZATIONNAME = "The Transmission Project";
TargetAttributes = {
8D1107260486CEB800E47090 = {
@ -3127,13 +3153,14 @@
A225A4C0187E369C00CDE823 /* ShareToolbarItem.mm in Sources */,
A2D77453154CC72B00A62B93 /* WebSeedTableView.mm in Sources */,
8D11072D0486CEB800E47090 /* main.mm in Sources */,
45489C5629AF6FB20098A812 /* TorrentCellRevealButton.mm in Sources */,
4DF0C5AB0899190500DD8943 /* Controller.mm in Sources */,
4D118E1A08CB46B20033958F /* PrefsController.mm in Sources */,
4521532E29AF9D61009331B0 /* TorrentCell.mm in Sources */,
4D364DA0091FBB2C00377D12 /* TorrentTableView.mm in Sources */,
4DE5CC9D0980656F00BE280E /* NSStringAdditions.mm in Sources */,
4DE5CCA70980735700BE280E /* Badger.mm in Sources */,
4DFBC2DF09C0970D00D5C571 /* Torrent.mm in Sources */,
4DCCBB3E09C3D71100D3CABF /* TorrentCell.mm in Sources */,
A200B9200A22798F007BBB1E /* InfoWindowController.mm in Sources */,
A2AF1C390A3D0F6200F1575D /* FileOutlineView.mm in Sources */,
A2710E770A86796000CE4F7D /* PrefsWindow.mm in Sources */,
@ -3153,12 +3180,14 @@
A2085DDC0C53BC74000BC3B7 /* AboutWindowController.mm in Sources */,
A21282A80CA6C66800EAEE0F /* StatusBarView.mm in Sources */,
A257C1820CAD3003004E121C /* PeerTableView.mm in Sources */,
45489C5429AF6FB20098A812 /* TorrentCellActionButton.mm in Sources */,
A2A6321B0CD9751700E3DA60 /* BadgeView.mm in Sources */,
A2ED7D8F0CEF431B00970975 /* FilterButton.mm in Sources */,
45A7D32C2843B55F00F0C32A /* PriorityPopUpButtonCell.mm in Sources */,
A25892640CF1F7E800CCCDDF /* StatsWindowController.mm in Sources */,
A2C89D600CFCBF57004CC2BC /* ButtonToolbarItem.mm in Sources */,
A219798B0D07B78400438EA7 /* GroupToolbarItem.mm in Sources */,
4521532B29AF891F009331B0 /* GroupCell.mm in Sources */,
C841A28129197724009F18E8 /* NSKeyedUnarchiverAdditions.mm in Sources */,
A22180980D148A71007D09ED /* GroupsPrefsController.mm in Sources */,
A26AF21A0D2DA35A00FF7140 /* FileOutlineController.mm in Sources */,
@ -3167,9 +3196,11 @@
45A7D3292843B54D00F0C32A /* GroupPopUpButtonCell.mm in Sources */,
A2D307A40D9EC6870051FD27 /* BlocklistDownloader.mm in Sources */,
A2725B6E0DE5C4F5003445E7 /* FileListNode.mm in Sources */,
4559D21629B32623004EFDF0 /* ProgressBarView.mm in Sources */,
A2725D5D0DE7507C003445E7 /* TrackerTableView.mm in Sources */,
A28F4F770E085BDC003A3882 /* ColorTextField.mm in Sources */,
A27F0F330E19AD9800B2DB97 /* TorrentGroup.mm in Sources */,
45489C5729AF6FB20098A812 /* TorrentCellControlButton.mm in Sources */,
C809AEE7291ECFD000BFDBE1 /* NSDataAdditions.mm in Sources */,
A222E9870E6B21D9009FB003 /* BlocklistDownloaderViewController.mm in Sources */,
A222EA7B0E6C32C4009FB003 /* BlocklistScheduler.mm in Sources */,
@ -3192,10 +3223,10 @@
A2E57BA713109E6B00A7DAB1 /* FilterBarController.mm in Sources */,
A2B5B4E91880665E0071A66A /* ShareTorrentFileHelper.mm in Sources */,
A22BAE281388040500FB022F /* NSMutableArrayAdditions.mm in Sources */,
454BB0562941E8D800F99F38 /* GroupTextCell.mm in Sources */,
A2966E8713DAF74C007B52DF /* GlobalOptionsPopoverViewController.mm in Sources */,
A234EA541453563B000F3E97 /* NSImageAdditions.mm in Sources */,
A2AB883E16A399A6008FAD50 /* VDKQueue.mm in Sources */,
4534164229B0EA8600F544C9 /* SmallTorrentCell.mm in Sources */,
A2451E6916ACE4EB00586E0E /* FileRenameSheetController.mm in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;

View File

@ -1,8 +1,7 @@
<?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="21507" 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="21507"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
@ -17,88 +16,341 @@
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" topStrut="YES"/>
<rect key="contentRect" x="71" y="712" width="515" height="248"/>
<rect key="screenRect" x="0.0" y="0.0" width="1440" height="875"/>
<rect key="screenRect" x="0.0" y="0.0" width="1470" height="919"/>
<value key="minSize" type="size" width="350" height="5"/>
<view key="contentView" id="2">
<rect key="frame" x="0.0" y="0.0" width="515" height="248"/>
<rect key="frame" x="0.0" y="0.0" width="524" height="220"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<stackView distribution="fill" orientation="vertical" alignment="leading" spacing="0.0" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Msk-Iv-kCJ">
<rect key="frame" x="0.0" y="0.0" width="515" height="248"/>
<rect key="frame" x="0.0" y="0.0" width="524" height="220"/>
<subviews>
<scrollView borderType="none" autohidesScrollers="YES" horizontalLineScroll="65" horizontalPageScroll="0.0" verticalLineScroll="65" verticalPageScroll="0.0" hasHorizontalScroller="NO" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="3088">
<rect key="frame" x="0.0" y="24" width="515" height="224"/>
<rect key="frame" x="0.0" y="24" width="524" height="196"/>
<clipView key="contentView" copiesOnScroll="NO" id="5cE-5g-l0I">
<rect key="frame" x="0.0" y="0.0" width="515" height="224"/>
<rect key="frame" x="0.0" y="0.0" width="524" height="196"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<outlineView verticalHuggingPriority="750" allowsExpansionToolTips="YES" alternatingRowBackgroundColors="YES" columnReordering="NO" columnResizing="NO" autosaveColumns="NO" rowHeight="62" indentationPerLevel="16" indentationMarkerFollowsCell="NO" outlineTableColumn="3093" id="3091" customClass="TorrentTableView">
<rect key="frame" x="0.0" y="0.0" width="515" height="224"/>
<outlineView verticalHuggingPriority="750" allowsExpansionToolTips="YES" alternatingRowBackgroundColors="YES" columnReordering="NO" columnResizing="NO" autosaveColumns="NO" rowHeight="62" rowSizeStyle="automatic" viewBased="YES" indentationPerLevel="16" indentationMarkerFollowsCell="NO" autoresizesOutlineColumn="YES" outlineTableColumn="3093" id="3091" customClass="TorrentTableView">
<rect key="frame" x="0.0" y="0.0" width="524" height="196"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<size key="intercellSpacing" width="2" height="3"/>
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
<color key="gridColor" white="0.80241936000000003" alpha="1" colorSpace="calibratedWhite"/>
<tableColumns>
<tableColumn identifier="Color" editable="NO" width="29" minWidth="16" maxWidth="3000" id="3093">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="left" title="Color">
<tableColumn identifier="Torrent" editable="NO" width="492" minWidth="16" maxWidth="3000" id="3093">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="left" title="Torrent">
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" white="0.33333299" alpha="1" colorSpace="calibratedWhite"/>
</tableHeaderCell>
<imageCell key="dataCell" controlSize="small" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="imageCell:3146:image" id="3146">
<imageCell key="dataCell" controlSize="small" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="imageCell:Uue-OE-Cnc:image" id="Uue-OE-Cnc">
<font key="font" metaFont="system"/>
</imageCell>
</tableColumn>
<tableColumn identifier="Group" editable="NO" width="284" minWidth="48" maxWidth="3.4028229999999999e+38" id="3140">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="left" title="Group">
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="headerColor" catalog="System" colorSpace="catalog"/>
</tableHeaderCell>
<textFieldCell key="dataCell" lineBreakMode="truncatingTail" selectable="YES" sendsActionOnEndEditing="YES" alignment="left" id="3145">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES"/>
</tableColumn>
<tableColumn identifier="DL Image" editable="NO" width="10" minWidth="10" maxWidth="10" id="3124">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="left">
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="headerColor" catalog="System" colorSpace="catalog"/>
</tableHeaderCell>
<imageCell key="dataCell" controlSize="small" refusesFirstResponder="YES" alignment="left" imageAlignment="right" imageScaling="proportionallyDown" image="imageCell:3146:image" id="3138">
<font key="font" metaFont="system"/>
</imageCell>
</tableColumn>
<tableColumn identifier="DL" editable="NO" width="70" minWidth="70" maxWidth="70" id="3126">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="left" title="DL Speed">
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="headerColor" catalog="System" colorSpace="catalog"/>
</tableHeaderCell>
<textFieldCell key="dataCell" lineBreakMode="truncatingTail" selectable="YES" alignment="left" id="3127">
<font key="font" metaFont="label"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</tableColumn>
<tableColumn identifier="UL Image" editable="NO" width="10" minWidth="10" maxWidth="10" id="3133">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="left">
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="headerColor" catalog="System" colorSpace="catalog"/>
</tableHeaderCell>
<imageCell key="dataCell" controlSize="small" refusesFirstResponder="YES" alignment="left" imageAlignment="right" imageScaling="proportionallyDown" image="imageCell:3146:image" id="3139">
<font key="font" metaFont="system"/>
</imageCell>
</tableColumn>
<tableColumn identifier="UL" editable="NO" width="70" minWidth="70" maxWidth="70" id="3135">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="left" title="UL Speed">
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="headerColor" catalog="System" colorSpace="catalog"/>
</tableHeaderCell>
<textFieldCell key="dataCell" lineBreakMode="truncatingTail" selectable="YES" alignment="left" id="3136">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<prototypeCellViews>
<tableCellView identifier="GroupCell" id="0FS-PW-dzb" customClass="GroupCell">
<rect key="frame" x="11" y="1" width="502" height="18"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="Gwg-pn-lM5">
<rect key="frame" x="11" y="2" width="14" height="14"/>
<constraints>
<constraint firstAttribute="height" constant="14" id="Jy3-b5-hDM"/>
<constraint firstAttribute="width" constant="14" id="njz-e9-z2g"/>
</constraints>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyUpOrDown" image="NSStatusAvailable" id="9EY-oY-PNG"/>
</imageView>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="ubd-A0-0oW">
<rect key="frame" x="337" y="1" width="16" height="16"/>
<constraints>
<constraint firstAttribute="height" constant="16" id="B9U-SP-PBV"/>
<constraint firstAttribute="width" constant="16" id="FmI-yQ-CfP"/>
</constraints>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="DownArrowTemplate" id="AQv-3A-dH5"/>
</imageView>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="hYZ-OW-ebc">
<rect key="frame" x="421" y="1" width="16" height="16"/>
<constraints>
<constraint firstAttribute="width" constant="16" id="CGc-C8-J6A"/>
<constraint firstAttribute="height" constant="16" id="SqQ-wV-N6u"/>
</constraints>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="UpArrowTemplate" id="M11-HA-ONH"/>
</imageView>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="JAO-DI-vbm">
<rect key="frame" x="351" y="2" width="64" height="14"/>
<constraints>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="60" id="MR1-WF-Gcs"/>
</constraints>
<textFieldCell key="cell" lineBreakMode="clipping" title="0.0KB/s" id="x8g-Nv-jcD">
<font key="font" metaFont="smallSystemBold"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="kLh-dT-fWl">
<rect key="frame" x="435" y="2" width="64" height="14"/>
<constraints>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="60" id="FLW-3y-3Ft"/>
</constraints>
<textFieldCell key="cell" lineBreakMode="clipping" title="0.0KB/s" id="HmT-FI-amY">
<font key="font" metaFont="smallSystemBold"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" allowsExpansionToolTips="YES" translatesAutoresizingMaskIntoConstraints="NO" id="XfM-wG-Zxc">
<rect key="frame" x="28" y="2" width="311" height="14"/>
<textFieldCell key="cell" lineBreakMode="truncatingMiddle" sendsActionOnEndEditing="YES" title="Group Title" id="zIM-6D-jsa">
<font key="font" metaFont="smallSystemBold"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<constraints>
<constraint firstItem="hYZ-OW-ebc" firstAttribute="leading" secondItem="JAO-DI-vbm" secondAttribute="trailing" constant="8" symbolic="YES" id="3ZJ-sm-LLQ"/>
<constraint firstItem="XfM-wG-Zxc" firstAttribute="leading" secondItem="Gwg-pn-lM5" secondAttribute="trailing" constant="5" id="4bb-lb-TYP"/>
<constraint firstItem="JAO-DI-vbm" firstAttribute="leading" secondItem="ubd-A0-0oW" secondAttribute="trailing" id="8gL-pL-QKz"/>
<constraint firstAttribute="trailing" secondItem="kLh-dT-fWl" secondAttribute="trailing" constant="5" id="D4t-PI-NVX"/>
<constraint firstItem="ubd-A0-0oW" firstAttribute="leading" secondItem="XfM-wG-Zxc" secondAttribute="trailing" id="E4I-Nb-vB5"/>
<constraint firstItem="ubd-A0-0oW" firstAttribute="centerY" secondItem="Gwg-pn-lM5" secondAttribute="centerY" id="GZB-R5-O8z"/>
<constraint firstItem="hYZ-OW-ebc" firstAttribute="centerY" secondItem="Gwg-pn-lM5" secondAttribute="centerY" id="NWK-Hj-44W"/>
<constraint firstItem="Gwg-pn-lM5" firstAttribute="centerY" secondItem="0FS-PW-dzb" secondAttribute="centerY" id="Yau-WM-aqf"/>
<constraint firstItem="XfM-wG-Zxc" firstAttribute="centerY" secondItem="Gwg-pn-lM5" secondAttribute="centerY" id="pB3-eZ-C1W"/>
<constraint firstItem="Gwg-pn-lM5" firstAttribute="leading" secondItem="0FS-PW-dzb" secondAttribute="leading" constant="11" id="qsg-vi-UIQ"/>
<constraint firstItem="JAO-DI-vbm" firstAttribute="centerY" secondItem="Gwg-pn-lM5" secondAttribute="centerY" id="rWB-xX-7V9"/>
<constraint firstItem="kLh-dT-fWl" firstAttribute="leading" secondItem="hYZ-OW-ebc" secondAttribute="trailing" id="t7S-2X-ur7"/>
<constraint firstItem="kLh-dT-fWl" firstAttribute="centerY" secondItem="Gwg-pn-lM5" secondAttribute="centerY" id="uLM-dC-jCY"/>
</constraints>
<connections>
<outlet property="fGroupDownloadField" destination="JAO-DI-vbm" id="jls-TR-sdX"/>
<outlet property="fGroupDownloadView" destination="ubd-A0-0oW" id="l2t-pP-sMi"/>
<outlet property="fGroupIndicatorView" destination="Gwg-pn-lM5" id="FCY-PW-4YQ"/>
<outlet property="fGroupTitleField" destination="XfM-wG-Zxc" id="o72-mm-63H"/>
<outlet property="fGroupUploadAndRatioField" destination="kLh-dT-fWl" id="31q-Z9-mDX"/>
<outlet property="fGroupUploadAndRatioView" destination="hYZ-OW-ebc" id="92U-rh-k8B"/>
</connections>
</tableCellView>
<tableCellView identifier="TorrentCell" id="7Ic-8y-f3v" customClass="TorrentCell">
<rect key="frame" x="11" y="22" width="502" height="62"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="FLl-ue-i20">
<rect key="frame" x="2" y="26" width="10" height="10"/>
<constraints>
<constraint firstAttribute="height" constant="10" id="NhU-Lf-nCs"/>
<constraint firstAttribute="width" constant="10" id="zar-fl-hEG"/>
</constraints>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="NSStatusAvailable" id="Oyo-d7-APt"/>
</imageView>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="WTV-L6-lc2">
<rect key="frame" x="15" y="13" width="36" height="36"/>
<constraints>
<constraint firstAttribute="height" constant="36" id="Qka-wO-mv5"/>
<constraint firstAttribute="width" constant="36" id="tDk-n4-lHb"/>
</constraints>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="NSFolder" id="WGQ-kl-WZm"/>
</imageView>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="gIX-h2-fIl">
<rect key="frame" x="70" y="44" width="377" height="15"/>
<textFieldCell key="cell" lineBreakMode="truncatingMiddle" title="Title" id="y9O-gd-sXA">
<font key="font" metaFont="cellTitle"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="7sy-Kk-M9Y">
<rect key="frame" x="70" y="30" width="377" height="13"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" title="Progress" id="oSz-TE-8jj">
<font key="font" metaFont="system" size="10"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="J4v-0p-u4L">
<rect key="frame" x="70" y="1" width="377" height="13"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" title="Status" id="lmp-Y3-ZQO">
<font key="font" metaFont="system" size="10"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<button horizontalHuggingPriority="1000" verticalHuggingPriority="750" horizontalCompressionResistancePriority="1000" translatesAutoresizingMaskIntoConstraints="NO" id="kNl-5i-USx" customClass="TorrentCellControlButton">
<rect key="frame" x="466" y="15" width="14" height="14"/>
<buttonCell key="cell" type="square" bezelStyle="shadowlessSquare" image="ResumeOff" imagePosition="only" alignment="center" lineBreakMode="truncatingTail" state="on" imageScaling="proportionallyDown" inset="2" id="oxr-Hn-H15">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
</button>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Rxb-qW-x6p" customClass="TorrentCellActionButton">
<rect key="frame" x="25" y="23" width="16" height="16"/>
<constraints>
<constraint firstAttribute="width" constant="16" id="WLP-JK-i9A"/>
<constraint firstAttribute="height" constant="16" id="bMb-uB-fO6"/>
</constraints>
<buttonCell key="cell" type="square" bezelStyle="shadowlessSquare" image="ActionHover" imagePosition="only" alignment="center" lineBreakMode="truncatingTail" state="on" imageScaling="proportionallyDown" inset="2" id="9Xz-qo-fuz">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
</button>
<button horizontalHuggingPriority="1000" verticalHuggingPriority="750" horizontalCompressionResistancePriority="1000" translatesAutoresizingMaskIntoConstraints="NO" id="k5I-MN-hYk" customClass="TorrentCellRevealButton">
<rect key="frame" x="483" y="15" width="14" height="14"/>
<buttonCell key="cell" type="square" bezelStyle="shadowlessSquare" image="RevealOff" imagePosition="only" alignment="center" lineBreakMode="truncatingTail" state="on" imageScaling="proportionallyDown" inset="2" id="WF0-QY-3DO">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
</button>
<customView translatesAutoresizingMaskIntoConstraints="NO" id="Ifq-ub-ay4">
<rect key="frame" x="72" y="15" width="373" height="14"/>
<constraints>
<constraint firstAttribute="height" constant="14" id="D7V-m1-heE"/>
</constraints>
</customView>
</subviews>
<constraints>
<constraint firstItem="7sy-Kk-M9Y" firstAttribute="top" secondItem="gIX-h2-fIl" secondAttribute="bottom" constant="1" id="3gb-Hf-Uub"/>
<constraint firstItem="FLl-ue-i20" firstAttribute="centerY" secondItem="7Ic-8y-f3v" secondAttribute="centerY" id="7iF-OP-ifv"/>
<constraint firstItem="WTV-L6-lc2" firstAttribute="leading" secondItem="FLl-ue-i20" secondAttribute="trailing" constant="3" id="8e4-oE-Kzh"/>
<constraint firstItem="7sy-Kk-M9Y" firstAttribute="leading" secondItem="gIX-h2-fIl" secondAttribute="leading" id="9xL-MW-2y6"/>
<constraint firstItem="J4v-0p-u4L" firstAttribute="top" secondItem="Ifq-ub-ay4" secondAttribute="bottom" constant="1" id="AyP-y5-tmL"/>
<constraint firstItem="Ifq-ub-ay4" firstAttribute="leading" secondItem="gIX-h2-fIl" secondAttribute="leading" id="DcH-RS-hMc"/>
<constraint firstItem="kNl-5i-USx" firstAttribute="centerY" secondItem="Ifq-ub-ay4" secondAttribute="centerY" id="F2k-wy-sR2"/>
<constraint firstItem="WTV-L6-lc2" firstAttribute="centerY" secondItem="FLl-ue-i20" secondAttribute="centerY" id="FXq-kY-5ny"/>
<constraint firstItem="FLl-ue-i20" firstAttribute="leading" secondItem="7Ic-8y-f3v" secondAttribute="leading" constant="2" id="Fu9-kh-dtb"/>
<constraint firstItem="Ifq-ub-ay4" firstAttribute="trailing" secondItem="7sy-Kk-M9Y" secondAttribute="trailing" id="KFR-7d-Sjr"/>
<constraint firstAttribute="trailing" secondItem="k5I-MN-hYk" secondAttribute="trailing" constant="5" id="NRQ-sA-ng6"/>
<constraint firstItem="J4v-0p-u4L" firstAttribute="leading" secondItem="gIX-h2-fIl" secondAttribute="leading" id="S8t-RL-exe"/>
<constraint firstItem="Rxb-qW-x6p" firstAttribute="centerX" secondItem="WTV-L6-lc2" secondAttribute="centerX" id="Uyo-j3-TSH"/>
<constraint firstItem="Ifq-ub-ay4" firstAttribute="top" secondItem="7sy-Kk-M9Y" secondAttribute="bottom" constant="1" id="XRq-cM-zeI"/>
<constraint firstItem="kNl-5i-USx" firstAttribute="leading" secondItem="Ifq-ub-ay4" secondAttribute="trailing" constant="21" id="b80-qd-DxN"/>
<constraint firstItem="gIX-h2-fIl" firstAttribute="leading" secondItem="WTV-L6-lc2" secondAttribute="trailing" constant="21" id="dgd-qt-xxb"/>
<constraint firstItem="k5I-MN-hYk" firstAttribute="leading" secondItem="kNl-5i-USx" secondAttribute="trailing" constant="3" id="fsy-23-flK"/>
<constraint firstItem="J4v-0p-u4L" firstAttribute="trailing" secondItem="Ifq-ub-ay4" secondAttribute="trailing" id="j5C-sr-mIK"/>
<constraint firstItem="gIX-h2-fIl" firstAttribute="top" secondItem="7Ic-8y-f3v" secondAttribute="top" constant="3" id="lhe-Ce-3SF"/>
<constraint firstItem="Ifq-ub-ay4" firstAttribute="trailing" secondItem="gIX-h2-fIl" secondAttribute="trailing" id="t3M-au-Bgz"/>
<constraint firstItem="k5I-MN-hYk" firstAttribute="centerY" secondItem="kNl-5i-USx" secondAttribute="centerY" id="xjv-n4-F1m"/>
<constraint firstItem="Rxb-qW-x6p" firstAttribute="centerY" secondItem="WTV-L6-lc2" secondAttribute="centerY" id="y0a-c5-pYS"/>
</constraints>
<connections>
<outlet property="fActionButton" destination="Rxb-qW-x6p" id="ICn-NC-ahy"/>
<outlet property="fControlButton" destination="kNl-5i-USx" id="iN7-sX-ciA"/>
<outlet property="fGroupIndicatorView" destination="FLl-ue-i20" id="0QU-wF-59e"/>
<outlet property="fIconView" destination="WTV-L6-lc2" id="gmV-h3-Muk"/>
<outlet property="fRevealButton" destination="k5I-MN-hYk" id="Qkq-Yq-UAY"/>
<outlet property="fTorrentProgressBarView" destination="Ifq-ub-ay4" id="rEw-BU-47h"/>
<outlet property="fTorrentProgressField" destination="7sy-Kk-M9Y" id="nk4-R3-1Wv"/>
<outlet property="fTorrentStatusField" destination="J4v-0p-u4L" id="nqa-rT-XUI"/>
<outlet property="fTorrentTitleField" destination="gIX-h2-fIl" id="h3s-C4-XNT"/>
</connections>
</tableCellView>
<tableCellView identifier="SmallTorrentCell" id="ouH-H8-Otv" customClass="SmallTorrentCell">
<rect key="frame" x="11" y="87" width="502" height="22"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="4yg-IA-aOF">
<rect key="frame" x="4" y="8" width="6" height="6"/>
<constraints>
<constraint firstAttribute="height" constant="6" id="Gfx-gR-O2I"/>
<constraint firstAttribute="width" constant="6" id="SCs-lp-qum"/>
</constraints>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="NSStatusAvailable" id="brn-HI-eEh"/>
</imageView>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="WWu-yD-Amw">
<rect key="frame" x="18" y="3" width="16" height="16"/>
<constraints>
<constraint firstAttribute="height" constant="16" id="42D-Ja-dYD"/>
<constraint firstAttribute="width" constant="16" id="y2G-x9-YIV"/>
</constraints>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="NSFolder" id="Kea-PS-H27"/>
</imageView>
<button translatesAutoresizingMaskIntoConstraints="NO" id="9Cg-Nh-Hcr" customClass="TorrentCellActionButton">
<rect key="frame" x="18" y="3" width="16" height="16"/>
<constraints>
<constraint firstAttribute="width" constant="16" id="B01-Zu-gNh"/>
<constraint firstAttribute="height" constant="16" id="dNQ-J3-27z"/>
</constraints>
<buttonCell key="cell" type="square" bezelStyle="shadowlessSquare" image="ActionHover" imagePosition="only" alignment="center" imageScaling="proportionallyUpOrDown" inset="2" id="ejE-Yu-JNS">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
</button>
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="nyW-bO-2kN">
<rect key="frame" x="52" y="4" width="370" height="15"/>
<textFieldCell key="cell" lineBreakMode="truncatingMiddle" title="Title" id="kbX-Av-eP4">
<font key="font" metaFont="cellTitle"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<customView translatesAutoresizingMaskIntoConstraints="NO" id="x9c-7U-Odp">
<rect key="frame" x="53" y="2" width="444" height="18"/>
<constraints>
<constraint firstAttribute="height" constant="18" id="UN3-pW-1Yw"/>
</constraints>
</customView>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="1000" translatesAutoresizingMaskIntoConstraints="NO" id="ExW-pe-aKm">
<rect key="frame" x="423" y="4" width="73" height="14"/>
<textFieldCell key="cell" lineBreakMode="truncatingMiddle" alignment="right" title="Status String" id="Zmj-A9-NPf">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<button translatesAutoresizingMaskIntoConstraints="NO" id="bGq-Kc-jWE" customClass="TorrentCellControlButton">
<rect key="frame" x="463" y="4" width="14" height="14"/>
<constraints>
<constraint firstAttribute="width" constant="14" id="hQW-cC-Lp7"/>
<constraint firstAttribute="height" constant="14" id="jWF-m2-n6g"/>
</constraints>
<buttonCell key="cell" type="square" bezelStyle="shadowlessSquare" image="ResumeOff" imagePosition="only" alignment="center" imageScaling="proportionallyUpOrDown" inset="2" id="DmO-My-6OF">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
</button>
<button translatesAutoresizingMaskIntoConstraints="NO" id="Hyt-Uv-vDm" customClass="TorrentCellRevealButton">
<rect key="frame" x="480" y="4" width="14" height="14"/>
<constraints>
<constraint firstAttribute="height" constant="14" id="CYn-n4-Daq"/>
<constraint firstAttribute="width" constant="14" id="P9R-Cw-RJF"/>
</constraints>
<buttonCell key="cell" type="square" bezelStyle="shadowlessSquare" image="RevealOff" imagePosition="only" alignment="center" imageScaling="proportionallyUpOrDown" inset="2" id="dWx-5q-ABk">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
</button>
</subviews>
<constraints>
<constraint firstItem="Hyt-Uv-vDm" firstAttribute="centerY" secondItem="x9c-7U-Odp" secondAttribute="centerY" id="1WR-Ta-vbl"/>
<constraint firstItem="nyW-bO-2kN" firstAttribute="centerY" secondItem="4yg-IA-aOF" secondAttribute="centerY" id="25K-kd-55i"/>
<constraint firstAttribute="trailing" secondItem="ExW-pe-aKm" secondAttribute="trailing" constant="8" id="5OS-LM-hyI"/>
<constraint firstItem="ExW-pe-aKm" firstAttribute="centerY" secondItem="4yg-IA-aOF" secondAttribute="centerY" id="Lmc-wT-yP4"/>
<constraint firstItem="nyW-bO-2kN" firstAttribute="leading" secondItem="9Cg-Nh-Hcr" secondAttribute="trailing" constant="20" id="N3b-dy-27i"/>
<constraint firstItem="x9c-7U-Odp" firstAttribute="leading" secondItem="9Cg-Nh-Hcr" secondAttribute="trailing" constant="19" id="On5-WE-C7H"/>
<constraint firstItem="bGq-Kc-jWE" firstAttribute="centerY" secondItem="x9c-7U-Odp" secondAttribute="centerY" id="PrG-V3-ry0"/>
<constraint firstItem="4yg-IA-aOF" firstAttribute="leading" secondItem="ouH-H8-Otv" secondAttribute="leading" constant="4" id="Ps8-HO-fGd"/>
<constraint firstItem="9Cg-Nh-Hcr" firstAttribute="centerY" secondItem="4yg-IA-aOF" secondAttribute="centerY" id="Rvd-RT-10P"/>
<constraint firstItem="x9c-7U-Odp" firstAttribute="centerY" secondItem="9Cg-Nh-Hcr" secondAttribute="centerY" id="VaS-hJ-vAV"/>
<constraint firstAttribute="trailing" secondItem="x9c-7U-Odp" secondAttribute="trailing" constant="5" id="daS-0F-yKf"/>
<constraint firstAttribute="trailing" secondItem="Hyt-Uv-vDm" secondAttribute="trailing" constant="8" id="fKb-o5-n1k"/>
<constraint firstItem="WWu-yD-Amw" firstAttribute="centerY" secondItem="9Cg-Nh-Hcr" secondAttribute="centerY" id="hYn-MV-EFh"/>
<constraint firstItem="4yg-IA-aOF" firstAttribute="centerY" secondItem="ouH-H8-Otv" secondAttribute="centerY" id="izm-2a-TOI"/>
<constraint firstItem="Hyt-Uv-vDm" firstAttribute="leading" secondItem="bGq-Kc-jWE" secondAttribute="trailing" constant="3" id="kX1-gB-jyF"/>
<constraint firstItem="9Cg-Nh-Hcr" firstAttribute="leading" secondItem="4yg-IA-aOF" secondAttribute="trailing" constant="8" id="lf2-Bm-TFR"/>
<constraint firstItem="ExW-pe-aKm" firstAttribute="leading" secondItem="nyW-bO-2kN" secondAttribute="trailing" constant="5" id="nNT-cl-cRg"/>
<constraint firstItem="WWu-yD-Amw" firstAttribute="centerX" secondItem="9Cg-Nh-Hcr" secondAttribute="centerX" id="tNz-Ea-fpP"/>
</constraints>
<connections>
<outlet property="fActionButton" destination="9Cg-Nh-Hcr" id="88t-qS-msz"/>
<outlet property="fControlButton" destination="bGq-Kc-jWE" id="Oyb-Iu-Na0"/>
<outlet property="fGroupIndicatorView" destination="4yg-IA-aOF" id="ROF-Ua-PGS"/>
<outlet property="fIconView" destination="WWu-yD-Amw" id="vWy-ch-grE"/>
<outlet property="fRevealButton" destination="Hyt-Uv-vDm" id="PHz-zj-UsH"/>
<outlet property="fTorrentProgressBarView" destination="x9c-7U-Odp" id="WGd-KA-6xD"/>
<outlet property="fTorrentStatusField" destination="ExW-pe-aKm" id="rQQ-ip-8MV"/>
<outlet property="fTorrentTitleField" destination="nyW-bO-2kN" id="Xsd-Ro-ren"/>
</connections>
</tableCellView>
</prototypeCellViews>
</tableColumn>
</tableColumns>
<connections>
@ -126,10 +378,10 @@
</scroller>
</scrollView>
<customView verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="JCm-xb-Dpw">
<rect key="frame" x="0.0" y="0.0" width="515" height="24"/>
<rect key="frame" x="0.0" y="0.0" width="524" height="24"/>
<subviews>
<textField verticalHuggingPriority="750" allowsCharacterPickerTouchBarItem="YES" translatesAutoresizingMaskIntoConstraints="NO" id="2700">
<rect key="frame" x="199" y="5" width="118" height="14"/>
<rect key="frame" x="258" y="5" width="8" height="14"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingTail" allowsUndo="NO" sendsActionOnEndEditing="YES" alignment="center" usesSingleLineMode="YES" id="3049">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
@ -1017,13 +1269,20 @@ CA
</menu>
</objects>
<resources>
<image name="ActionHover" width="16" height="16"/>
<image name="CleanupTemplate" width="18" height="18"/>
<image name="DownArrowTemplate" width="8" height="12"/>
<image name="EllipsisTemplate" width="18" height="18"/>
<image name="NSFolder" width="32" height="32"/>
<image name="NSStatusAvailable" width="16" height="16"/>
<image name="PriorityHighTemplate" width="12" height="12"/>
<image name="PriorityLowTemplate" width="12" height="12"/>
<image name="PriorityNormalTemplate" width="12" height="12"/>
<image name="ResumeOff" width="14" height="14"/>
<image name="RevealOff" width="14" height="14"/>
<image name="TortoiseTemplate" width="18" height="18"/>
<image name="imageCell:3146:image" width="62" height="62">
<image name="UpArrowTemplate" width="8" height="12"/>
<image name="imageCell:Uue-OE-Cnc:image" width="62" height="62">
<mutableData key="keyedArchiveRepresentation">
YnBsaXN0MDDUAQIDBAUGBwpYJHZlcnNpb25ZJGFyY2hpdmVyVCR0b3BYJG9iamVjdHMSAAGGoF8QD05T
S2V5ZWRBcmNoaXZlctEICVRyb290gAGuCwwZGh8UJCkqMTQ3PUBVJG51bGzWDQ4PEBESExQVFhcYVk5T

View File

@ -90,14 +90,14 @@ target_sources(${TR_NAME}-mac
FilterButton.mm
GlobalOptionsPopoverViewController.h
GlobalOptionsPopoverViewController.mm
GroupCell.h
GroupCell.mm
GroupPopUpButtonCell.h
GroupPopUpButtonCell.mm
GroupsController.h
GroupsController.mm
GroupsPrefsController.h
GroupsPrefsController.mm
GroupTextCell.h
GroupTextCell.mm
GroupToolbarItem.h
GroupToolbarItem.mm
InfoActivityViewController.h
@ -148,12 +148,16 @@ target_sources(${TR_NAME}-mac
PrefsWindow.mm
PriorityPopUpButtonCell.h
PriorityPopUpButtonCell.mm
ProgressBarView.h
ProgressBarView.mm
ProgressGradients.h
ProgressGradients.mm
ShareToolbarItem.h
ShareToolbarItem.mm
ShareTorrentFileHelper.h
ShareTorrentFileHelper.mm
SmallTorrentCell.h
SmallTorrentCell.mm
SparkleProxy.mm
StatsWindowController.h
StatsWindowController.mm
@ -167,6 +171,12 @@ target_sources(${TR_NAME}-mac
Torrent.mm
TorrentCell.h
TorrentCell.mm
TorrentCellActionButton.h
TorrentCellActionButton.mm
TorrentCellControlButton.h
TorrentCellControlButton.mm
TorrentCellRevealButton.h
TorrentCellRevealButton.mm
TorrentGroup.h
TorrentGroup.mm
TorrentTableView.h

View File

@ -625,11 +625,10 @@ void onTorrentCompletenessChanged(tr_torrent* tor, tr_completeness status, bool
//set table size
BOOL const small = [self.fDefaults boolForKey:@"SmallView"];
if (small)
{
self.fTableView.rowHeight = kRowHeightSmall;
}
self.fTableView.usesAlternatingRowBackgroundColors = !small;
self.fTableView.rowHeight = small ? kRowHeightSmall : kRowHeightRegular;
self.fTableView.usesAutomaticRowHeights = NO;
self.fTableView.floatsGroupRows = YES;
//self.fTableView.usesAlternatingRowBackgroundColors = !small;
[self.fWindow setContentBorderThickness:NSMinY(self.fTableView.enclosingScrollView.frame) forEdge:NSMinYEdge];
self.fWindow.movableByWindowBackground = YES;
@ -2317,6 +2316,8 @@ void onTorrentCompletenessChanged(tr_torrent* tor, tr_completeness status, bool
{
[self.fInfoController updateInfoStats];
}
[self.fTableView reloadVisibleRows];
}
//badge dock
@ -3239,10 +3240,6 @@ void onTorrentCompletenessChanged(tr_torrent* tor, tr_completeness status, bool
//set all groups as expanded
[self.fTableView removeAllCollapsedGroups];
//since we're not doing this the right way (boo buggy animation), we need to remember selected values
#warning when Lion-only and using views instead of cells, this likely won't be needed
NSArray* selectedValues = self.fTableView.selectedValues;
beganUpdates = YES;
[self.fTableView beginUpdates];
@ -3286,11 +3283,6 @@ void onTorrentCompletenessChanged(tr_torrent* tor, tr_completeness status, bool
for (TorrentGroup* group in self.fDisplayedTorrents)
[self.fTableView expandItem:group];
}
if (selectedValues)
{
[self.fTableView selectValues:selectedValues];
}
}
//sort the torrents (won't sort the groups, though)
@ -3599,66 +3591,6 @@ void onTorrentCompletenessChanged(tr_torrent* tor, tr_completeness status, bool
return ![item isKindOfClass:[Torrent class]];
}
- (id)outlineView:(NSOutlineView*)outlineView objectValueForTableColumn:(NSTableColumn*)tableColumn byItem:(id)item
{
if ([item isKindOfClass:[Torrent class]])
{
if (tableColumn)
{
return nil;
}
return ((Torrent*)item).hashString;
}
else
{
NSString* ident = tableColumn.identifier;
TorrentGroup* group = (TorrentGroup*)item;
if ([ident isEqualToString:@"Group"])
{
NSInteger groupIndex = group.groupIndex;
return groupIndex != -1 ? [GroupsController.groups nameForIndex:groupIndex] : NSLocalizedString(@"No Group", "Group table row");
}
else if ([ident isEqualToString:@"Color"])
{
NSInteger groupIndex = group.groupIndex;
return [GroupsController.groups imageForIndex:groupIndex];
}
else if ([ident isEqualToString:@"DL Image"])
{
NSImage* image = [NSImage imageNamed:@"DownArrowGroupTemplate"];
image.accessibilityDescription = NSLocalizedString(@"DL", "Torrent -> status image");
return image;
}
else if ([ident isEqualToString:@"UL Image"])
{
if ([self.fDefaults boolForKey:@"DisplayGroupRowRatio"])
{
NSImage* image = [NSImage imageNamed:@"YingYangGroupTemplate"];
image.accessibilityDescription = NSLocalizedString(@"Ratio", "Torrent -> status image");
return image;
}
else
{
NSImage* image = [NSImage imageNamed:@"UpArrowGroupTemplate"];
image.accessibilityDescription = NSLocalizedString(@"UL", "Torrent -> status image");
return image;
}
}
else
{
if ([self.fDefaults boolForKey:@"DisplayGroupRowRatio"])
{
return [NSString stringForRatio:group.ratio];
}
else
{
CGFloat rate = [ident isEqualToString:@"UL"] ? group.uploadRate : group.downloadRate;
return [NSString stringForSpeed:rate];
}
}
}
}
- (BOOL)outlineView:(NSOutlineView*)outlineView writeItems:(NSArray*)items toPasteboard:(NSPasteboard*)pasteboard
{
//only allow reordering of rows if sorting by order
@ -3980,7 +3912,7 @@ void onTorrentCompletenessChanged(tr_torrent* tor, tr_completeness status, bool
BOOL makeSmall = ![self.fDefaults boolForKey:@"SmallView"];
[self.fDefaults setBool:makeSmall forKey:@"SmallView"];
self.fTableView.usesAlternatingRowBackgroundColors = !makeSmall;
//self.fTableView.usesAlternatingRowBackgroundColors = !makeSmall;
self.fTableView.rowHeight = makeSmall ? kRowHeightSmall : kRowHeightRegular;
@ -5319,6 +5251,12 @@ void onTorrentCompletenessChanged(tr_torrent* tor, tr_completeness status, bool
height = (kGroupSeparatorHeight + self.fTableView.intercellSpacing.height) * groups +
(self.fTableView.rowHeight + self.fTableView.intercellSpacing.height) * (self.fTableView.numberOfRows - groups);
//account for group padding...
if (groups > 1)
{
height += (groups - 1) * 20;
}
}
else
{

18
macosx/GroupCell.h Normal file
View File

@ -0,0 +1,18 @@
// This file Copyright © 2006-2023 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>
#import "TorrentTableView.h"
@interface GroupCell : NSTableCellView
@property(nonatomic) IBOutlet NSImageView* fGroupIndicatorView;
@property(nonatomic) IBOutlet NSTextField* fGroupTitleField;
@property(nonatomic) IBOutlet NSImageView* fGroupDownloadView;
@property(nonatomic) IBOutlet NSImageView* fGroupUploadAndRatioView;
@property(nonatomic) IBOutlet NSTextField* fGroupDownloadField;
@property(nonatomic) IBOutlet NSTextField* fGroupUploadAndRatioField;
@end

9
macosx/GroupCell.mm Normal file
View File

@ -0,0 +1,9 @@
// This file Copyright © 2006-2023 Transmission authors and contributors.
// It may be used under the MIT (SPDX: MIT) license.
// License text can be found in the licenses/ folder.
#import "GroupCell.h"
@implementation GroupCell
@end

View File

@ -1,11 +0,0 @@
// This file Copyright © 2022-2023 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 GroupTextCell : NSTextFieldCell
@property(nonatomic) BOOL selected;
@end

View File

@ -1,35 +0,0 @@
// This file Copyright © 2022-2023 Transmission authors and contributors.
// It may be used under the MIT (SPDX: MIT) license.
// License text can be found in the licenses/ folder.
#import "GroupTextCell.h"
#import "TorrentGroup.h"
#import "TorrentTableView.h"
@implementation GroupTextCell
//vertically align text
- (NSRect)titleRectForBounds:(NSRect)theRect
{
NSRect titleFrame = [super titleRectForBounds:theRect];
NSSize titleSize = [[self attributedStringValue] size];
titleFrame.origin.y = NSMidY(theRect) - (CGFloat)1.0 - titleSize.height * (CGFloat)0.5;
titleFrame.origin.x = theRect.origin.x;
return titleFrame;
}
- (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView*)controlView
{
//set font size and color
NSRect titleRect = [self titleRectForBounds:cellFrame];
NSMutableAttributedString* string = [[self attributedStringValue] mutableCopy];
NSDictionary* attributes = @{
NSFontAttributeName : [NSFont boldSystemFontOfSize:11.0],
NSForegroundColorAttributeName : self.selected ? [NSColor labelColor] : [NSColor secondaryLabelColor]
};
[string addAttributes:attributes range:NSMakeRange(0, string.length)];
[string drawInRect:titleRect];
}
@end

14
macosx/ProgressBarView.h Normal file
View File

@ -0,0 +1,14 @@
// This file Copyright © 2006-2023 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 TorrentTableView;
@class Torrent;
@interface ProgressBarView : NSView
- (void)drawBarInRect:(NSRect)barRect forTableView:(TorrentTableView*)tableView withTorrent:(Torrent*)torrent;
@end

203
macosx/ProgressBarView.mm Normal file
View File

@ -0,0 +1,203 @@
// This file Copyright © 2006-2023 Transmission authors and contributors.
// It may be used under the MIT (SPDX: MIT) license.
// License text can be found in the licenses/ folder.
#import "ProgressBarView.h"
#import "ProgressGradients.h"
#import "TorrentTableView.h"
#import "Torrent.h"
static CGFloat const kPiecesTotalPercent = 0.6;
static NSInteger const kMaxPieces = 18 * 18;
@interface ProgressBarView ()
@property(nonatomic, readonly) NSUserDefaults* fDefaults;
@property(nonatomic, readonly) NSColor* fBarBorderColor;
@property(nonatomic, readonly) NSColor* fBluePieceColor;
@property(nonatomic, readonly) NSColor* fBarMinimalBorderColor;
@end
@implementation ProgressBarView
- (instancetype)init
{
if ((self = [super init]))
{
_fDefaults = NSUserDefaults.standardUserDefaults;
_fBluePieceColor = [NSColor colorWithCalibratedRed:0.0 green:0.4 blue:0.8 alpha:1.0];
_fBarBorderColor = [NSColor colorWithCalibratedWhite:0.0 alpha:0.2];
_fBarMinimalBorderColor = [NSColor colorWithCalibratedWhite:0.0 alpha:0.015];
}
return self;
}
- (void)drawBarInRect:(NSRect)barRect forTableView:(TorrentTableView*)tableView withTorrent:(Torrent*)torrent
{
BOOL const minimal = [self.fDefaults boolForKey:@"SmallView"];
CGFloat const piecesBarPercent = tableView.piecesBarPercent;
if (piecesBarPercent > 0.0)
{
NSRect piecesBarRect, regularBarRect;
NSDivideRect(barRect, &piecesBarRect, &regularBarRect, floor(NSHeight(barRect) * kPiecesTotalPercent * piecesBarPercent), NSMaxYEdge);
[self drawRegularBar:regularBarRect forTorrent:torrent];
[self drawPiecesBar:piecesBarRect forTorrent:torrent];
}
else
{
torrent.previousFinishedPieces = nil;
[self drawRegularBar:barRect forTorrent:torrent];
}
NSColor* borderColor = minimal ? self.fBarMinimalBorderColor : self.fBarBorderColor;
[borderColor set];
[NSBezierPath strokeRect:NSInsetRect(barRect, 0.5, 0.5)];
}
- (void)drawRegularBar:(NSRect)barRect forTorrent:(Torrent*)torrent
{
NSRect haveRect, missingRect;
NSDivideRect(barRect, &haveRect, &missingRect, round(torrent.progress * NSWidth(barRect)), NSMinXEdge);
if (!NSIsEmptyRect(haveRect))
{
if (torrent.active)
{
if (torrent.checking)
{
[ProgressGradients.progressYellowGradient drawInRect:haveRect angle:90];
}
else if (torrent.seeding)
{
NSRect ratioHaveRect, ratioRemainingRect;
NSDivideRect(haveRect, &ratioHaveRect, &ratioRemainingRect, round(torrent.progressStopRatio * NSWidth(haveRect)), NSMinXEdge);
[ProgressGradients.progressGreenGradient drawInRect:ratioHaveRect angle:90];
[ProgressGradients.progressLightGreenGradient drawInRect:ratioRemainingRect angle:90];
}
else
{
[ProgressGradients.progressBlueGradient drawInRect:haveRect angle:90];
}
}
else
{
if (torrent.waitingToStart)
{
if (torrent.allDownloaded)
{
[ProgressGradients.progressDarkGreenGradient drawInRect:haveRect angle:90];
}
else
{
[ProgressGradients.progressDarkBlueGradient drawInRect:haveRect angle:90];
}
}
else
{
[ProgressGradients.progressGrayGradient drawInRect:haveRect angle:90];
}
}
}
if (!torrent.allDownloaded)
{
CGFloat const widthRemaining = round(NSWidth(barRect) * torrent.progressLeft);
NSRect wantedRect;
NSDivideRect(missingRect, &wantedRect, &missingRect, widthRemaining, NSMinXEdge);
//not-available section
if (torrent.active && !torrent.checking && torrent.availableDesired < 1.0 && [self.fDefaults boolForKey:@"DisplayProgressBarAvailable"])
{
NSRect unavailableRect;
NSDivideRect(wantedRect, &wantedRect, &unavailableRect, round(NSWidth(wantedRect) * torrent.availableDesired), NSMinXEdge);
[ProgressGradients.progressRedGradient drawInRect:unavailableRect angle:90];
}
//remaining section
[ProgressGradients.progressWhiteGradient drawInRect:wantedRect angle:90];
}
//unwanted section
if (!NSIsEmptyRect(missingRect))
{
if (!torrent.magnet)
{
[ProgressGradients.progressLightGrayGradient drawInRect:missingRect angle:90];
}
else
{
[ProgressGradients.progressRedGradient drawInRect:missingRect angle:90];
}
}
}
- (void)drawPiecesBar:(NSRect)barRect forTorrent:(Torrent*)torrent
{
//fill an all-white bar for magnet links
if (torrent.magnet)
{
[[NSColor colorWithCalibratedWhite:1.0 alpha:[self.fDefaults boolForKey:@"SmallView"] ? 0.25 : 1.0] set];
NSRectFillUsingOperation(barRect, NSCompositingOperationSourceOver);
return;
}
NSInteger pieceCount = MIN(torrent.pieceCount, kMaxPieces);
float* piecesPercent = static_cast<float*>(malloc(pieceCount * sizeof(float)));
[torrent getAmountFinished:piecesPercent size:pieceCount];
NSBitmapImageRep* bitmap = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:nil pixelsWide:pieceCount pixelsHigh:1
bitsPerSample:8
samplesPerPixel:4
hasAlpha:YES
isPlanar:NO
colorSpaceName:NSCalibratedRGBColorSpace
bytesPerRow:0
bitsPerPixel:0];
NSIndexSet* previousFinishedIndexes = torrent.previousFinishedPieces;
NSMutableIndexSet* finishedIndexes = [NSMutableIndexSet indexSet];
for (NSInteger i = 0; i < pieceCount; i++)
{
NSColor* pieceColor;
if (piecesPercent[i] == 1.0f)
{
if (previousFinishedIndexes && ![previousFinishedIndexes containsIndex:i])
{
pieceColor = NSColor.orangeColor;
}
else
{
pieceColor = self.fBluePieceColor;
}
[finishedIndexes addIndex:i];
}
else
{
pieceColor = [NSColor.whiteColor blendedColorWithFraction:piecesPercent[i] ofColor:self.fBluePieceColor];
}
//it's faster to just set color instead of checking previous color
[bitmap setColor:pieceColor atX:i y:0];
}
free(piecesPercent);
torrent.previousFinishedPieces = finishedIndexes.count > 0 ? finishedIndexes : nil; //don't bother saving if none are complete
//actually draw image
[bitmap drawInRect:barRect fromRect:NSZeroRect operation:NSCompositingOperationSourceOver
fraction:([self.fDefaults boolForKey:@"SmallView"] ? 0.25 : 1.0)respectFlipped:YES
hints:nil];
}
@end

25
macosx/SmallTorrentCell.h Normal file
View File

@ -0,0 +1,25 @@
// This file Copyright © 2006-2023 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 TorrentTableView;
@interface SmallTorrentCell : NSTableCellView
@property(nonatomic) IBOutlet NSButton* fActionButton;
@property(nonatomic) IBOutlet NSButton* fControlButton;
@property(nonatomic) IBOutlet NSButton* fRevealButton;
@property(nonatomic) IBOutlet NSImageView* fIconView;
@property(nonatomic) IBOutlet NSImageView* fGroupIndicatorView;
@property(nonatomic) IBOutlet NSTextField* fTorrentTitleField;
@property(nonatomic) IBOutlet NSTextField* fTorrentStatusField;
@property(nonatomic) IBOutlet NSView* fTorrentProgressBarView;
@property(nonatomic) TorrentTableView* fTorrentTableView;
@end

View File

@ -0,0 +1,98 @@
// This file Copyright © 2006-2023 Transmission authors and contributors.
// It may be used under the MIT (SPDX: MIT) license.
// License text can be found in the licenses/ folder.
#import "SmallTorrentCell.h"
#import "ProgressBarView.h"
#import "ProgressGradients.h"
#import "TorrentTableView.h"
#import "Torrent.h"
@interface SmallTorrentCell ()
@property(nonatomic) NSTrackingArea* fTrackingArea;
@end
@implementation SmallTorrentCell
//draw progress bar
- (void)drawRect:(NSRect)dirtyRect
{
if (self.fTorrentTableView)
{
NSRect barRect = self.fTorrentProgressBarView.frame;
ProgressBarView* progressBar = [[ProgressBarView alloc] init];
Torrent* torrent = (Torrent*)self.objectValue;
[progressBar drawBarInRect:barRect forTableView:self.fTorrentTableView withTorrent:torrent];
}
[super drawRect:dirtyRect];
}
//otherwise progress bar is inverted
- (BOOL)isFlipped
{
return YES;
}
//show fControlButton and fRevealButton
- (void)mouseEntered:(NSEvent*)event
{
[super mouseEntered:event];
NSPoint mouseLocation = [self convertPoint:[event locationInWindow] fromView:nil];
if (NSPointInRect(mouseLocation, self.fTrackingArea.rect))
{
[self.fTorrentTableView hoverEventBeganForView:self];
}
}
- (void)mouseExited:(NSEvent*)event
{
[super mouseExited:event];
NSPoint mouseLocation = [self convertPoint:[event locationInWindow] fromView:nil];
if (!NSPointInRect(mouseLocation, self.fTrackingArea.rect))
{
[self.fTorrentTableView hoverEventEndedForView:self];
}
}
- (void)mouseUp:(NSEvent*)event
{
[super mouseUp:event];
[self updateTrackingAreas];
}
- (void)updateTrackingAreas
{
if (self.fTrackingArea != nil)
{
[self removeTrackingArea:self.fTrackingArea];
}
//tracking rect should not be entire row, but start at fGroupDownloadView
NSRect titleRect = self.fTorrentTitleField.frame;
CGFloat maxX = NSMaxX(titleRect);
NSRect rect = self.bounds;
rect.origin.x = maxX;
NSTrackingAreaOptions opts = (NSTrackingMouseEnteredAndExited | NSTrackingActiveInKeyWindow);
self.fTrackingArea = [[NSTrackingArea alloc] initWithRect:rect options:opts owner:self userInfo:nil];
[self addTrackingArea:self.fTrackingArea];
//check to see if mouse is already within rect
NSPoint mouseLocation = [self.window mouseLocationOutsideOfEventStream];
mouseLocation = [self.superview convertPoint:mouseLocation fromView:nil];
if (NSPointInRect(mouseLocation, rect))
{
[self mouseEntered:[[NSEvent alloc] init]];
}
else
{
[self mouseExited:[[NSEvent alloc] init]];
}
}
@end

View File

@ -3,20 +3,23 @@
// License text can be found in the licenses/ folder.
#import <AppKit/AppKit.h>
#import "TorrentTableView.h"
@interface TorrentCell : NSActionCell
@interface TorrentCell : NSTableCellView
@property(nonatomic) BOOL hover;
@property(nonatomic) BOOL hoverControl;
@property(nonatomic) BOOL hoverReveal;
@property(nonatomic) BOOL hoverAction;
@property(nonatomic) IBOutlet NSButton* fActionButton;
@property(nonatomic) IBOutlet NSButton* fControlButton;
@property(nonatomic) IBOutlet NSButton* fRevealButton;
- (NSRect)iconRectForBounds:(NSRect)bounds;
- (NSRect)actionRectForBounds:(NSRect)bounds;
@property(nonatomic) IBOutlet NSImageView* fIconView;
@property(nonatomic) IBOutlet NSImageView* fGroupIndicatorView;
- (void)addTrackingAreasForView:(NSView*)controlView
inRect:(NSRect)cellFrame
withUserInfo:(NSDictionary*)userInfo
mouseLocation:(NSPoint)mouseLocation;
@property(nonatomic) IBOutlet NSTextField* fTorrentTitleField;
@property(nonatomic) IBOutlet NSTextField* fTorrentProgressField;
@property(nonatomic) IBOutlet NSTextField* fTorrentStatusField;
@property(nonatomic) IBOutlet NSView* fTorrentProgressBarView;
@property(nonatomic) TorrentTableView* fTorrentTableView;
@end

View File

@ -3,919 +3,31 @@
// License text can be found in the licenses/ folder.
#import "TorrentCell.h"
#import "FileNameCell.h"
#import "GroupsController.h"
#import "NSImageAdditions.h"
#import "NSStringAdditions.h"
#import "ProgressBarView.h"
#import "ProgressGradients.h"
#import "Torrent.h"
#import "TorrentTableView.h"
static CGFloat const kBarHeight = 12.0;
static CGFloat const kImageSizeRegular = 32.0;
static CGFloat const kImageSizeMin = 16.0;
static CGFloat const kErrorImageSize = 20.0;
static CGFloat const kGroupImageSizeRegular = 10.0;
static CGFloat const kGroupImageSizeMin = 6.0;
static CGFloat const kGroupPaddingRegular = 22.0;
static CGFloat const kGroupPaddingMin = 14.0;
static CGFloat const kNormalButtonWidth = 14.0;
static CGFloat const kActionButtonWidth = 16.0;
static CGFloat const kPriorityIconSize = 12.0;
//ends up being larger than font height
static CGFloat const kHeightTitle = 16.0;
static CGFloat const kHeightStatus = 12.0;
static CGFloat const kPaddingHorizontal = 5.0;
static CGFloat const kPaddingEdgeMax = 12.0;
static CGFloat const kPaddingBetweenButtons = 3.0;
static CGFloat const kPaddingBetweenImageAndTitle = kPaddingHorizontal + 1.0;
static CGFloat const kPaddingBetweenImageAndBar = kPaddingHorizontal;
static CGFloat const kPaddingBetweenTitleAndPriority = 6.0;
static CGFloat const kPaddingAboveTitle = 4.0;
static CGFloat const kPaddingBetweenTitleAndMinStatus = 3.0;
static CGFloat const kPaddingBetweenTitleAndProgress = 1.0;
static CGFloat const kPaddingBetweenProgressAndBar = 2.0;
static CGFloat const kPaddingBetweenBarAndStatus = 2.0;
static CGFloat const kPaddingBetweenBarAndEdgeMin = 3.0;
static CGFloat const kPaddingExpansionFrame = 2.0;
static CGFloat const kPiecesTotalPercent = 0.6;
static NSInteger const kMaxPieces = 18 * 18;
static NSMutableParagraphStyle* sParagraphStyle()
{
NSMutableParagraphStyle* paragraphStyle = [NSParagraphStyle.defaultParagraphStyle mutableCopy];
paragraphStyle.lineBreakMode = NSLineBreakByTruncatingMiddle;
return paragraphStyle;
}
static NSDictionary<NSAttributedStringKey, id>* const kTitleAttributes = @{
NSFontAttributeName : [NSFont messageFontOfSize:12.0],
NSParagraphStyleAttributeName : sParagraphStyle(),
NSForegroundColorAttributeName : NSColor.labelColor
};
static NSDictionary<NSAttributedStringKey, id>* const kStatusAttributes = @{
NSFontAttributeName : [NSFont messageFontOfSize:10.0],
NSParagraphStyleAttributeName : sParagraphStyle(),
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:10.0],
NSParagraphStyleAttributeName : sParagraphStyle(),
NSForegroundColorAttributeName : NSColor.whiteColor
};
@interface TorrentCell ()
@property(nonatomic) BOOL fTracking;
@property(nonatomic) BOOL fMouseDownControlButton;
@property(nonatomic) BOOL fMouseDownRevealButton;
@property(nonatomic, readonly) NSColor* fBarBorderColor;
@property(nonatomic, readonly) NSColor* fBluePieceColor;
@property(nonatomic, readonly) NSColor* fBarMinimalBorderColor;
@end
@implementation TorrentCell
//only called once and the main table is always needed, so don't worry about releasing
- (instancetype)init
//draw progress bar
- (void)drawRect:(NSRect)dirtyRect
{
if ((self = [super init]))
if (self.fTorrentTableView)
{
_fBluePieceColor = [NSColor colorWithCalibratedRed:0.0 green:0.4 blue:0.8 alpha:1.0];
_fBarBorderColor = [NSColor colorWithCalibratedWhite:0.0 alpha:0.2];
_fBarMinimalBorderColor = [NSColor colorWithCalibratedWhite:0.0 alpha:0.015];
}
return self;
}
NSRect barRect = self.fTorrentProgressBarView.frame;
ProgressBarView* progressBar = [[ProgressBarView alloc] init];
Torrent* torrent = (Torrent*)self.objectValue;
- (id)copyWithZone:(NSZone*)zone
{
TorrentCell* copy = [super copyWithZone:zone];
copy->_fBluePieceColor = _fBluePieceColor;
copy->_fBarBorderColor = _fBarBorderColor;
copy->_fBarMinimalBorderColor = _fBarMinimalBorderColor;
[copy setRepresentedObject:self.representedObject];
return copy;
}
- (NSUserDefaults*)fDefaults
{
return NSUserDefaults.standardUserDefaults;
}
- (NSRect)iconRectForBounds:(NSRect)bounds
{
BOOL const minimal = [self.fDefaults boolForKey:@"SmallView"];
CGFloat const imageSize = minimal ? kImageSizeMin : kImageSizeRegular;
CGFloat const padding = minimal ? kGroupPaddingMin : kGroupPaddingRegular;
return NSMakeRect(NSMinX(bounds) + (padding * 0.5) + kPaddingHorizontal, ceil(NSMidY(bounds) - imageSize * 0.5), imageSize, imageSize);
}
- (NSRect)actionRectForBounds:(NSRect)bounds
{
NSRect iconRect = [self iconRectForBounds:bounds];
NSRect actionRect = [self actionButtonRectForBounds:iconRect];
return actionRect;
}
- (NSCellHitResult)hitTestForEvent:(NSEvent*)event inRect:(NSRect)cellFrame ofView:(NSView*)controlView
{
NSPoint point = [controlView convertPoint:event.locationInWindow fromView:nil];
if (NSMouseInRect(point, [self controlButtonRectForBounds:cellFrame], controlView.flipped) ||
NSMouseInRect(point, [self revealButtonRectForBounds:cellFrame], controlView.flipped))
{
return NSCellHitContentArea | NSCellHitTrackableArea;
[progressBar drawBarInRect:barRect forTableView:self.fTorrentTableView withTorrent:torrent];
}
return NSCellHitContentArea;
[super drawRect:dirtyRect];
}
+ (BOOL)prefersTrackingUntilMouseUp
//otherwise progress bar is inverted
- (BOOL)isFlipped
{
return YES;
}
- (BOOL)trackMouse:(NSEvent*)event inRect:(NSRect)cellFrame ofView:(NSView*)controlView untilMouseUp:(BOOL)flag
{
self.fTracking = YES;
self.controlView = controlView;
NSPoint point = [controlView convertPoint:event.locationInWindow fromView:nil];
NSRect const controlRect = [self controlButtonRectForBounds:cellFrame];
BOOL const checkControl = NSMouseInRect(point, controlRect, controlView.flipped);
NSRect const revealRect = [self revealButtonRectForBounds:cellFrame];
BOOL const checkReveal = NSMouseInRect(point, revealRect, controlView.flipped);
[(TorrentTableView*)controlView removeTrackingAreas];
while (event.type != NSEventTypeLeftMouseUp)
{
point = [controlView convertPoint:event.locationInWindow fromView:nil];
if (checkControl)
{
BOOL const inControlButton = NSMouseInRect(point, controlRect, controlView.flipped);
if (self.fMouseDownControlButton != inControlButton)
{
self.fMouseDownControlButton = inControlButton;
[controlView setNeedsDisplayInRect:cellFrame];
}
}
else if (checkReveal)
{
BOOL const inRevealButton = NSMouseInRect(point, revealRect, controlView.flipped);
if (self.fMouseDownRevealButton != inRevealButton)
{
self.fMouseDownRevealButton = inRevealButton;
[controlView setNeedsDisplayInRect:cellFrame];
}
}
//send events to where necessary
if (event.type == NSEventTypeMouseEntered || event.type == NSEventTypeMouseExited)
{
[NSApp sendEvent:event];
}
event = [controlView.window nextEventMatchingMask:(NSEventMaskLeftMouseUp | NSEventMaskLeftMouseDragged |
NSEventMaskMouseEntered | NSEventMaskMouseExited)];
}
self.fTracking = NO;
if (self.fMouseDownControlButton)
{
self.fMouseDownControlButton = NO;
[(TorrentTableView*)controlView toggleControlForTorrent:self.representedObject];
}
else if (self.fMouseDownRevealButton)
{
self.fMouseDownRevealButton = NO;
[controlView setNeedsDisplayInRect:cellFrame];
NSString* location = ((Torrent*)self.representedObject).dataLocation;
if (location)
{
NSURL* file = [NSURL fileURLWithPath:location];
[NSWorkspace.sharedWorkspace activateFileViewerSelectingURLs:@[ file ]];
}
}
[controlView updateTrackingAreas];
return YES;
}
- (void)addTrackingAreasForView:(NSView*)controlView
inRect:(NSRect)cellFrame
withUserInfo:(NSDictionary*)userInfo
mouseLocation:(NSPoint)mouseLocation
{
NSTrackingAreaOptions const options = NSTrackingEnabledDuringMouseDrag | NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways;
//whole row
if ([self.fDefaults boolForKey:@"SmallView"])
{
NSTrackingAreaOptions rowOptions = options;
if (NSMouseInRect(mouseLocation, cellFrame, controlView.flipped))
{
rowOptions |= NSTrackingAssumeInside;
((TorrentTableView*)controlView).hoverRow = [userInfo[@"Row"] integerValue];
}
NSMutableDictionary* rowInfo = [userInfo mutableCopy];
rowInfo[@"Type"] = @"Row";
NSTrackingArea* area = [[NSTrackingArea alloc] initWithRect:cellFrame options:rowOptions owner:controlView userInfo:rowInfo];
[controlView addTrackingArea:area];
}
//control button
NSRect controlButtonRect = [self controlButtonRectForBounds:cellFrame];
NSTrackingAreaOptions controlOptions = options;
if (NSMouseInRect(mouseLocation, controlButtonRect, controlView.flipped))
{
controlOptions |= NSTrackingAssumeInside;
((TorrentTableView*)controlView).controlButtonHoverRow = [userInfo[@"Row"] integerValue];
}
NSMutableDictionary* controlInfo = [userInfo mutableCopy];
controlInfo[@"Type"] = @"Control";
NSTrackingArea* area = [[NSTrackingArea alloc] initWithRect:controlButtonRect options:controlOptions owner:controlView
userInfo:controlInfo];
[controlView addTrackingArea:area];
//reveal button
NSRect revealButtonRect = [self revealButtonRectForBounds:cellFrame];
NSTrackingAreaOptions revealOptions = options;
if (NSMouseInRect(mouseLocation, revealButtonRect, controlView.flipped))
{
revealOptions |= NSTrackingAssumeInside;
((TorrentTableView*)controlView).revealButtonHoverRow = [userInfo[@"Row"] integerValue];
}
NSMutableDictionary* revealInfo = [userInfo mutableCopy];
revealInfo[@"Type"] = @"Reveal";
area = [[NSTrackingArea alloc] initWithRect:revealButtonRect options:revealOptions owner:controlView userInfo:revealInfo];
[controlView addTrackingArea:area];
//action button
NSRect actionButtonRect = [self actionRectForBounds:cellFrame];
NSTrackingAreaOptions actionOptions = options;
if (NSMouseInRect(mouseLocation, actionButtonRect, controlView.flipped))
{
actionOptions |= NSTrackingAssumeInside;
((TorrentTableView*)controlView).actionButtonHoverRow = [userInfo[@"Row"] integerValue];
}
NSMutableDictionary* actionInfo = [userInfo mutableCopy];
actionInfo[@"Type"] = @"Action";
area = [[NSTrackingArea alloc] initWithRect:actionButtonRect options:actionOptions owner:controlView userInfo:actionInfo];
[controlView addTrackingArea:area];
}
- (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView*)controlView
{
Torrent* torrent = self.representedObject;
NSAssert(torrent != nil, @"can't have a TorrentCell without a Torrent");
BOOL const minimal = [self.fDefaults boolForKey:@"SmallView"];
//bar
[self drawBar:minimal ? [self barRectMinForBounds:cellFrame] : [self barRectRegForBounds:cellFrame]];
//group coloring
NSRect const parentRect = [self iconRectForBounds:cellFrame];
NSRect iconRect = NSMakeRect(parentRect.origin.x, parentRect.origin.y, parentRect.size.width, parentRect.size.height);
NSInteger const groupValue = torrent.groupValue;
if (groupValue != -1)
{
NSRect groupRect = [self groupIconRectForBounds:iconRect];
NSColor* groupColor = [GroupsController.groups colorForIndex:groupValue];
NSImage* icon = [NSImage discIconWithColor:groupColor insetFactor:0];
[icon drawInRect:groupRect fromRect:NSZeroRect operation:NSCompositingOperationSourceOver fraction:1.0f];
}
BOOL const error = torrent.anyErrorOrWarning;
//icon
if (!minimal || !(!self.fTracking && self.hoverAction)) //don't show in minimal mode when hovered over
{
NSImage* icon = (minimal && error) ? [NSImage imageNamed:NSImageNameCaution] : torrent.icon;
[icon drawInRect:iconRect fromRect:NSZeroRect operation:NSCompositingOperationSourceOver fraction:1.0 respectFlipped:YES
hints:nil];
}
//error badge
if (error && !minimal)
{
NSImage* errorImage = [NSImage imageNamed:NSImageNameCaution];
NSRect const errorRect = NSMakeRect(NSMaxX(iconRect) - kErrorImageSize, NSMaxY(iconRect) - kErrorImageSize, kErrorImageSize, kErrorImageSize);
[errorImage drawInRect:errorRect fromRect:NSZeroRect operation:NSCompositingOperationSourceOver fraction:1.0
respectFlipped:YES
hints:nil];
}
AttributesStyle style = self.backgroundStyle == NSBackgroundStyleEmphasized ? AttributesStyleEmphasized : AttributesStyleNormal;
CGFloat titleRightBound;
//minimal status
if (minimal)
{
NSAttributedString* minimalString = [self attributedStatusString:self.minimalStatusString style:style];
NSRect minimalStatusRect = [self rectForMinimalStatusWithStringSize:[minimalString size] inBounds:cellFrame];
if (!self.hover)
{
[minimalString drawInRect:minimalStatusRect];
}
titleRightBound = NSMinX(minimalStatusRect);
}
//progress
else
{
NSAttributedString* progressString = [self attributedStatusString:torrent.progressString style:style];
NSRect progressRect = [self rectForProgressWithStringInBounds:cellFrame];
[progressString drawInRect:progressRect];
titleRightBound = NSMaxX(cellFrame);
}
if (!minimal || self.hover)
{
//control button
NSString* controlImageSuffix;
if (self.fMouseDownControlButton)
{
controlImageSuffix = @"On";
}
else if (!self.fTracking && self.hoverControl)
{
controlImageSuffix = @"Hover";
}
else
{
controlImageSuffix = @"Off";
}
NSImage* controlImage;
if (torrent.active)
{
controlImage = [NSImage imageNamed:[@"Pause" stringByAppendingString:controlImageSuffix]];
}
else
{
if (NSApp.currentEvent.modifierFlags & NSEventModifierFlagOption)
{
controlImage = [NSImage imageNamed:[@"ResumeNoWait" stringByAppendingString:controlImageSuffix]];
}
else if (torrent.waitingToStart)
{
controlImage = [NSImage imageNamed:[@"Pause" stringByAppendingString:controlImageSuffix]];
}
else
{
controlImage = [NSImage imageNamed:[@"Resume" stringByAppendingString:controlImageSuffix]];
}
}
NSRect const controlRect = [self controlButtonRectForBounds:cellFrame];
[controlImage drawInRect:controlRect fromRect:NSZeroRect operation:NSCompositingOperationSourceOver fraction:1.0
respectFlipped:YES
hints:nil];
if (minimal)
{
titleRightBound = MIN(titleRightBound, NSMinX(controlRect));
}
//reveal button
NSString* revealImageString;
if (self.fMouseDownRevealButton)
{
revealImageString = @"RevealOn";
}
else if (!self.fTracking && self.hoverReveal)
{
revealImageString = @"RevealHover";
}
else
{
revealImageString = @"RevealOff";
}
NSImage* revealImage = [NSImage imageNamed:revealImageString];
[revealImage drawInRect:[self revealButtonRectForBounds:cellFrame] fromRect:NSZeroRect
operation:NSCompositingOperationSourceOver
fraction:1.0
respectFlipped:YES
hints:nil];
//action button
#warning image should use new gear
if (!self.fTracking && self.hoverAction)
{
NSImage* actionImage = [NSImage imageNamed:@"ActionHover"];
[actionImage drawInRect:[self actionButtonRectForBounds:iconRect] fromRect:NSZeroRect
operation:NSCompositingOperationSourceOver
fraction:1.0
respectFlipped:YES
hints:nil];
}
}
//title
NSAttributedString* titleString = [self attributedTitleWithStyle:style];
NSRect titleRect = [self rectForTitleWithStringSize:[titleString size] withRightBound:titleRightBound inBounds:cellFrame
minimal:minimal];
[titleString drawInRect:titleRect];
//priority icon
if (torrent.priority != TR_PRI_NORMAL)
{
NSRect const priorityRect = NSMakeRect(
NSMaxX(titleRect) + kPaddingBetweenTitleAndPriority,
NSMidY(titleRect) - kPriorityIconSize * 0.5,
kPriorityIconSize,
kPriorityIconSize);
NSColor* priorityColor = self.backgroundStyle == NSBackgroundStyleEmphasized ? NSColor.whiteColor : NSColor.labelColor;
NSImage* priorityImage = [[NSImage imageNamed:(torrent.priority == TR_PRI_HIGH ? @"PriorityHighTemplate" : @"PriorityLowTemplate")]
imageWithColor:priorityColor];
[priorityImage drawInRect:priorityRect fromRect:NSZeroRect operation:NSCompositingOperationSourceOver fraction:1.0
respectFlipped:YES
hints:nil];
}
//status
if (!minimal)
{
NSAttributedString* statusString = [self attributedStatusString:self.statusString style:style];
[statusString drawInRect:[self rectForStatusWithStringInBounds:cellFrame]];
}
}
- (NSRect)expansionFrameWithFrame:(NSRect)cellFrame inView:(NSView*)view
{
BOOL minimal = [self.fDefaults boolForKey:@"SmallView"];
//this code needs to match the code in drawInteriorWithFrame:withView:
CGFloat titleRightBound;
if (minimal)
{
NSAttributedString* minimalString = [self attributedStatusString:self.minimalStatusString style:AttributesStyleNormal];
NSRect minimalStatusRect = [self rectForMinimalStatusWithStringSize:[minimalString size] inBounds:cellFrame];
titleRightBound = NSMinX(minimalStatusRect);
if (self.hover)
{
NSRect const controlRect = [self controlButtonRectForBounds:cellFrame];
titleRightBound = MIN(titleRightBound, NSMinX(controlRect));
}
}
else
{
titleRightBound = NSMaxX(cellFrame);
}
NSAttributedString* titleString = [self attributedTitleWithStyle:AttributesStyleNormal];
NSSize titleStringSize = [titleString size];
NSRect realRect = [self rectForTitleWithStringSize:titleStringSize withRightBound:titleRightBound inBounds:cellFrame
minimal:minimal];
NSAssert(titleStringSize.width >= NSWidth(realRect), @"Full rect width should not be less than the used title rect width!");
if (titleStringSize.width > NSWidth(realRect) &&
NSMouseInRect([view convertPoint:view.window.mouseLocationOutsideOfEventStream fromView:nil], realRect, view.flipped))
{
realRect.size.width = titleStringSize.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
- (void)drawBar:(NSRect)barRect
{
BOOL const minimal = [self.fDefaults boolForKey:@"SmallView"];
CGFloat const piecesBarPercent = ((TorrentTableView*)self.controlView).piecesBarPercent;
if (piecesBarPercent > 0.0)
{
NSRect piecesBarRect, regularBarRect;
NSDivideRect(barRect, &piecesBarRect, &regularBarRect, floor(NSHeight(barRect) * kPiecesTotalPercent * piecesBarPercent), NSMaxYEdge);
[self drawRegularBar:regularBarRect];
[self drawPiecesBar:piecesBarRect];
}
else
{
((Torrent*)self.representedObject).previousFinishedPieces = nil;
[self drawRegularBar:barRect];
}
NSColor* borderColor = minimal ? self.fBarMinimalBorderColor : self.fBarBorderColor;
[borderColor set];
[NSBezierPath strokeRect:NSInsetRect(barRect, 0.5, 0.5)];
}
- (void)drawRegularBar:(NSRect)barRect
{
Torrent* torrent = self.representedObject;
NSRect haveRect, missingRect;
NSDivideRect(barRect, &haveRect, &missingRect, round(torrent.progress * NSWidth(barRect)), NSMinXEdge);
if (!NSIsEmptyRect(haveRect))
{
if (torrent.active)
{
if (torrent.checking)
{
[ProgressGradients.progressYellowGradient drawInRect:haveRect angle:90];
}
else if (torrent.seeding)
{
NSRect ratioHaveRect, ratioRemainingRect;
NSDivideRect(haveRect, &ratioHaveRect, &ratioRemainingRect, round(torrent.progressStopRatio * NSWidth(haveRect)), NSMinXEdge);
[ProgressGradients.progressGreenGradient drawInRect:ratioHaveRect angle:90];
[ProgressGradients.progressLightGreenGradient drawInRect:ratioRemainingRect angle:90];
}
else
{
[ProgressGradients.progressBlueGradient drawInRect:haveRect angle:90];
}
}
else
{
if (torrent.waitingToStart)
{
if (torrent.allDownloaded)
{
[ProgressGradients.progressDarkGreenGradient drawInRect:haveRect angle:90];
}
else
{
[ProgressGradients.progressDarkBlueGradient drawInRect:haveRect angle:90];
}
}
else
{
[ProgressGradients.progressGrayGradient drawInRect:haveRect angle:90];
}
}
}
if (!torrent.allDownloaded)
{
CGFloat const widthRemaining = round(NSWidth(barRect) * torrent.progressLeft);
NSRect wantedRect;
NSDivideRect(missingRect, &wantedRect, &missingRect, widthRemaining, NSMinXEdge);
//not-available section
if (torrent.active && !torrent.checking && torrent.availableDesired < 1.0 && [self.fDefaults boolForKey:@"DisplayProgressBarAvailable"])
{
NSRect unavailableRect;
NSDivideRect(wantedRect, &wantedRect, &unavailableRect, round(NSWidth(wantedRect) * torrent.availableDesired), NSMinXEdge);
[ProgressGradients.progressRedGradient drawInRect:unavailableRect angle:90];
}
//remaining section
[ProgressGradients.progressWhiteGradient drawInRect:wantedRect angle:90];
}
//unwanted section
if (!NSIsEmptyRect(missingRect))
{
if (!torrent.magnet)
{
[ProgressGradients.progressLightGrayGradient drawInRect:missingRect angle:90];
}
else
{
[ProgressGradients.progressRedGradient drawInRect:missingRect angle:90];
}
}
}
- (void)drawPiecesBar:(NSRect)barRect
{
Torrent* torrent = self.representedObject;
//fill an all-white bar for magnet links
if (torrent.magnet)
{
[[NSColor colorWithCalibratedWhite:1.0 alpha:[self.fDefaults boolForKey:@"SmallView"] ? 0.25 : 1.0] set];
NSRectFillUsingOperation(barRect, NSCompositingOperationSourceOver);
return;
}
NSInteger pieceCount = MIN(torrent.pieceCount, kMaxPieces);
float* piecesPercent = static_cast<float*>(malloc(pieceCount * sizeof(float)));
[torrent getAmountFinished:piecesPercent size:pieceCount];
NSBitmapImageRep* bitmap = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:nil pixelsWide:pieceCount pixelsHigh:1
bitsPerSample:8
samplesPerPixel:4
hasAlpha:YES
isPlanar:NO
colorSpaceName:NSCalibratedRGBColorSpace
bytesPerRow:0
bitsPerPixel:0];
NSIndexSet* previousFinishedIndexes = torrent.previousFinishedPieces;
NSMutableIndexSet* finishedIndexes = [NSMutableIndexSet indexSet];
for (NSInteger i = 0; i < pieceCount; i++)
{
NSColor* pieceColor;
if (piecesPercent[i] == 1.0f)
{
if (previousFinishedIndexes && ![previousFinishedIndexes containsIndex:i])
{
pieceColor = NSColor.orangeColor;
}
else
{
pieceColor = self.fBluePieceColor;
}
[finishedIndexes addIndex:i];
}
else
{
pieceColor = [NSColor.whiteColor blendedColorWithFraction:piecesPercent[i] ofColor:self.fBluePieceColor];
}
//it's faster to just set color instead of checking previous color
// faster and non-broken alternative to `[bitmap setColor:pieceColor atX:i y:0]`
unsigned char* data = bitmap.bitmapData + (i << 2);
data[0] = pieceColor.redComponent * 255;
data[1] = pieceColor.greenComponent * 255;
data[2] = pieceColor.blueComponent * 255;
data[3] = pieceColor.alphaComponent * 255;
}
free(piecesPercent);
torrent.previousFinishedPieces = finishedIndexes.count > 0 ? finishedIndexes : nil; //don't bother saving if none are complete
//actually draw image
[bitmap drawInRect:barRect fromRect:NSZeroRect operation:NSCompositingOperationSourceOver
fraction:[self.fDefaults boolForKey:@"SmallView"] ? 0.25 : 1.0
respectFlipped:YES
hints:nil];
}
- (NSRect)rectForMinimalStatusWithStringSize:(NSSize)stringSize inBounds:(NSRect)bounds
{
NSRect result;
result.size = stringSize;
result.origin.x = NSMaxX(bounds) - (kPaddingHorizontal + NSWidth(result) + kPaddingEdgeMax);
result.origin.y = ceil(NSMidY(bounds) - NSHeight(result) * 0.5);
return result;
}
- (NSRect)rectForTitleWithStringSize:(NSSize)stringSize
withRightBound:(CGFloat)rightBound
inBounds:(NSRect)bounds
minimal:(BOOL)minimal
{
NSRect result;
result.origin.x = NSMinX(bounds) + kPaddingHorizontal + (minimal ? kImageSizeMin : kImageSizeRegular) + kPaddingBetweenImageAndTitle;
result.size.height = kHeightTitle;
if (minimal)
{
result.origin.x += kGroupPaddingMin;
result.origin.y = ceil(NSMidY(bounds) - NSHeight(result) * 0.5);
result.size.width = rightBound - NSMinX(result) - kPaddingBetweenTitleAndMinStatus;
}
else
{
result.origin.x += kGroupPaddingRegular;
result.origin.y = NSMinY(bounds) + kPaddingAboveTitle;
result.size.width = rightBound - NSMinX(result) - kPaddingHorizontal - kPaddingEdgeMax;
}
if (((Torrent*)self.representedObject).priority != TR_PRI_NORMAL)
{
result.size.width -= kPriorityIconSize + kPaddingBetweenTitleAndPriority;
}
result.size.width = MIN(NSWidth(result), stringSize.width);
return result;
}
- (NSRect)rectForProgressWithStringInBounds:(NSRect)bounds
{
NSRect result;
result.origin.y = NSMinY(bounds) + kPaddingAboveTitle + kHeightTitle + kPaddingBetweenTitleAndProgress;
result.origin.x = NSMinX(bounds) + kPaddingHorizontal + kGroupPaddingRegular + kImageSizeRegular + kPaddingBetweenImageAndTitle;
result.size.height = kHeightStatus;
result.size.width = NSMaxX(bounds) - NSMinX(result) - kPaddingHorizontal;
return result;
}
- (NSRect)rectForStatusWithStringInBounds:(NSRect)bounds
{
NSRect result;
result.origin.y = NSMinY(bounds) + kPaddingAboveTitle + kHeightTitle + kPaddingBetweenTitleAndProgress + kHeightStatus +
kPaddingBetweenProgressAndBar + kBarHeight + kPaddingBetweenBarAndStatus;
result.origin.x = NSMinX(bounds) + kPaddingHorizontal + kGroupPaddingRegular + kImageSizeRegular + kPaddingBetweenImageAndTitle;
result.size.height = kHeightStatus;
result.size.width = NSMaxX(bounds) - NSMinX(result) - kPaddingHorizontal;
return result;
}
- (NSRect)barRectRegForBounds:(NSRect)bounds
{
NSRect result;
result.size.height = kBarHeight;
result.origin.x = NSMinX(bounds) + kPaddingHorizontal + kGroupPaddingRegular + kImageSizeRegular + kPaddingBetweenImageAndBar;
result.origin.y = NSMinY(bounds) + kPaddingAboveTitle + kHeightTitle + kPaddingBetweenTitleAndProgress + kHeightStatus +
kPaddingBetweenProgressAndBar;
result.size.width = floor(
NSMaxX(bounds) - NSMinX(result) - kPaddingHorizontal - 2.0 * (kPaddingBetweenButtons + kNormalButtonWidth + kPaddingEdgeMax));
return result;
}
- (NSRect)barRectMinForBounds:(NSRect)bounds
{
NSRect result;
result.origin.x = NSMinX(bounds) + kPaddingHorizontal + kImageSizeMin + kGroupPaddingMin + kPaddingBetweenImageAndBar;
result.origin.y = NSMinY(bounds) + kPaddingBetweenBarAndEdgeMin;
result.size.height = NSHeight(bounds) - 2.0 * kPaddingBetweenBarAndEdgeMin;
result.size.width = NSMaxX(bounds) - NSMinX(result) - kPaddingBetweenBarAndEdgeMin - kPaddingEdgeMax;
return result;
}
- (NSRect)controlButtonRectForBounds:(NSRect)bounds
{
NSRect result;
result.size.height = kNormalButtonWidth;
result.size.width = kNormalButtonWidth;
result.origin.x = NSMaxX(bounds) - (kPaddingHorizontal + kNormalButtonWidth + kPaddingBetweenButtons + kNormalButtonWidth + kPaddingEdgeMax);
if (![self.fDefaults boolForKey:@"SmallView"])
{
result.origin.y = NSMinY(bounds) + kPaddingAboveTitle + kHeightTitle - (kNormalButtonWidth - kBarHeight) * 0.5 +
kPaddingBetweenTitleAndProgress + kHeightStatus + kPaddingBetweenProgressAndBar;
}
else
{
result.origin.y = ceil(NSMidY(bounds) - NSHeight(result) * 0.5);
}
return result;
}
- (NSRect)revealButtonRectForBounds:(NSRect)bounds
{
NSRect result;
result.size.height = kNormalButtonWidth;
result.size.width = kNormalButtonWidth;
result.origin.x = NSMaxX(bounds) - (kPaddingHorizontal + kNormalButtonWidth + kPaddingEdgeMax);
if (![self.fDefaults boolForKey:@"SmallView"])
{
result.origin.y = NSMinY(bounds) + kPaddingAboveTitle + kHeightTitle - (kNormalButtonWidth - kBarHeight) * 0.5 +
kPaddingBetweenTitleAndProgress + kHeightStatus + kPaddingBetweenProgressAndBar;
}
else
{
result.origin.y = ceil(NSMidY(bounds) - NSHeight(result) * 0.5);
}
return result;
}
- (NSRect)actionButtonRectForBounds:(NSRect)bounds
{
return NSMakeRect(NSMidX(bounds) - kActionButtonWidth * 0.5, NSMidY(bounds) - kActionButtonWidth * 0.5, kActionButtonWidth, kActionButtonWidth);
}
- (NSRect)groupIconRectForBounds:(NSRect)bounds
{
BOOL const minimal = [self.fDefaults boolForKey:@"SmallView"];
CGFloat const imageSize = minimal ? kGroupImageSizeMin : kGroupImageSizeRegular;
CGFloat const padding = minimal ? kGroupPaddingMin + 2 : kGroupPaddingRegular + 1.5;
return NSMakeRect(NSMinX(bounds) - padding * 0.5, NSMidY(bounds) - imageSize * 0.5, imageSize, imageSize);
}
- (NSAttributedString*)attributedTitleWithStyle:(AttributesStyle)style
{
NSString* title = ((Torrent*)self.representedObject).name;
return [[NSAttributedString alloc] initWithString:title
attributes:style == AttributesStyleEmphasized ? kTitleEmphasizedAttributes : kTitleAttributes];
}
- (NSAttributedString*)attributedStatusString:(NSString*)string style:(AttributesStyle)style
{
return [[NSAttributedString alloc] initWithString:string
attributes:style == AttributesStyleEmphasized ? kStatusEmphasizedAttributes : kStatusAttributes];
}
- (NSString*)buttonString
{
if (self.fMouseDownRevealButton || (!self.fTracking && self.hoverReveal))
{
return NSLocalizedString(@"Show the data file in Finder", "Torrent cell -> button info");
}
else if (self.fMouseDownControlButton || (!self.fTracking && self.hoverControl))
{
Torrent* torrent = self.representedObject;
if (torrent.active)
return NSLocalizedString(@"Pause the transfer", "Torrent Table -> tooltip");
else
{
if (NSApp.currentEvent.modifierFlags & NSEventModifierFlagOption)
{
return NSLocalizedString(@"Resume the transfer right away", "Torrent cell -> button info");
}
else if (torrent.waitingToStart)
{
return NSLocalizedString(@"Stop waiting to start", "Torrent cell -> button info");
}
else
{
return NSLocalizedString(@"Resume the transfer", "Torrent cell -> button info");
}
}
}
else if (!self.fTracking && self.hoverAction)
{
return NSLocalizedString(@"Change transfer settings", "Torrent Table -> tooltip");
}
else
{
return nil;
}
}
- (NSString*)statusString
{
NSString* buttonString;
if ((buttonString = self.buttonString))
{
return buttonString;
}
else
{
return ((Torrent*)self.representedObject).statusString;
}
}
- (NSString*)minimalStatusString
{
Torrent* torrent = self.representedObject;
return [self.fDefaults boolForKey:@"DisplaySmallStatusRegular"] ? torrent.shortStatusString : torrent.remainingTimeString;
}
@end

View File

@ -0,0 +1,8 @@
// This file Copyright © 2006-2023 Transmission authors and contributors.
// It may be used under the MIT (SPDX: MIT) license.
// License text can be found in the licenses/ folder.
#import <Cocoa/Cocoa.h>
@interface TorrentCellActionButton : NSButton
@end

View File

@ -0,0 +1,87 @@
// This file Copyright © 2006-2023 Transmission authors and contributors.
// It may be used under the MIT (SPDX: MIT) license.
// License text can be found in the licenses/ folder.
#import "TorrentCellActionButton.h"
#import "TorrentTableView.h"
#import "Torrent.h"
@interface TorrentCellActionButton ()
@property(nonatomic) NSTrackingArea* fTrackingArea;
@property(nonatomic) NSImage* fImage;
@property(nonatomic) NSImage* fAlternativeImage;
@property(nonatomic) TorrentTableView* torrentTableView;
@property(nonatomic) NSUserDefaults* fDefaults;
@end
@implementation TorrentCellActionButton
- (void)awakeFromNib
{
self.fDefaults = NSUserDefaults.standardUserDefaults;
self.fImage = self.image;
// hide image by default and show only on hover
self.fAlternativeImage = [[NSImage alloc] init];
self.image = self.fAlternativeImage;
// disable button click highlighting
[self.cell setHighlightsBy:NSNoCellMask];
}
- (void)setupTorrentTableView
{
if (!self.torrentTableView)
{
self.torrentTableView = (TorrentTableView*)[[[self superview] superview] superview];
}
}
- (void)mouseEntered:(NSEvent*)event
{
[super mouseEntered:event];
self.image = self.fImage;
[self setupTorrentTableView];
[self.torrentTableView hoverEventBeganForView:self];
}
- (void)mouseExited:(NSEvent*)event
{
[super mouseExited:event];
self.image = self.fAlternativeImage;
[self setupTorrentTableView];
[self.torrentTableView hoverEventEndedForView:self];
}
- (void)mouseDown:(NSEvent*)event
{
//when filterbar is shown, we need to remove focus otherwise action fails
[self.window makeFirstResponder:self.torrentTableView];
[super mouseDown:event];
BOOL minimal = [self.fDefaults boolForKey:@"SmallView"];
if (!minimal)
{
[self setupTorrentTableView];
[self.torrentTableView hoverEventEndedForView:self];
}
}
- (void)updateTrackingAreas
{
if (self.fTrackingArea != nil)
{
[self removeTrackingArea:self.fTrackingArea];
}
NSTrackingAreaOptions opts = (NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways);
self.fTrackingArea = [[NSTrackingArea alloc] initWithRect:self.bounds options:opts owner:self userInfo:nil];
[self addTrackingArea:self.fTrackingArea];
}
@end

View File

@ -0,0 +1,11 @@
// This file Copyright © 2006-2023 Transmission authors and contributors.
// It may be used under the MIT (SPDX: MIT) license.
// License text can be found in the licenses/ folder.
#import <Cocoa/Cocoa.h>
@interface TorrentCellControlButton : NSButton
- (void)resetImage;
@end

View File

@ -0,0 +1,107 @@
// This file Copyright © 2006-2023 Transmission authors and contributors.
// It may be used under the MIT (SPDX: MIT) license.
// License text can be found in the licenses/ folder.
#import "TorrentCellControlButton.h"
#import "TorrentTableView.h"
#import "Torrent.h"
@interface TorrentCellControlButton ()
@property(nonatomic) NSTrackingArea* fTrackingArea;
@property(nonatomic, copy) NSString* controlImageSuffix;
@property(nonatomic) TorrentTableView* torrentTableView;
@end
@implementation TorrentCellControlButton
- (void)awakeFromNib
{
self.controlImageSuffix = @"Off";
[self updateImage];
}
- (void)resetImage
{
self.controlImageSuffix = @"Off";
[self updateImage];
}
- (void)setupTorrentTableView
{
if (!self.torrentTableView)
{
self.torrentTableView = (TorrentTableView*)[[[self superview] superview] superview];
}
}
- (void)mouseEntered:(NSEvent*)event
{
[super mouseEntered:event];
self.controlImageSuffix = @"Hover";
[self updateImage];
[self.torrentTableView hoverEventBeganForView:self];
}
- (void)mouseExited:(NSEvent*)event
{
[super mouseExited:event];
self.controlImageSuffix = @"Off";
[self updateImage];
[self.torrentTableView hoverEventEndedForView:self];
}
- (void)mouseDown:(NSEvent*)event
{
//when filterbar is shown, we need to remove focus otherwise action fails
[self.window makeFirstResponder:self.torrentTableView];
[super mouseDown:event];
self.controlImageSuffix = @"On";
[self updateImage];
[self.torrentTableView hoverEventEndedForView:self];
}
- (void)updateImage
{
[self setupTorrentTableView];
NSImage* controlImage;
Torrent* torrent = [self.torrentTableView itemAtRow:[self.torrentTableView rowForView:self]];
if (torrent.active)
{
controlImage = [NSImage imageNamed:[@"Pause" stringByAppendingString:self.controlImageSuffix]];
}
else
{
if (NSApp.currentEvent.modifierFlags & NSEventModifierFlagOption)
{
controlImage = [NSImage imageNamed:[@"ResumeNoWait" stringByAppendingString:self.controlImageSuffix]];
}
else if (torrent.waitingToStart)
{
controlImage = [NSImage imageNamed:[@"Pause" stringByAppendingString:self.controlImageSuffix]];
}
else
{
controlImage = [NSImage imageNamed:[@"Resume" stringByAppendingString:self.controlImageSuffix]];
}
}
self.image = controlImage;
}
- (void)updateTrackingAreas
{
if (self.fTrackingArea != nil)
{
[self removeTrackingArea:self.fTrackingArea];
}
NSTrackingAreaOptions opts = (NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways);
self.fTrackingArea = [[NSTrackingArea alloc] initWithRect:self.bounds options:opts owner:self userInfo:nil];
[self addTrackingArea:self.fTrackingArea];
}
@end

View File

@ -0,0 +1,8 @@
// This file Copyright © 2006-2023 Transmission authors and contributors.
// It may be used under the MIT (SPDX: MIT) license.
// License text can be found in the licenses/ folder.
#import <Cocoa/Cocoa.h>
@interface TorrentCellRevealButton : NSButton
@end

View File

@ -0,0 +1,79 @@
// This file Copyright © 2006-2023 Transmission authors and contributors.
// It may be used under the MIT (SPDX: MIT) license.
// License text can be found in the licenses/ folder.
#import "TorrentCellRevealButton.h"
#import "TorrentTableView.h"
@interface TorrentCellRevealButton ()
@property(nonatomic) NSTrackingArea* fTrackingArea;
@property(nonatomic, copy) NSString* revealImageString;
@property(nonatomic) TorrentTableView* torrentTableView;
@end
@implementation TorrentCellRevealButton
- (void)awakeFromNib
{
self.revealImageString = @"RevealOff";
[self updateImage];
}
- (void)setupTorrentTableView
{
if (!self.torrentTableView)
{
self.torrentTableView = (TorrentTableView*)[[[self superview] superview] superview];
}
}
- (void)mouseEntered:(NSEvent*)event
{
[super mouseEntered:event];
self.revealImageString = @"RevealHover";
[self updateImage];
[self.torrentTableView hoverEventBeganForView:self];
}
- (void)mouseExited:(NSEvent*)event
{
[super mouseExited:event];
self.revealImageString = @"RevealOff";
[self updateImage];
[self.torrentTableView hoverEventEndedForView:self];
}
- (void)mouseDown:(NSEvent*)event
{
//when filterbar is shown, we need to remove focus otherwise action fails
[self.window makeFirstResponder:self.torrentTableView];
[super mouseDown:event];
self.revealImageString = @"RevealOn";
[self updateImage];
}
- (void)updateImage
{
[self setupTorrentTableView];
NSImage* revealImage = [NSImage imageNamed:self.revealImageString];
self.image = revealImage;
[self setNeedsDisplay:YES];
}
- (void)updateTrackingAreas
{
if (self.fTrackingArea != nil)
{
[self removeTrackingArea:self.fTrackingArea];
}
NSTrackingAreaOptions opts = (NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways);
self.fTrackingArea = [[NSTrackingArea alloc] initWithRect:self.bounds options:opts owner:self userInfo:nil];
[self addTrackingArea:self.fTrackingArea];
}
@end

View File

@ -10,19 +10,15 @@ extern const CGFloat kGroupSeparatorHeight;
@interface TorrentTableView : NSOutlineView<NSOutlineViewDelegate, NSAnimationDelegate, NSPopoverDelegate>
- (void)reloadVisibleRows;
- (BOOL)isGroupCollapsed:(NSInteger)value;
- (void)removeCollapsedGroup:(NSInteger)value;
- (void)removeAllCollapsedGroups;
- (void)saveCollapsedGroups;
- (void)removeTrackingAreas;
@property(nonatomic) NSInteger hoverRow;
@property(nonatomic) NSInteger controlButtonHoverRow;
@property(nonatomic) NSInteger revealButtonHoverRow;
@property(nonatomic) NSInteger actionButtonHoverRow;
- (void)restoreSelectionIndexes;
- (void)selectValues:(NSArray*)values;
@property(nonatomic, readonly) NSArray* selectedValues;
@property(nonatomic, readonly) NSArray<Torrent*>* selectedTorrents;
- (NSRect)iconRectForRow:(NSInteger)row;
@ -30,9 +26,14 @@ extern const CGFloat kGroupSeparatorHeight;
- (void)copy:(id)sender;
- (void)paste:(id)sender;
- (void)toggleControlForTorrent:(Torrent*)torrent;
- (void)hoverEventBeganForView:(id)view;
- (void)hoverEventEndedForView:(id)view;
- (void)displayTorrentActionPopoverForEvent:(NSEvent*)event;
- (void)toggleGroupRowRatio;
- (IBAction)toggleControlForTorrent:(id)sender;
- (IBAction)displayTorrentActionPopover:(id)sender;
- (IBAction)setQuickLimitMode:(id)sender;
- (void)setQuickLimit:(id)sender;

View File

@ -12,12 +12,19 @@
#import "NSStringAdditions.h"
#import "Torrent.h"
#import "TorrentCell.h"
#import "SmallTorrentCell.h"
#import "GroupCell.h"
#import "TorrentGroup.h"
#import "GroupTextCell.h"
#import "GroupsController.h"
#import "NSImageAdditions.h"
#import "TorrentCellActionButton.h"
#import "TorrentCellControlButton.h"
#import "TorrentCellRevealButton.h"
CGFloat const kGroupSeparatorHeight = 18.0;
static NSInteger const kMaxGroup = 999999;
static CGFloat const kErrorImageSize = 20.0;
//eliminate when Lion-only
typedef NS_ENUM(NSUInteger, ActionMenuTag) {
@ -38,9 +45,6 @@ static NSTimeInterval const kToggleProgressSeconds = 0.175;
@property(nonatomic) IBOutlet Controller* fController;
@property(nonatomic) TorrentCell* fTorrentCell;
@property(nonatomic) GroupTextCell* fGroupTextCell;
@property(nonatomic, readonly) NSUserDefaults* fDefaults;
@property(nonatomic, readonly) NSMutableIndexSet* fCollapsedGroups;
@ -48,7 +52,7 @@ static NSTimeInterval const kToggleProgressSeconds = 0.175;
@property(nonatomic) IBOutlet NSMenu* fContextRow;
@property(nonatomic) IBOutlet NSMenu* fContextNoRow;
@property(nonatomic) NSArray* fSelectedValues;
@property(nonatomic) NSIndexSet* fSelectedRowIndexes;
@property(nonatomic) IBOutlet NSMenu* fActionMenu;
@property(nonatomic) IBOutlet NSMenu* fUploadMenu;
@ -64,6 +68,8 @@ static NSTimeInterval const kToggleProgressSeconds = 0.175;
@property(nonatomic) BOOL fActionPopoverShown;
@property(nonatomic) NSView* fPositioningView;
@property(nonatomic) NSDictionary* fHoverEventDict;
@end
@implementation TorrentTableView
@ -74,9 +80,6 @@ static NSTimeInterval const kToggleProgressSeconds = 0.175;
{
_fDefaults = NSUserDefaults.standardUserDefaults;
_fTorrentCell = [[TorrentCell alloc] init];
_fGroupTextCell = [[GroupTextCell alloc] init];
NSData* groupData;
if ((groupData = [_fDefaults dataForKey:@"CollapsedGroupIndexes"]))
{
@ -93,14 +96,10 @@ static NSTimeInterval const kToggleProgressSeconds = 0.175;
_fCollapsedGroups = [[NSMutableIndexSet alloc] init];
}
_hoverRow = -1;
_controlButtonHoverRow = -1;
_revealButtonHoverRow = -1;
_actionButtonHoverRow = -1;
_fActionPopoverShown = NO;
self.delegate = self;
self.indentationPerLevel = 0;
_piecesBarPercent = [_fDefaults boolForKey:@"PiecesBar"] ? 1.0 : 0.0;
@ -120,19 +119,74 @@ static NSTimeInterval const kToggleProgressSeconds = 0.175;
- (void)awakeFromNib
{
//set group columns to show ratio, needs to be in awakeFromNib to size columns correctly
[self setGroupStatusColumns];
//disable highlight color and set manually in drawRow
[self setSelectionHighlightStyle:NSTableViewSelectionHighlightStyleNone];
[NSNotificationCenter.defaultCenter addObserver:self selector:@selector(refreshTorrentTable) name:@"RefreshTorrentTable"
object:nil];
[NSNotificationCenter.defaultCenter addObserver:self selector:@selector(setNeedsDisplay) name:@"RefreshTorrentTable" object:nil];
}
- (void)refreshTorrentTable
//make sure we don't lose selection on manual reloads
- (void)reloadData
{
self.needsDisplay = YES;
[super reloadData];
[self restoreSelectionIndexes];
}
- (void)reloadVisibleRows
{
NSRect visibleRect = self.visibleRect;
NSRange range = [self rowsInRect:visibleRect];
//since we use floating group rows, we need some magic to find visible group rows
if ([self.fDefaults boolForKey:@"SortByGroup"])
{
NSInteger location = range.location;
NSInteger length = range.length;
NSRange fullRange = NSMakeRange(0, length + location);
NSIndexSet* fullIndexSet = [NSIndexSet indexSetWithIndexesInRange:fullRange];
NSMutableIndexSet* visibleIndexSet = [[NSMutableIndexSet alloc] init];
[fullIndexSet enumerateIndexesUsingBlock:^(NSUInteger row, BOOL* stop) {
id rowView = [self rowViewAtRow:row makeIfNecessary:NO];
if ([rowView isGroupRowStyle])
{
[visibleIndexSet addIndex:row];
}
else if (NSIntersectsRect(visibleRect, [self rectOfRow:row]))
{
[visibleIndexSet addIndex:row];
}
}];
[self reloadDataForRowIndexes:visibleIndexSet columnIndexes:[NSIndexSet indexSetWithIndex:0]];
}
else
{
[self reloadDataForRowIndexes:[NSIndexSet indexSetWithIndexesInRange:range] columnIndexes:[NSIndexSet indexSetWithIndex:0]];
}
}
- (void)reloadDataForRowIndexes:(NSIndexSet*)rowIndexes columnIndexes:(NSIndexSet*)columnIndexes
{
[super reloadDataForRowIndexes:rowIndexes columnIndexes:columnIndexes];
//redraw fControlButton
BOOL minimal = [self.fDefaults boolForKey:@"SmallView"];
[rowIndexes enumerateIndexesUsingBlock:^(NSUInteger row, BOOL* stop) {
id rowView = [self rowViewAtRow:row makeIfNecessary:NO];
if (![rowView isGroupRowStyle])
{
if (minimal)
{
SmallTorrentCell* smallCell = [self viewAtColumn:0 row:row makeIfNecessary:NO];
[(TorrentCellControlButton*)smallCell.fControlButton resetImage];
}
else
{
TorrentCell* torrentCell = [self viewAtColumn:0 row:row makeIfNecessary:NO];
[(TorrentCellControlButton*)torrentCell.fControlButton resetImage];
}
}
}];
[self restoreSelectionIndexes];
}
- (BOOL)isGroupCollapsed:(NSInteger)value
@ -168,9 +222,11 @@ static NSTimeInterval const kToggleProgressSeconds = 0.175;
- (BOOL)outlineView:(NSOutlineView*)outlineView isGroupItem:(id)item
{
//return no and style the groupItem cell manually in willDisplayCell
//otherwise we get unwanted padding before each group header
return NO;
if ([item isKindOfClass:[Torrent class]])
{
return NO;
}
return YES;
}
- (CGFloat)outlineView:(NSOutlineView*)outlineView heightOfRowByItem:(id)item
@ -178,119 +234,207 @@ static NSTimeInterval const kToggleProgressSeconds = 0.175;
return [item isKindOfClass:[Torrent class]] ? self.rowHeight : kGroupSeparatorHeight;
}
- (NSCell*)outlineView:(NSOutlineView*)outlineView dataCellForTableColumn:(NSTableColumn*)tableColumn item:(id)item
{
BOOL const group = ![item isKindOfClass:[Torrent class]];
if (!tableColumn)
{
return !group ? self.fTorrentCell : nil;
}
else
{
NSString* ident = tableColumn.identifier;
NSArray* imageColumns = @[ @"Color", @"DL Image", @"UL Image" ];
if (![imageColumns containsObject:ident])
{
return group ? self.fGroupTextCell : nil;
}
else
{
return group ? [tableColumn dataCellForRow:[self rowForItem:item]] : nil;
}
}
}
- (void)outlineView:(NSOutlineView*)outlineView
willDisplayCell:(id)cell
forTableColumn:(NSTableColumn*)tableColumn
item:(id)item
- (NSView*)outlineView:(NSOutlineView*)outlineView viewForTableColumn:(NSTableColumn*)tableColumn item:(id)item
{
if ([item isKindOfClass:[Torrent class]])
{
if (!tableColumn)
Torrent* torrent = (Torrent*)item;
BOOL const minimal = [self.fDefaults boolForKey:@"SmallView"];
BOOL const error = torrent.anyErrorOrWarning;
if (minimal)
{
TorrentCell* torrentCell = cell;
torrentCell.representedObject = item;
SmallTorrentCell* smallCell = [outlineView makeViewWithIdentifier:@"SmallTorrentCell" owner:self];
smallCell.fTorrentTableView = self;
NSInteger const row = [self rowForItem:item];
torrentCell.hover = (row == self.hoverRow);
torrentCell.hoverControl = (row == self.controlButtonHoverRow);
torrentCell.hoverReveal = (row == self.revealButtonHoverRow);
torrentCell.hoverAction = (row == self.actionButtonHoverRow);
//set this so that we can draw bar in smallCell drawRect
smallCell.objectValue = torrent;
// if cell is selected, set backgroundStyle
// then can provide alternate font color in TorrentCell - drawInteriorWithFrame
NSIndexSet* selectedRowIndexes = self.selectedRowIndexes;
if ([selectedRowIndexes containsIndex:row])
smallCell.fTorrentTitleField.stringValue = torrent.name;
//set torrent icon
smallCell.fIconView.image = error ? [NSImage imageNamed:NSImageNameCaution] : torrent.icon;
smallCell.fTorrentStatusField.stringValue = [self.fDefaults boolForKey:@"DisplaySmallStatusRegular"] ?
torrent.shortStatusString :
torrent.remainingTimeString;
;
smallCell.fActionButton.action = @selector(displayTorrentActionPopover:);
NSInteger const groupValue = torrent.groupValue;
NSImage* groupImage;
if (groupValue != -1)
{
torrentCell.backgroundStyle = NSBackgroundStyleEmphasized;
if (![self.fDefaults boolForKey:@"SortByGroup"])
{
NSColor* groupColor = [GroupsController.groups colorForIndex:groupValue];
groupImage = [NSImage discIconWithColor:groupColor insetFactor:0];
}
}
}
}
else
{
if ([cell isKindOfClass:[GroupTextCell class]])
{
GroupTextCell* groupCell = cell;
// if cell is selected, set selected flag so we can style the title in GroupTextCell drawInteriorWithFrame
NSInteger const row = [self rowForItem:item];
NSIndexSet* selectedRowIndexes = self.selectedRowIndexes;
groupCell.selected = [selectedRowIndexes containsIndex:row];
}
}
}
smallCell.fGroupIndicatorView.image = groupImage;
//we override row highlighting because we are custom drawing the group rows
//see isGroupItem
- (void)drawRow:(NSInteger)row clipRect:(NSRect)clipRect
{
NSColor* highlightColor = nil;
smallCell.fControlButton.action = @selector(toggleControlForTorrent:);
smallCell.fRevealButton.action = @selector(revealTorrentFile:);
id item = [self itemAtRow:row];
if (self.fHoverEventDict)
{
NSInteger row = [self rowForItem:item];
NSInteger hoverRow = [self.fHoverEventDict[@"row"] integerValue];
//we only highlight torrent cells
if ([item isKindOfClass:[Torrent class]])
{
//use system highlight color when Transmission is active
if (self == [self.window firstResponder] && [self.window isMainWindow] && [self.window isKeyWindow])
{
highlightColor = [NSColor alternateSelectedControlColor];
if (row == hoverRow)
{
smallCell.fTorrentStatusField.hidden = YES;
smallCell.fControlButton.hidden = NO;
smallCell.fRevealButton.hidden = NO;
}
}
else
{
smallCell.fTorrentStatusField.hidden = NO;
smallCell.fControlButton.hidden = YES;
smallCell.fRevealButton.hidden = YES;
}
//redraw buttons
[smallCell.fControlButton display];
[smallCell.fRevealButton display];
return smallCell;
}
else
{
highlightColor = [NSColor disabledControlTextColor];
TorrentCell* torrentCell = [outlineView makeViewWithIdentifier:@"TorrentCell" owner:self];
torrentCell.fTorrentTableView = self;
//set this so that we can draw bar in torrentCell drawRect
torrentCell.objectValue = torrent;
torrentCell.fTorrentTitleField.stringValue = torrent.name;
torrentCell.fTorrentProgressField.stringValue = torrent.progressString;
//set torrent icon
NSImage* fileImage = torrent.icon;
//error badge
if (error)
{
NSRect frame = torrentCell.fIconView.frame;
NSImage* resultImage = [[NSImage alloc] initWithSize:NSMakeSize(frame.size.height, frame.size.width)];
[resultImage lockFocus];
//draw fileImage
[fileImage drawAtPoint:NSZeroPoint fromRect:NSZeroRect operation:NSCompositingOperationSourceOver fraction:1.0];
//overlay error badge
NSImage* errorImage = [NSImage imageNamed:NSImageNameCaution];
NSRect const errorRect = NSMakeRect(frame.origin.x, 0, kErrorImageSize, kErrorImageSize);
[errorImage drawInRect:errorRect fromRect:NSZeroRect operation:NSCompositingOperationSourceOver fraction:1.0
respectFlipped:YES
hints:nil];
[resultImage unlockFocus];
torrentCell.fIconView.image = resultImage;
}
else
{
torrentCell.fIconView.image = fileImage;
}
NSString* status;
if (self.fHoverEventDict)
{
NSInteger row = [self rowForItem:item];
NSInteger hoverRow = [self.fHoverEventDict[@"row"] integerValue];
if (row == hoverRow)
{
status = self.fHoverEventDict[@"string"];
}
}
torrentCell.fTorrentStatusField.stringValue = status ? status : torrent.statusString;
torrentCell.fActionButton.action = @selector(displayTorrentActionPopover:);
NSInteger const groupValue = torrent.groupValue;
NSImage* groupImage;
if (groupValue != -1)
{
if (![self.fDefaults boolForKey:@"SortByGroup"])
{
NSColor* groupColor = [GroupsController.groups colorForIndex:groupValue];
groupImage = [NSImage discIconWithColor:groupColor insetFactor:0];
}
}
torrentCell.fGroupIndicatorView.image = groupImage;
torrentCell.fControlButton.action = @selector(toggleControlForTorrent:);
torrentCell.fRevealButton.action = @selector(revealTorrentFile:);
//redraw buttons
[torrentCell.fControlButton display];
[torrentCell.fRevealButton display];
return torrentCell;
}
NSIndexSet* selectedRowIndexes = [self selectedRowIndexes];
if ([selectedRowIndexes containsIndex:row])
{
[highlightColor setFill];
NSRectFill([self rectOfRow:row]);
}
}
[super drawRow:row clipRect:clipRect];
}
- (NSRect)frameOfCellAtColumn:(NSInteger)column row:(NSInteger)row
{
if (column == -1)
{
return [self rectOfRow:row];
}
else
{
NSRect rect = [super frameOfCellAtColumn:column row:row];
TorrentGroup* group = (TorrentGroup*)item;
GroupCell* groupCell = [outlineView makeViewWithIdentifier:@"GroupCell" owner:self];
//adjust placement for proper vertical alignment
if (column == [self columnWithIdentifier:@"Group"])
NSInteger groupIndex = group.groupIndex;
NSColor* groupColor = groupIndex != -1 ? [GroupsController.groups colorForIndex:groupIndex] :
[NSColor colorWithWhite:1.0 alpha:0];
groupCell.fGroupIndicatorView.image = [NSImage discIconWithColor:groupColor insetFactor:0];
NSString* groupName = groupIndex != -1 ? [GroupsController.groups nameForIndex:groupIndex] :
NSLocalizedString(@"No Group", "Group table row");
NSInteger row = [self rowForItem:item];
if ([self isRowSelected:row])
{
rect.size.height -= 1.0f;
NSMutableAttributedString* string = [[NSMutableAttributedString alloc] initWithString:groupName];
NSDictionary* attributes = @{
NSFontAttributeName : [NSFont boldSystemFontOfSize:11.0],
NSForegroundColorAttributeName : [NSColor labelColor]
};
[string addAttributes:attributes range:NSMakeRange(0, string.length)];
groupCell.fGroupTitleField.attributedStringValue = string;
}
else
{
groupCell.fGroupTitleField.stringValue = groupName;
}
return rect;
groupCell.fGroupDownloadField.stringValue = [NSString stringForSpeed:group.downloadRate];
groupCell.fGroupDownloadView.image = [NSImage imageNamed:@"DownArrowGroupTemplate"];
BOOL displayGroupRowRatio = [self.fDefaults boolForKey:@"DisplayGroupRowRatio"];
groupCell.fGroupDownloadField.hidden = displayGroupRowRatio;
groupCell.fGroupDownloadView.hidden = displayGroupRowRatio;
if (displayGroupRowRatio)
{
groupCell.fGroupUploadAndRatioView.image = [NSImage imageNamed:@"YingYangGroupTemplate"];
groupCell.fGroupUploadAndRatioView.image.accessibilityDescription = NSLocalizedString(@"Ratio", "Torrent -> status image");
groupCell.fGroupUploadAndRatioField.stringValue = [NSString stringForRatio:group.ratio];
}
else
{
groupCell.fGroupUploadAndRatioView.image = [NSImage imageNamed:@"UpArrowGroupTemplate"];
groupCell.fGroupUploadAndRatioView.image.accessibilityDescription = NSLocalizedString(@"UL", "Torrent -> status image");
groupCell.fGroupUploadAndRatioField.stringValue = [NSString stringForSpeed:group.uploadRate];
}
return groupCell;
}
return nil;
}
- (NSString*)outlineView:(NSOutlineView*)outlineView typeSelectStringForTableColumn:(NSTableColumn*)tableColumn item:(id)item
@ -342,160 +486,10 @@ static NSTimeInterval const kToggleProgressSeconds = 0.175;
}
}
- (void)updateTrackingAreas
- (void)outlineViewSelectionDidChange:(NSNotification*)notification
{
[super updateTrackingAreas];
[self removeTrackingAreas];
NSRange const rows = [self rowsInRect:self.visibleRect];
if (rows.length == 0)
{
return;
}
NSPoint mouseLocation = [self convertPoint:self.window.mouseLocationOutsideOfEventStream fromView:nil];
for (NSUInteger row = rows.location; row < NSMaxRange(rows); row++)
{
if (![[self itemAtRow:row] isKindOfClass:[Torrent class]])
{
continue;
}
NSDictionary* userInfo = @{ @"Row" : @(row) };
TorrentCell* cell = (TorrentCell*)[self preparedCellAtColumn:-1 row:row];
[cell addTrackingAreasForView:self inRect:[self rectOfRow:row] withUserInfo:userInfo mouseLocation:mouseLocation];
}
}
- (void)removeTrackingAreas
{
_hoverRow = -1;
_controlButtonHoverRow = -1;
_revealButtonHoverRow = -1;
_actionButtonHoverRow = -1;
for (NSTrackingArea* area in self.trackingAreas)
{
if (area.owner == self && area.userInfo[@"Row"])
{
[self removeTrackingArea:area];
}
}
}
- (void)setHoverRow:(NSInteger)row
{
NSAssert([self.fDefaults boolForKey:@"SmallView"], @"cannot set a hover row when not in compact view");
_hoverRow = row;
if (row >= 0)
{
[self setNeedsDisplayInRect:[self rectOfRow:row]];
}
}
- (void)setControlButtonHoverRow:(NSInteger)row
{
_controlButtonHoverRow = row;
if (row >= 0)
{
[self setNeedsDisplayInRect:[self rectOfRow:row]];
}
}
- (void)setRevealButtonHoverRow:(NSInteger)row
{
_revealButtonHoverRow = row;
if (row >= 0)
{
[self setNeedsDisplayInRect:[self rectOfRow:row]];
}
}
- (void)setActionButtonHoverRow:(NSInteger)row
{
_actionButtonHoverRow = row;
if (row >= 0)
{
[self setNeedsDisplayInRect:[self rectOfRow:row]];
}
}
- (void)mouseEntered:(NSEvent*)event
{
NSDictionary* dict = (NSDictionary*)event.userData;
NSNumber* row;
if ((row = dict[@"Row"]))
{
NSInteger rowVal = row.integerValue;
NSString* type = dict[@"Type"];
if ([type isEqualToString:@"Action"])
{
_actionButtonHoverRow = rowVal;
}
else if ([type isEqualToString:@"Control"])
{
_controlButtonHoverRow = rowVal;
}
else if ([type isEqualToString:@"Reveal"])
{
_revealButtonHoverRow = rowVal;
}
else
{
_hoverRow = rowVal;
if (![self.fDefaults boolForKey:@"SmallView"])
{
return;
}
}
[self setNeedsDisplayInRect:[self rectOfRow:rowVal]];
}
}
- (void)mouseExited:(NSEvent*)event
{
NSDictionary* dict = (NSDictionary*)event.userData;
NSNumber* row;
if ((row = dict[@"Row"]))
{
NSString* type = dict[@"Type"];
if ([type isEqualToString:@"Action"])
{
_actionButtonHoverRow = -1;
}
else if ([type isEqualToString:@"Control"])
{
_controlButtonHoverRow = -1;
}
else if ([type isEqualToString:@"Reveal"])
{
_revealButtonHoverRow = -1;
}
else
{
_hoverRow = -1;
if (![self.fDefaults boolForKey:@"SmallView"])
{
return;
}
}
[self setNeedsDisplayInRect:[self rectOfRow:row.integerValue]];
}
}
- (void)outlineViewSelectionIsChanging:(NSNotification*)notification
{
#warning eliminate when view-based?
//if pushing a button, don't change the selected rows
if (self.fSelectedValues)
{
[self selectValues:self.fSelectedValues];
}
self.fSelectedRowIndexes = self.selectedRowIndexes;
[self reloadVisibleRows];
}
- (void)outlineViewItemDidExpand:(NSNotification*)notification
@ -532,42 +526,16 @@ static NSTimeInterval const kToggleProgressSeconds = 0.175;
NSPoint point = [self convertPoint:event.locationInWindow fromView:nil];
NSInteger const row = [self rowAtPoint:point];
//check to toggle group status before anything else
if ([self pointInGroupStatusRect:point])
{
[self.fDefaults setBool:![self.fDefaults boolForKey:@"DisplayGroupRowRatio"] forKey:@"DisplayGroupRowRatio"];
[self setGroupStatusColumns];
return;
}
BOOL const pushed = row != -1 &&
(self.actionButtonHoverRow == row || self.revealButtonHoverRow == row || self.controlButtonHoverRow == row);
//if pushing a button, don't change the selected rows
if (pushed)
{
self.fSelectedValues = self.selectedValues;
}
[super mouseDown:event];
self.fSelectedValues = nil;
//avoid weird behavior when showing menu by doing this after mouse down
if (row != -1 && self.actionButtonHoverRow == row)
id item = nil;
if (row != -1)
{
#warning maybe make appear on mouse down
[self displayTorrentActionPopoverForEvent:event];
item = [self itemAtRow:row];
}
else if (!pushed && event.clickCount == 2) //double click
{
id item = nil;
if (row != -1)
{
item = [self itemAtRow:row];
}
if (event.clickCount == 2) //double click
{
if (!item || [item isKindOfClass:[Torrent class]])
{
[self.fController showInfo:nil];
@ -584,52 +552,12 @@ static NSTimeInterval const kToggleProgressSeconds = 0.175;
}
}
}
}
- (void)selectValues:(NSArray*)values
{
NSMutableIndexSet* indexSet = [NSMutableIndexSet indexSet];
for (id item in values)
else if ([self pointInGroupStatusRect:point])
{
if ([item isKindOfClass:[Torrent class]])
{
NSInteger const index = [self rowForItem:item];
if (index != -1)
{
[indexSet addIndex:index];
}
}
else
{
TorrentGroup* group = (TorrentGroup*)item;
NSInteger const groupIndex = group.groupIndex;
for (NSInteger i = 0; i < self.numberOfRows; i++)
{
id tableItem = [self itemAtRow:i];
if ([tableItem isKindOfClass:[TorrentGroup class]] && groupIndex == ((TorrentGroup*)tableItem).groupIndex)
{
[indexSet addIndex:i];
break;
}
}
}
//we check for this here rather than in the GroupCell
//as using floating group rows causes all sorts of weirdness...
[self toggleGroupRowRatio];
}
[self selectRowIndexes:indexSet byExtendingSelection:NO];
}
- (NSArray*)selectedValues
{
NSIndexSet* selectedIndexes = self.selectedRowIndexes;
NSMutableArray* values = [NSMutableArray arrayWithCapacity:selectedIndexes.count];
for (NSUInteger i = selectedIndexes.firstIndex; i != NSNotFound; i = [selectedIndexes indexGreaterThanIndex:i])
{
[values addObject:[self itemAtRow:i]];
}
return values;
}
- (NSArray<Torrent*>*)selectedTorrents
@ -676,6 +604,11 @@ static NSTimeInterval const kToggleProgressSeconds = 0.175;
}
}
- (void)restoreSelectionIndexes
{
[self selectRowIndexes:self.fSelectedRowIndexes byExtendingSelection:NO];
}
//make sure that the pause buttons become orange when holding down the option key
- (void)flagsChanged:(NSEvent*)event
{
@ -708,7 +641,24 @@ static NSTimeInterval const kToggleProgressSeconds = 0.175;
- (NSRect)iconRectForRow:(NSInteger)row
{
return [self.fTorrentCell iconRectForBounds:[self rectOfRow:row]];
BOOL minimal = [self.fDefaults boolForKey:@"SmallView"];
NSRect rect;
if (minimal)
{
SmallTorrentCell* smallCell = [self viewAtColumn:0 row:row makeIfNecessary:NO];
rect = smallCell.fActionButton.frame;
}
else
{
TorrentCell* torrentCell = [self viewAtColumn:0 row:row makeIfNecessary:NO];
rect = torrentCell.fIconView.frame;
}
NSRect rowRect = [self rectOfRow:row];
rect.origin.y += rowRect.origin.y;
rect.origin.x += self.intercellSpacing.width;
return rect;
}
- (BOOL)acceptsFirstResponder
@ -799,8 +749,101 @@ static NSTimeInterval const kToggleProgressSeconds = 0.175;
return YES;
}
- (void)toggleControlForTorrent:(Torrent*)torrent
- (void)hoverEventBeganForView:(id)view
{
NSInteger row = [self rowForView:view];
Torrent* torrent = [self itemAtRow:row];
BOOL minimal = [self.fDefaults boolForKey:@"SmallView"];
if (minimal)
{
if ([view isKindOfClass:[SmallTorrentCell class]])
{
self.fHoverEventDict = @{ @"row" : [NSNumber numberWithInteger:row] };
}
else if ([view isKindOfClass:[TorrentCellActionButton class]])
{
SmallTorrentCell* smallCell = [self viewAtColumn:0 row:row makeIfNecessary:NO];
smallCell.fIconView.hidden = YES;
}
}
else
{
NSString* statusString;
if ([view isKindOfClass:[TorrentCellRevealButton class]])
{
statusString = NSLocalizedString(@"Show the data file in Finder", "Torrent cell -> button info");
}
else if ([view isKindOfClass:[TorrentCellControlButton class]])
{
if (torrent.active)
statusString = NSLocalizedString(@"Pause the transfer", "Torrent Table -> tooltip");
else
{
if (NSApp.currentEvent.modifierFlags & NSEventModifierFlagOption)
{
statusString = NSLocalizedString(@"Resume the transfer right away", "Torrent cell -> button info");
}
else if (torrent.waitingToStart)
{
statusString = NSLocalizedString(@"Stop waiting to start", "Torrent cell -> button info");
}
else
{
statusString = NSLocalizedString(@"Resume the transfer", "Torrent cell -> button info");
}
}
}
else if ([view isKindOfClass:[TorrentCellActionButton class]])
{
statusString = NSLocalizedString(@"Change transfer settings", "Torrent Table -> tooltip");
}
if (statusString)
{
self.fHoverEventDict = @{ @"string" : statusString, @"row" : [NSNumber numberWithInteger:row] };
}
}
[self reloadVisibleRows];
}
- (void)hoverEventEndedForView:(id)view
{
NSInteger row = [self rowForView:[view superview]];
BOOL update = YES;
BOOL minimal = [self.fDefaults boolForKey:@"SmallView"];
if (minimal)
{
if (minimal && ![view isKindOfClass:[SmallTorrentCell class]])
{
if ([view isKindOfClass:[TorrentCellActionButton class]])
{
SmallTorrentCell* smallCell = [self viewAtColumn:0 row:row makeIfNecessary:NO];
smallCell.fIconView.hidden = NO;
}
update = NO;
}
}
if (update)
{
self.fHoverEventDict = nil;
[self reloadDataForRowIndexes:[NSIndexSet indexSetWithIndex:row] columnIndexes:[NSIndexSet indexSetWithIndex:0]];
}
}
- (void)toggleGroupRowRatio
{
BOOL displayGroupRowRatio = [self.fDefaults boolForKey:@"DisplayGroupRowRatio"];
[self.fDefaults setBool:!displayGroupRowRatio forKey:@"DisplayGroupRowRatio"];
[self reloadVisibleRows];
}
- (IBAction)toggleControlForTorrent:(id)sender
{
Torrent* torrent = [self itemAtRow:[self rowForView:[sender superview]]];
if (torrent.active)
{
[self.fController stopTorrents:@[ torrent ]];
@ -822,22 +865,26 @@ static NSTimeInterval const kToggleProgressSeconds = 0.175;
}
}
- (void)displayTorrentActionPopoverForEvent:(NSEvent*)event
- (IBAction)revealTorrentFile:(id)sender
{
NSInteger const row = [self rowAtPoint:[self convertPoint:event.locationInWindow fromView:nil]];
if (row < 0)
Torrent* torrent = [self itemAtRow:[self rowForView:[sender superview]]];
NSString* location = torrent.dataLocation;
if (location)
{
return;
NSURL* file = [NSURL fileURLWithPath:location];
[NSWorkspace.sharedWorkspace activateFileViewerSelectingURLs:@[ file ]];
}
}
NSRect const rect = [self.fTorrentCell actionRectForBounds:[self rectOfRow:row]];
- (IBAction)displayTorrentActionPopover:(id)sender
{
if (self.fActionPopoverShown)
{
return;
}
Torrent* torrent = [self itemAtRow:row];
Torrent* torrent = [self itemAtRow:[self rowForView:[sender superview]]];
NSRect rect = [sender bounds];
NSPopover* popover = [[NSPopover alloc] init];
popover.behavior = NSPopoverBehaviorTransient;
@ -845,7 +892,7 @@ static NSTimeInterval const kToggleProgressSeconds = 0.175;
popover.contentViewController = infoViewController;
popover.delegate = self;
[popover showRelativeToRect:rect ofView:self preferredEdge:NSMaxYEdge];
[popover showRelativeToRect:rect ofView:sender preferredEdge:NSMaxYEdge];
[infoViewController setInfoForTorrents:@[ torrent ]];
[infoViewController updateInfo];
@ -862,7 +909,7 @@ static NSTimeInterval const kToggleProgressSeconds = 0.175;
}
else
{
[popover showRelativeToRect:rect ofView:self preferredEdge:NSMaxYEdge];
[popover showRelativeToRect:rect ofView:sender preferredEdge:NSMaxYEdge];
}
}
@ -1112,22 +1159,17 @@ static NSTimeInterval const kToggleProgressSeconds = 0.175;
- (BOOL)pointInGroupStatusRect:(NSPoint)point
{
NSInteger row = [self rowAtPoint:point];
if (row < 0 || [[self itemAtRow:row] isKindOfClass:[Torrent class]])
if (![[self itemAtRow:row] isKindOfClass:[TorrentGroup class]])
{
return NO;
}
NSString* ident = (self.tableColumns[[self columnAtPoint:point]]).identifier;
return [ident isEqualToString:@"UL"] || [ident isEqualToString:@"UL Image"] || [ident isEqualToString:@"DL"] ||
[ident isEqualToString:@"DL Image"];
}
//check if click is within the status/ratio rect
GroupCell* groupCell = [self viewAtColumn:0 row:row makeIfNecessary:NO];
NSRect titleRect = groupCell.fGroupTitleField.frame;
CGFloat maxX = NSMaxX(titleRect);
- (void)setGroupStatusColumns
{
BOOL const ratio = [self.fDefaults boolForKey:@"DisplayGroupRowRatio"];
[self tableColumnWithIdentifier:@"DL"].hidden = ratio;
[self tableColumnWithIdentifier:@"DL Image"].hidden = ratio;
return point.x > maxX;
}
@end