From 590adb13cd865054aa3d14decee582544bc0c80a Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Tue, 22 Apr 2025 21:25:06 -0700 Subject: [PATCH] Make RSTree a local module. --- Account/Package.swift | 2 +- NetNewsWire.xcodeproj/project.pbxproj | 79 +++--- .../xcshareddata/swiftpm/Package.resolved | 9 - RSTree/Package.swift | 18 ++ .../Sources/RSTree/NSOutlineView+RSTree.swift | 58 +++++ RSTree/Sources/RSTree/Node.swift | 224 ++++++++++++++++++ RSTree/Sources/RSTree/NodePath.swift | 42 ++++ RSTree/Sources/RSTree/RSTree.swift | 3 + .../RSTree/TopLevelRepresentedObject.swift | 15 ++ RSTree/Sources/RSTree/TreeController.swift | 135 +++++++++++ 10 files changed, 530 insertions(+), 55 deletions(-) create mode 100644 RSTree/Package.swift create mode 100644 RSTree/Sources/RSTree/NSOutlineView+RSTree.swift create mode 100644 RSTree/Sources/RSTree/Node.swift create mode 100644 RSTree/Sources/RSTree/NodePath.swift create mode 100644 RSTree/Sources/RSTree/RSTree.swift create mode 100644 RSTree/Sources/RSTree/TopLevelRepresentedObject.swift create mode 100644 RSTree/Sources/RSTree/TreeController.swift diff --git a/Account/Package.swift b/Account/Package.swift index dc591cdbd..6c54966e1 100644 --- a/Account/Package.swift +++ b/Account/Package.swift @@ -18,7 +18,7 @@ dependencies.append(contentsOf: [ let package = Package( name: "Account", platforms: [.macOS(.v13), .iOS(.v17)], - products: [ + products: [ .library( name: "Account", type: .dynamic, diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 61c457f04..5491a15a7 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -93,8 +93,6 @@ 5137C2E426F3F52D009EFEDB /* Sepia.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = 5137C2E326F3F52D009EFEDB /* Sepia.nnwtheme */; }; 5137C2E626F3F52D009EFEDB /* Sepia.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = 5137C2E326F3F52D009EFEDB /* Sepia.nnwtheme */; }; 51386A8E25673277005F3762 /* AccountCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51386A8D25673276005F3762 /* AccountCell.swift */; }; - 5138E93A24D33E5600AFF0FE /* RSTree in Frameworks */ = {isa = PBXBuildFile; productRef = 5138E93924D33E5600AFF0FE /* RSTree */; }; - 5138E93B24D33E5600AFF0FE /* RSTree in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 5138E93924D33E5600AFF0FE /* RSTree */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 5138E94C24D3417A00AFF0FE /* RSDatabase in Frameworks */ = {isa = PBXBuildFile; productRef = 5138E94B24D3417A00AFF0FE /* RSDatabase */; }; 5138E94D24D3417A00AFF0FE /* RSDatabase in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 5138E94B24D3417A00AFF0FE /* RSDatabase */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 513C5CF0232571C2003D4054 /* NetNewsWire iOS Share Extension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 513C5CE6232571C2003D4054 /* NetNewsWire iOS Share Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; @@ -116,8 +114,6 @@ 5144EA51227B8E4500D19003 /* AccountsFeedbinWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5144EA4F227B8E4500D19003 /* AccountsFeedbinWindowController.swift */; }; 5144EA52227B8E4500D19003 /* AccountsFeedbin.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5144EA50227B8E4500D19003 /* AccountsFeedbin.xib */; }; 514C16CE24D2E63F009A3AFA /* Account in Frameworks */ = {isa = PBXBuildFile; productRef = 514C16CD24D2E63F009A3AFA /* Account */; }; - 514C16DE24D2EF15009A3AFA /* RSTree in Frameworks */ = {isa = PBXBuildFile; productRef = 514C16DD24D2EF15009A3AFA /* RSTree */; }; - 514C16DF24D2EF15009A3AFA /* RSTree in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 514C16DD24D2EF15009A3AFA /* RSTree */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 5154368B229404D1005E1CDF /* FaviconGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51EF0F76227716200050506E /* FaviconGenerator.swift */; }; 515D4FCA23257CB500EE1167 /* Node-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97971ED9EFAA007D329B /* Node-Extensions.swift */; }; 515D4FCC2325815A00EE1167 /* SafariExt.js in Resources */ = {isa = PBXBuildFile; fileRef = 515D4FCB2325815A00EE1167 /* SafariExt.js */; }; @@ -158,8 +154,6 @@ 51B5C8E623F4BBFA00032075 /* ExtensionFeedAddRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B5C87A23F2317700032075 /* ExtensionFeedAddRequest.swift */; }; 51B5C8E723F4BBFA00032075 /* ExtensionFeedAddRequestFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B5C8BF23F3866C00032075 /* ExtensionFeedAddRequestFile.swift */; }; 51BC2F3824D3439A00E90810 /* Account in Frameworks */ = {isa = PBXBuildFile; productRef = 51BC2F3724D3439A00E90810 /* Account */; }; - 51BC2F4824D3439E00E90810 /* RSTree in Frameworks */ = {isa = PBXBuildFile; productRef = 51BC2F4724D3439E00E90810 /* RSTree */; }; - 51BC2F4D24D343AB00E90810 /* RSTree in Frameworks */ = {isa = PBXBuildFile; productRef = 51BC2F4C24D343AB00E90810 /* RSTree */; }; 51BC4AFF247277E0000A6ED8 /* URL-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BC4ADD247277DF000A6ED8 /* URL-Extensions.swift */; }; 51BC4B01247277E0000A6ED8 /* URL-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BC4ADD247277DF000A6ED8 /* URL-Extensions.swift */; }; 51C03081257D815A00609262 /* UnifiedWindow.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 51C0307F257D815A00609262 /* UnifiedWindow.storyboard */; }; @@ -373,6 +367,12 @@ 84E8E0DB202EC49300562D8F /* TimelineViewController+ContextualMenus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E8E0DA202EC49300562D8F /* TimelineViewController+ContextualMenus.swift */; }; 84E8E0EB202F693600562D8F /* DetailWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E8E0EA202F693600562D8F /* DetailWebView.swift */; }; 84E95D241FB1087500552D99 /* ArticlePasteboardWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E95D231FB1087500552D99 /* ArticlePasteboardWriter.swift */; }; + 84EE3F122DB8A088009D3A8D /* RSTree in Frameworks */ = {isa = PBXBuildFile; productRef = 84EE3F112DB8A088009D3A8D /* RSTree */; }; + 84EE3F132DB8A088009D3A8D /* RSTree in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 84EE3F112DB8A088009D3A8D /* RSTree */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + 84EE3F152DB8A0A0009D3A8D /* RSTree in Frameworks */ = {isa = PBXBuildFile; productRef = 84EE3F142DB8A0A0009D3A8D /* RSTree */; }; + 84EE3F162DB8A0A0009D3A8D /* RSTree in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 84EE3F142DB8A0A0009D3A8D /* RSTree */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + 84EE3F182DB8A0AC009D3A8D /* RSTree in Frameworks */ = {isa = PBXBuildFile; productRef = 84EE3F172DB8A0AC009D3A8D /* RSTree */; }; + 84EE3F1B2DB8A0B6009D3A8D /* RSTree in Frameworks */ = {isa = PBXBuildFile; productRef = 84EE3F1A2DB8A0B6009D3A8D /* RSTree */; }; 84F204E01FAACBB30076E152 /* ArticleArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F204DF1FAACBB30076E152 /* ArticleArray.swift */; }; 84F2D5371FC22FCC00998D64 /* PseudoFeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F2D5351FC22FCB00998D64 /* PseudoFeed.swift */; }; 84F2D5381FC22FCC00998D64 /* TodayFeedDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F2D5361FC22FCB00998D64 /* TodayFeedDelegate.swift */; }; @@ -548,9 +548,9 @@ 513F32782593EE6F0003048F /* Secrets in Embed Frameworks */, 843E2F1B2CF2B8C500ED170F /* RSWeb in Embed Frameworks */, 513F327B2593EE6F0003048F /* SyncDatabase in Embed Frameworks */, + 84EE3F162DB8A0A0009D3A8D /* RSTree in Embed Frameworks */, 513F32722593EE6F0003048F /* Articles in Embed Frameworks */, 513F32812593EF180003048F /* Account in Embed Frameworks */, - 5138E93B24D33E5600AFF0FE /* RSTree in Embed Frameworks */, 5138E94D24D3417A00AFF0FE /* RSDatabase in Embed Frameworks */, 513F32752593EE6F0003048F /* ArticlesDatabase in Embed Frameworks */, ); @@ -594,11 +594,11 @@ 513277442590FBB60064F1E7 /* Account in Embed Frameworks */, 5132775F2590FC640064F1E7 /* Articles in Embed Frameworks */, 843E2F182CF2B8A700ED170F /* RSWeb in Embed Frameworks */, + 84EE3F132DB8A088009D3A8D /* RSTree in Embed Frameworks */, 51A737C024DB197F0015FA66 /* RSDatabase in Embed Frameworks */, 513277662590FC780064F1E7 /* Secrets in Embed Frameworks */, 513277652590FC640064F1E7 /* SyncDatabase in Embed Frameworks */, 513277622590FC640064F1E7 /* ArticlesDatabase in Embed Frameworks */, - 514C16DF24D2EF15009A3AFA /* RSTree in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -863,6 +863,7 @@ 84E8E0DA202EC49300562D8F /* TimelineViewController+ContextualMenus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TimelineViewController+ContextualMenus.swift"; sourceTree = ""; }; 84E8E0EA202F693600562D8F /* DetailWebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailWebView.swift; sourceTree = ""; }; 84E95D231FB1087500552D99 /* ArticlePasteboardWriter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticlePasteboardWriter.swift; sourceTree = ""; }; + 84EE3F102DB8A078009D3A8D /* RSTree */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = RSTree; sourceTree = ""; }; 84F204DF1FAACBB30076E152 /* ArticleArray.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleArray.swift; sourceTree = ""; }; 84F2D5351FC22FCB00998D64 /* PseudoFeed.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PseudoFeed.swift; sourceTree = ""; }; 84F2D5361FC22FCB00998D64 /* TodayFeedDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TodayFeedDelegate.swift; sourceTree = ""; }; @@ -994,7 +995,7 @@ files = ( 27B86EEB25A53AAB00264340 /* Account in Frameworks */, 848E84DB2DB749860023F3BA /* RSCore in Frameworks */, - 51BC2F4D24D343AB00E90810 /* RSTree in Frameworks */, + 84EE3F1B2DB8A0B6009D3A8D /* RSTree in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1004,7 +1005,7 @@ files = ( 51BC2F3824D3439A00E90810 /* Account in Frameworks */, 848E84D82DB749720023F3BA /* RSCore in Frameworks */, - 51BC2F4824D3439E00E90810 /* RSTree in Frameworks */, + 84EE3F182DB8A0AC009D3A8D /* RSTree in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1026,6 +1027,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 84EE3F152DB8A0A0009D3A8D /* RSTree in Frameworks */, 848E84D52DB749670023F3BA /* RSCore in Frameworks */, 179D280B26F6F93D003B2E0A /* Zip in Frameworks */, 8424B31B2DB73D530053AA11 /* RSParser in Frameworks */, @@ -1038,7 +1040,6 @@ 51E4DB082425F9EB0091EB5B /* CloudKit.framework in Frameworks */, 513F32742593EE6F0003048F /* ArticlesDatabase in Frameworks */, 513F327A2593EE6F0003048F /* SyncDatabase in Frameworks */, - 5138E93A24D33E5600AFF0FE /* RSTree in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1050,8 +1051,8 @@ 848E84D02DB749440023F3BA /* RSCoreResources in Frameworks */, 513277642590FC640064F1E7 /* SyncDatabase in Frameworks */, 17192ADA2567B3D500AAEACA /* RSSparkle in Frameworks */, - 514C16DE24D2EF15009A3AFA /* RSTree in Frameworks */, 5132775E2590FC640064F1E7 /* Articles in Frameworks */, + 84EE3F122DB8A088009D3A8D /* RSTree in Frameworks */, 513277612590FC640064F1E7 /* ArticlesDatabase in Frameworks */, 51C4CFF624D37DD500AF9874 /* Secrets in Frameworks */, 179C39EA26F76B0500D4E741 /* Zip in Frameworks */, @@ -1534,6 +1535,7 @@ 51CD32C724D2E06C009ABAEF /* Secrets */, 51CD32A824D2CB25009ABAEF /* SyncDatabase */, 8424B3162DB73D320053AA11 /* RSParser */, + 84EE3F102DB8A078009D3A8D /* RSTree */, 843E2F152CF2B43700ED170F /* RSWeb */, 848E84CB2DB749300023F3BA /* RSCore */, ); @@ -1908,8 +1910,8 @@ name = "NetNewsWire iOS Intents Extension"; packageProductDependencies = ( 51BC2F4A24D343A500E90810 /* Account */, - 51BC2F4C24D343AB00E90810 /* RSTree */, 848E84DA2DB749860023F3BA /* RSCore */, + 84EE3F1A2DB8A0B6009D3A8D /* RSTree */, ); productName = "NetNewsWire iOS Intents Extension"; productReference = 51314637235A7BBE00387FDC /* NetNewsWire iOS Intents Extension.appex */; @@ -1932,8 +1934,8 @@ name = "NetNewsWire iOS Share Extension"; packageProductDependencies = ( 51BC2F3724D3439A00E90810 /* Account */, - 51BC2F4724D3439E00E90810 /* RSTree */, 848E84D72DB749720023F3BA /* RSCore */, + 84EE3F172DB8A0AC009D3A8D /* RSTree */, ); productName = "NetNewsWire iOS Share Extension"; productReference = 513C5CE6232571C2003D4054 /* NetNewsWire iOS Share Extension.appex */; @@ -1998,7 +2000,6 @@ name = "NetNewsWire-iOS"; packageProductDependencies = ( 516B695E24D2F33B00B5702F /* Account */, - 5138E93924D33E5600AFF0FE /* RSTree */, 5138E94B24D3417A00AFF0FE /* RSDatabase */, 513F32702593EE6F0003048F /* Articles */, 513F32732593EE6F0003048F /* ArticlesDatabase */, @@ -2008,6 +2009,7 @@ 843E2F192CF2B8C500ED170F /* RSWeb */, 8424B31A2DB73D530053AA11 /* RSParser */, 848E84D42DB749670023F3BA /* RSCore */, + 84EE3F142DB8A0A0009D3A8D /* RSTree */, ); productName = "NetNewsWire-iOS"; productReference = 840D617C2029031C009BC708 /* NetNewsWire.app */; @@ -2037,7 +2039,6 @@ name = NetNewsWire; packageProductDependencies = ( 514C16CD24D2E63F009A3AFA /* Account */, - 514C16DD24D2EF15009A3AFA /* RSTree */, 51C4CFF524D37DD500AF9874 /* Secrets */, 51A737BE24DB197F0015FA66 /* RSDatabase */, 17192AD92567B3D500AAEACA /* RSSparkle */, @@ -2050,6 +2051,7 @@ 8424B3172DB73D4C0053AA11 /* RSParser */, 848E84CC2DB749440023F3BA /* RSCore */, 848E84CF2DB749440023F3BA /* RSCoreResources */, + 84EE3F112DB8A088009D3A8D /* RSTree */, ); productName = NetNewsWire; productReference = 849C64601ED37A5D003D8FC0 /* NetNewsWire.app */; @@ -2154,7 +2156,6 @@ ); mainGroup = 849C64571ED37A5D003D8FC0; packageReferences = ( - 510ECA4024D1DCD0001C31A6 /* XCRemoteSwiftPackageReference "RSTree" */, 51B0DF0D24D24E3B000AD99E /* XCRemoteSwiftPackageReference "RSDatabase" */, 17192AD82567B3D500AAEACA /* XCRemoteSwiftPackageReference "Sparkle-Binary" */, 519CA8E325841DB700EB079A /* XCRemoteSwiftPackageReference "plcrashreporter" */, @@ -3185,14 +3186,6 @@ revision = 059e7346082d02de16220cd79df7db18ddeba8c3; }; }; - 510ECA4024D1DCD0001C31A6 /* XCRemoteSwiftPackageReference "RSTree" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/Ranchero-Software/RSTree.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 1.0.0; - }; - }; 519CA8E325841DB700EB079A /* XCRemoteSwiftPackageReference "plcrashreporter" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/microsoft/plcrashreporter.git"; @@ -3247,11 +3240,6 @@ isa = XCSwiftPackageProductDependency; productName = SyncDatabase; }; - 5138E93924D33E5600AFF0FE /* RSTree */ = { - isa = XCSwiftPackageProductDependency; - package = 510ECA4024D1DCD0001C31A6 /* XCRemoteSwiftPackageReference "RSTree" */; - productName = RSTree; - }; 5138E94B24D3417A00AFF0FE /* RSDatabase */ = { isa = XCSwiftPackageProductDependency; package = 51B0DF0D24D24E3B000AD99E /* XCRemoteSwiftPackageReference "RSDatabase" */; @@ -3277,11 +3265,6 @@ isa = XCSwiftPackageProductDependency; productName = Account; }; - 514C16DD24D2EF15009A3AFA /* RSTree */ = { - isa = XCSwiftPackageProductDependency; - package = 510ECA4024D1DCD0001C31A6 /* XCRemoteSwiftPackageReference "RSTree" */; - productName = RSTree; - }; 516B695E24D2F33B00B5702F /* Account */ = { isa = XCSwiftPackageProductDependency; productName = Account; @@ -3300,20 +3283,10 @@ isa = XCSwiftPackageProductDependency; productName = Account; }; - 51BC2F4724D3439E00E90810 /* RSTree */ = { - isa = XCSwiftPackageProductDependency; - package = 510ECA4024D1DCD0001C31A6 /* XCRemoteSwiftPackageReference "RSTree" */; - productName = RSTree; - }; 51BC2F4A24D343A500E90810 /* Account */ = { isa = XCSwiftPackageProductDependency; productName = Account; }; - 51BC2F4C24D343AB00E90810 /* RSTree */ = { - isa = XCSwiftPackageProductDependency; - package = 510ECA4024D1DCD0001C31A6 /* XCRemoteSwiftPackageReference "RSTree" */; - productName = RSTree; - }; 51C4CFF524D37DD500AF9874 /* Secrets */ = { isa = XCSwiftPackageProductDependency; productName = Secrets; @@ -3366,6 +3339,22 @@ isa = XCSwiftPackageProductDependency; productName = RSCore; }; + 84EE3F112DB8A088009D3A8D /* RSTree */ = { + isa = XCSwiftPackageProductDependency; + productName = RSTree; + }; + 84EE3F142DB8A0A0009D3A8D /* RSTree */ = { + isa = XCSwiftPackageProductDependency; + productName = RSTree; + }; + 84EE3F172DB8A0AC009D3A8D /* RSTree */ = { + isa = XCSwiftPackageProductDependency; + productName = RSTree; + }; + 84EE3F1A2DB8A0B6009D3A8D /* RSTree */ = { + isa = XCSwiftPackageProductDependency; + productName = RSTree; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 849C64581ED37A5D003D8FC0 /* Project object */; diff --git a/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 208b08cf1..66607425a 100644 --- a/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -19,15 +19,6 @@ "version": "1.0.0" } }, - { - "package": "RSTree", - "repositoryURL": "https://github.com/Ranchero-Software/RSTree.git", - "state": { - "branch": null, - "revision": "9d051f42cfc4faa991fd79cdb32e4cc8c545e334", - "version": "1.0.0" - } - }, { "package": "RSSparkle", "repositoryURL": "https://github.com/Ranchero-Software/Sparkle-Binary.git", diff --git a/RSTree/Package.swift b/RSTree/Package.swift new file mode 100644 index 000000000..4079adec4 --- /dev/null +++ b/RSTree/Package.swift @@ -0,0 +1,18 @@ +// swift-tools-version:5.10 +import PackageDescription + +let package = Package( + name: "RSTree", + platforms: [.macOS(.v13), .iOS(.v17)], + products: [ + .library( + name: "RSTree", + type: .dynamic, + targets: ["RSTree"]), + ], + targets: [ + .target( + name: "RSTree", + dependencies: []), + ] +) diff --git a/RSTree/Sources/RSTree/NSOutlineView+RSTree.swift b/RSTree/Sources/RSTree/NSOutlineView+RSTree.swift new file mode 100644 index 000000000..bd26475c9 --- /dev/null +++ b/RSTree/Sources/RSTree/NSOutlineView+RSTree.swift @@ -0,0 +1,58 @@ +// +// NSOutlineView+RSTree.swift +// RSTree +// +// Created by Brent Simmons on 9/5/16. +// Copyright © 2016 Ranchero Software, LLC. All rights reserved. +// + +#if os(OSX) + +import AppKit + +public extension NSOutlineView { + + @discardableResult + func revealAndSelectNodeAtPath(_ nodePath: NodePath) -> Bool { + + // Returns true on success. Expands folders on the way. May succeed partially (returns false, in that case). + + let numberOfNodes = nodePath.components.count + if numberOfNodes < 2 { + return false + } + + let indexOfNodeToSelect = numberOfNodes - 1 + + for i in 1...indexOfNodeToSelect { // Start at 1 to skip root node. + + let oneNode = nodePath.components[i] + let oneRow = row(forItem: oneNode) + if oneRow < 0 { + return false + } + + if i == indexOfNodeToSelect { + selectRowIndexes(NSIndexSet(index: oneRow) as IndexSet, byExtendingSelection: false) + scrollRowToVisible(oneRow) + return true + } + else { + expandItem(oneNode) + } + } + + return false + } + + @discardableResult + func revealAndSelectRepresentedObject(_ representedObject: AnyObject, _ treeController: TreeController) -> Bool { + + guard let nodePath = NodePath(representedObject: representedObject, treeController: treeController) else { + return false + } + return revealAndSelectNodeAtPath(nodePath) + } +} + +#endif diff --git a/RSTree/Sources/RSTree/Node.swift b/RSTree/Sources/RSTree/Node.swift new file mode 100644 index 000000000..0c70265fe --- /dev/null +++ b/RSTree/Sources/RSTree/Node.swift @@ -0,0 +1,224 @@ +// +// Node.swift +// NetNewsWire +// +// Created by Brent Simmons on 7/21/15. +// Copyright © 2015 Ranchero Software, LLC. All rights reserved. +// + +import Foundation + +// Main thread only. + +public final class Node: Hashable { + + public weak var parent: Node? + public let representedObject: AnyObject + public var canHaveChildNodes = false + public var isGroupItem = false + public var childNodes = [Node]() + public let uniqueID: Int + private static var incrementingID = 0 + + public var isRoot: Bool { + if let _ = parent { + return false + } + return true + } + + public var numberOfChildNodes: Int { + return childNodes.count + } + + public var indexPath: IndexPath { + if let parent = parent { + let parentPath = parent.indexPath + if let childIndex = parent.indexOfChild(self) { + return parentPath.appending(childIndex) + } + preconditionFailure("A Node’s parent must contain it as a child.") + } + return IndexPath(index: 0) //root node + } + + public var level: Int { + if let parent = parent { + return parent.level + 1 + } + return 0 + } + + public var isLeaf: Bool { + return numberOfChildNodes < 1 + } + + public init(representedObject: AnyObject, parent: Node?) { + + precondition(Thread.isMainThread) + + self.representedObject = representedObject + self.parent = parent + + self.uniqueID = Node.incrementingID + Node.incrementingID += 1 + } + + public class func genericRootNode() -> Node { + + let node = Node(representedObject: TopLevelRepresentedObject(), parent: nil) + node.canHaveChildNodes = true + return node + } + + public func existingOrNewChildNode(with representedObject: AnyObject) -> Node { + + if let node = childNodeRepresentingObject(representedObject) { + return node + } + return createChildNode(representedObject) + } + + public func createChildNode(_ representedObject: AnyObject) -> Node { + + // Just creates — doesn’t add it. + return Node(representedObject: representedObject, parent: self) + } + + public func childAtIndex(_ index: Int) -> Node? { + + if index >= childNodes.count || index < 0 { + return nil + } + return childNodes[index] + } + + public func indexOfChild(_ node: Node) -> Int? { + + return childNodes.firstIndex{ (oneChildNode) -> Bool in + oneChildNode === node + } + } + + public func childNodeRepresentingObject(_ obj: AnyObject) -> Node? { + return findNodeRepresentingObject(obj, recursively: false) + } + + public func descendantNodeRepresentingObject(_ obj: AnyObject) -> Node? { + return findNodeRepresentingObject(obj, recursively: true) + } + + public func descendantNode(where test: (Node) -> Bool) -> Node? { + return findNode(where: test, recursively: true) + } + + public func hasAncestor(in nodes: [Node]) -> Bool { + + for node in nodes { + if node.isAncestor(of: self) { + return true + } + } + return false + } + + public func isAncestor(of node: Node) -> Bool { + + if node == self { + return false + } + + var nomad = node + while true { + guard let parent = nomad.parent else { + return false + } + if parent == self { + return true + } + nomad = parent + } + } + + public class func nodesOrganizedByParent(_ nodes: [Node]) -> [Node: [Node]] { + + let nodesWithParents = nodes.filter { $0.parent != nil } + return Dictionary(grouping: nodesWithParents, by: { $0.parent! }) + } + + public class func indexSetsGroupedByParent(_ nodes: [Node]) -> [Node: IndexSet] { + + let d = nodesOrganizedByParent(nodes) + let indexSetDictionary = d.mapValues { (nodes) -> IndexSet in + + var indexSet = IndexSet() + if nodes.isEmpty { + return indexSet + } + + let parent = nodes.first!.parent! + for node in nodes { + if let index = parent.indexOfChild(node) { + indexSet.insert(index) + } + } + + return indexSet + } + + return indexSetDictionary + } + + // MARK: - Hashable + + public func hash(into hasher: inout Hasher) { + hasher.combine(uniqueID) + } + + // MARK: - Equatable + + public class func ==(lhs: Node, rhs: Node) -> Bool { + return lhs === rhs + } +} + + +public extension Array where Element == Node { + + func representedObjects() -> [AnyObject] { + + return self.map{ $0.representedObject } + } +} + +private extension Node { + + func findNodeRepresentingObject(_ obj: AnyObject, recursively: Bool = false) -> Node? { + + for childNode in childNodes { + if childNode.representedObject === obj { + return childNode + } + if recursively, let foundNode = childNode.descendantNodeRepresentingObject(obj) { + return foundNode + } + } + + return nil + } + + func findNode(where test: (Node) -> Bool, recursively: Bool = false) -> Node? { + + for childNode in childNodes { + if test(childNode) { + return childNode + } + if recursively, let foundNode = childNode.findNode(where: test, recursively: recursively) { + return foundNode + } + } + + return nil + } + +} diff --git a/RSTree/Sources/RSTree/NodePath.swift b/RSTree/Sources/RSTree/NodePath.swift new file mode 100644 index 000000000..1e9fee5d9 --- /dev/null +++ b/RSTree/Sources/RSTree/NodePath.swift @@ -0,0 +1,42 @@ +// +// NodePath.swift +// RSTree +// +// Created by Brent Simmons on 9/5/16. +// Copyright © 2016 Ranchero Software, LLC. All rights reserved. +// + +import Foundation + +public struct NodePath { + + let components: [Node] + + public init(node: Node) { + + var tempArray = [node] + + var nomad: Node = node + while true { + if let parent = nomad.parent { + tempArray.append(parent) + nomad = parent + } + else { + break + } + } + + self.components = tempArray.reversed() + } + + public init?(representedObject: AnyObject, treeController: TreeController) { + + if let node = treeController.nodeInTreeRepresentingObject(representedObject) { + self.init(node: node) + } + else { + return nil + } + } +} diff --git a/RSTree/Sources/RSTree/RSTree.swift b/RSTree/Sources/RSTree/RSTree.swift new file mode 100644 index 000000000..ac0aa000a --- /dev/null +++ b/RSTree/Sources/RSTree/RSTree.swift @@ -0,0 +1,3 @@ +struct RSTree { + var text = "Hello, World!" +} diff --git a/RSTree/Sources/RSTree/TopLevelRepresentedObject.swift b/RSTree/Sources/RSTree/TopLevelRepresentedObject.swift new file mode 100644 index 000000000..ec4619a7b --- /dev/null +++ b/RSTree/Sources/RSTree/TopLevelRepresentedObject.swift @@ -0,0 +1,15 @@ +// +// TopLevelRepresentedObject.swift +// RSTree +// +// Created by Brent Simmons on 8/10/16. +// Copyright © 2016 Ranchero Software, LLC. All rights reserved. +// + +import Foundation + +// Handy to use as the represented object for a root node. Not required to use it, though. + +final class TopLevelRepresentedObject { + +} diff --git a/RSTree/Sources/RSTree/TreeController.swift b/RSTree/Sources/RSTree/TreeController.swift new file mode 100644 index 000000000..6475f9a60 --- /dev/null +++ b/RSTree/Sources/RSTree/TreeController.swift @@ -0,0 +1,135 @@ +// +// TreeController.swift +// NetNewsWire +// +// Created by Brent Simmons on 5/29/16. +// Copyright © 2016 Ranchero Software, LLC. All rights reserved. +// + +import Foundation + +public protocol TreeControllerDelegate: class { + + func treeController(treeController: TreeController, childNodesFor: Node) -> [Node]? +} + +public typealias NodeVisitBlock = (_ : Node) -> Void + +public final class TreeController { + + private weak var delegate: TreeControllerDelegate? + public let rootNode: Node + + public init(delegate: TreeControllerDelegate, rootNode: Node) { + + self.delegate = delegate + self.rootNode = rootNode + rebuild() + } + + public convenience init(delegate: TreeControllerDelegate) { + + self.init(delegate: delegate, rootNode: Node.genericRootNode()) + } + + @discardableResult + public func rebuild() -> Bool { + + // Rebuild and re-sort. Return true if any changes in the entire tree. + + return rebuildChildNodes(node: rootNode) + } + + public func visitNodes(_ visitBlock: NodeVisitBlock) { + + visitNode(rootNode, visitBlock) + } + + public func nodeInArrayRepresentingObject(nodes: [Node], representedObject: AnyObject, recurse: Bool = false) -> Node? { + + for oneNode in nodes { + + if oneNode.representedObject === representedObject { + return oneNode + } + + if recurse, oneNode.canHaveChildNodes { + if let foundNode = nodeInArrayRepresentingObject(nodes: oneNode.childNodes, representedObject: representedObject, recurse: recurse) { + return foundNode + } + + } + } + return nil + } + + public func nodeInTreeRepresentingObject(_ representedObject: AnyObject) -> Node? { + + return nodeInArrayRepresentingObject(nodes: [rootNode], representedObject: representedObject, recurse: true) + } + + public func normalizedSelectedNodes(_ nodes: [Node]) -> [Node] { + + // An array of nodes might include a leaf node and its parent. Remove the leaf node. + + var normalizedNodes = [Node]() + + for node in nodes { + if !node.hasAncestor(in: nodes) { + normalizedNodes += [node] + } + } + + return normalizedNodes + } +} + +private extension TreeController { + + func visitNode(_ node: Node, _ visitBlock: NodeVisitBlock) { + + visitBlock(node) + node.childNodes.forEach{ (oneChildNode) in + visitNode(oneChildNode, visitBlock) + } + } + + func nodeArraysAreEqual(_ nodeArray1: [Node]?, _ nodeArray2: [Node]?) -> Bool { + + if nodeArray1 == nil && nodeArray2 == nil { + return true + } + if nodeArray1 != nil && nodeArray2 == nil { + return false + } + if nodeArray1 == nil && nodeArray2 != nil { + return false + } + + return nodeArray1! == nodeArray2! + } + + func rebuildChildNodes(node: Node) -> Bool { + + if !node.canHaveChildNodes { + return false + } + + var childNodesDidChange = false + + let childNodes = delegate?.treeController(treeController: self, childNodesFor: node) ?? [Node]() + + childNodesDidChange = !nodeArraysAreEqual(childNodes, node.childNodes) + if (childNodesDidChange) { + node.childNodes = childNodes + } + + childNodes.forEach{ (oneChildNode) in + if rebuildChildNodes(node: oneChildNode) { + childNodesDidChange = true + } + } + + return childNodesDidChange + } +}