From 78eb2694217d65e9af64608af22e16507aae55d2 Mon Sep 17 00:00:00 2001 From: Drew Volz Date: Sun, 25 Feb 2024 22:43:30 -0800 Subject: [PATCH] Filter scoping and file path status bar view (#18) * scope selection to search current dump or all dumps * filepath statusbar on code editor to identify origin * hover text on file row to identify origin when searching all --- ClassDumper.xcodeproj/project.pbxproj | 8 ++++ ClassDumper/AppView.swift | 7 ++++ ClassDumper/Extension/AppDelegate.swift | 4 +- ClassDumper/Shared/Constants.swift | 20 +++++++++ ClassDumper/Shared/Keys.swift | 6 +++ ClassDumper/Views/FileContentsView.swift | 8 ++++ ClassDumper/Views/FilePathView.swift | 43 ++++++++++++++++++++ ClassDumper/Views/FileRowView.swift | 40 +++++++++++------- ClassDumper/Views/FilterScopeView.swift | 28 +++++++++++++ ClassDumperUITests/ClassDumperUITests.swift | 7 ++++ ClassDumperUITests/ImportFlow.swift | 45 ++++++++++++++++++++- 11 files changed, 200 insertions(+), 16 deletions(-) create mode 100644 ClassDumper/Views/FilePathView.swift create mode 100644 ClassDumper/Views/FilterScopeView.swift diff --git a/ClassDumper.xcodeproj/project.pbxproj b/ClassDumper.xcodeproj/project.pbxproj index a8a72e6..c8a6758 100644 --- a/ClassDumper.xcodeproj/project.pbxproj +++ b/ClassDumper.xcodeproj/project.pbxproj @@ -19,6 +19,8 @@ 3A2F8F932A46B3D700D9FB26 /* Files in Frameworks */ = {isa = PBXBuildFile; productRef = 3A2F8F922A46B3D700D9FB26 /* Files */; }; 3A2F8F952A4D4C5A00D9FB26 /* FolderRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2F8F942A4D4C5A00D9FB26 /* FolderRowView.swift */; }; 3A5863712B84564F00487B13 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A5863702B84564F00487B13 /* URL.swift */; }; + 3A5863F02B8C069C00487B13 /* FilterScopeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A5863EF2B8C069C00487B13 /* FilterScopeView.swift */; }; + 3A5863F22B8C14A400487B13 /* FilePathView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A5863F12B8C14A400487B13 /* FilePathView.swift */; }; 3A67D7642A511F7600516FF2 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A67D7632A511F7600516FF2 /* App.swift */; }; 3A67D7662A511FFA00516FF2 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A67D7652A511FFA00516FF2 /* AppDelegate.swift */; }; 3A67D7682A51566B00516FF2 /* String+ConsoleOutput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A67D7672A51566B00516FF2 /* String+ConsoleOutput.swift */; }; @@ -91,6 +93,8 @@ 3A2F8F912A46B3C500D9FB26 /* Files */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Files; path = Packages/Files; sourceTree = ""; }; 3A2F8F942A4D4C5A00D9FB26 /* FolderRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FolderRowView.swift; sourceTree = ""; }; 3A5863702B84564F00487B13 /* URL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URL.swift; sourceTree = ""; }; + 3A5863EF2B8C069C00487B13 /* FilterScopeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterScopeView.swift; sourceTree = ""; }; + 3A5863F12B8C14A400487B13 /* FilePathView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilePathView.swift; sourceTree = ""; }; 3A67D7632A511F7600516FF2 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = ""; }; 3A67D7652A511FFA00516FF2 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 3A67D7672A51566B00516FF2 /* String+ConsoleOutput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+ConsoleOutput.swift"; sourceTree = ""; }; @@ -180,6 +184,8 @@ children = ( 3A2F8F942A4D4C5A00D9FB26 /* FolderRowView.swift */, 3A2F8F6D2A397BAF00D9FB26 /* FileRowView.swift */, + 3A5863EF2B8C069C00487B13 /* FilterScopeView.swift */, + 3A5863F12B8C14A400487B13 /* FilePathView.swift */, 3A67D7772A56542300516FF2 /* FileContentsView.swift */, 3A2F8F702A397CF100D9FB26 /* DatabaseButtons.swift */, 3A2F8F722A397CF100D9FB26 /* InformationStyle.swift */, @@ -458,9 +464,11 @@ 3A67D7662A511FFA00516FF2 /* AppDelegate.swift in Sources */, 3A67D7722A54913100516FF2 /* GeneralTabView.swift in Sources */, 3A67D7782A56542300516FF2 /* FileContentsView.swift in Sources */, + 3A5863F02B8C069C00487B13 /* FilterScopeView.swift in Sources */, 3A2F8F952A4D4C5A00D9FB26 /* FolderRowView.swift in Sources */, 3A67D7762A549F6700516FF2 /* Styles.swift in Sources */, 3AA565492A25BD6F00EBD7FB /* String.swift in Sources */, + 3A5863F22B8C14A400487B13 /* FilePathView.swift in Sources */, 3A2F8F692A397A6B00D9FB26 /* Database.swift in Sources */, 3A2F8F6B2A397ABB00D9FB26 /* AppView.swift in Sources */, 3AAED1F72A3975490036217E /* Database+SwiftUI.swift in Sources */, diff --git a/ClassDumper/AppView.swift b/ClassDumper/AppView.swift index d51a663..0cdc9ed 100644 --- a/ClassDumper/AppView.swift +++ b/ClassDumper/AppView.swift @@ -32,6 +32,13 @@ struct AppView: View { } detail: { + } + .toolbar { + if fileCount != 0 { + ToolbarItemGroup(placement: .navigation) { + FilterScopeView() + } + } } .onReceive(NotificationCenter.default.publisher(for: Notification.Name.folderSelectedFromFinderNotification)) { _ in parseDirectory() diff --git a/ClassDumper/Extension/AppDelegate.swift b/ClassDumper/Extension/AppDelegate.swift index c77cb0e..437b2a6 100644 --- a/ClassDumper/Extension/AppDelegate.swift +++ b/ClassDumper/Extension/AppDelegate.swift @@ -8,7 +8,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate { } func resetState() { - UserDefaults.standard.removePersistentDomain(forName: PlistKey.Identifier.rawValue) + if let bundleID = Bundle.main.bundleIdentifier { + UserDefaults.standard.removePersistentDomain(forName: bundleID) + } } } } diff --git a/ClassDumper/Shared/Constants.swift b/ClassDumper/Shared/Constants.swift index 92feed1..9890025 100644 --- a/ClassDumper/Shared/Constants.swift +++ b/ClassDumper/Shared/Constants.swift @@ -14,5 +14,25 @@ struct Preferences { // source code viewer static var themeName: CodeEditor.ThemeName = .default static var fontSize: Int = Int(NSFont.systemFontSize) + + // scoped search view + static var scopedSearch = Preferences.FilterScope.default + } +} + + +extension Preferences { + struct FilterScope: TypedString { + public let rawValue : String + + @inlinable + public init(rawValue: String) { self.rawValue = rawValue } + + static var `default` = Preferences.FilterScope(rawValue: "scoped") + static var all = Preferences.FilterScope(rawValue: "all") + + static var allCases: [FilterScope] { + return [.default, .all] + } } } diff --git a/ClassDumper/Shared/Keys.swift b/ClassDumper/Shared/Keys.swift index 2c8e430..6c62aa8 100644 --- a/ClassDumper/Shared/Keys.swift +++ b/ClassDumper/Shared/Keys.swift @@ -13,5 +13,11 @@ struct Keys { struct Detail { static let CodeViewer = "CodeViewer" + static let PathBar = "PathBar" + static let PathBarFile = "PathBarFile" + } + + struct Filters { + static let FilterFiles = "FilterFiles" } } diff --git a/ClassDumper/Views/FileContentsView.swift b/ClassDumper/Views/FileContentsView.swift index 8dce96c..f139a07 100644 --- a/ClassDumper/Views/FileContentsView.swift +++ b/ClassDumper/Views/FileContentsView.swift @@ -6,6 +6,8 @@ struct DetailView: View { @AppStorage("codeViewerFontSize") var fontSize: Int = Preferences.Defaults.fontSize var fileContents: String + var folderName: String + var fileName: String var body: some View { CodeEditor(source: fileContents, @@ -15,5 +17,11 @@ struct DetailView: View { set: { fontSize = Int($0) }), flags: [.defaultViewerFlags]) .accessibilityIdentifier(Keys.Detail.CodeViewer) + + FilePathView(folderName: folderName, fileName: fileName) } } + +#Preview { + DetailView(fileContents: "File contents", folderName: "TestApp", fileName: "TestFile.Swift") +} diff --git a/ClassDumper/Views/FilePathView.swift b/ClassDumper/Views/FilePathView.swift new file mode 100644 index 0000000..8c1b195 --- /dev/null +++ b/ClassDumper/Views/FilePathView.swift @@ -0,0 +1,43 @@ +import SwiftUI + +struct FilePathView: View { + var folderName: String + var fileName: String + + var body: some View { + ScrollView(.horizontal) { + HStack { + HStack(spacing: 4) { + Image(systemName: "folder.fill") + .foregroundColor(.accentColor) + Text(folderName) + } + + Image(systemName: "chevron.right") + .imageScale(.small) + + HStack(spacing: 4) { + Image(systemName: "doc.fill") + .foregroundColor(.accentColor) + Text(fileName) + .accessibilityIdentifier(Keys.Detail.PathBarFile) + } + } + .padding(.horizontal, 15) + .padding(.bottom, 5) + .frame(maxWidth: .infinity, alignment: .leading) + } + .accessibilityIdentifier(Keys.Detail.PathBar) + } +} + +#Preview { + FilePathView(folderName: "Test App", fileName: "TestFile.swift") +} + +#Preview { + FilePathView( + folderName: "Test App", + fileName: "TestFileWithReallyLongNameThatShouldTruncateInTheMiddleIfItGetsLongerThanExpected.swift" + ) +} diff --git a/ClassDumper/Views/FileRowView.swift b/ClassDumper/Views/FileRowView.swift index 88b438a..1e2be1d 100644 --- a/ClassDumper/Views/FileRowView.swift +++ b/ClassDumper/Views/FileRowView.swift @@ -23,8 +23,22 @@ struct FileRowRequest: Queryable { } struct FileRowView: View { + @AppStorage("scopedSearchPreference") var scopedSearchPreference = Preferences.Defaults.scopedSearch + + // query to fetch all data @Query(FileRowRequest()) - var fileRows: FileRowResponse + var allFileRows: FileRowResponse + + // filtering the active directory + var filteredFileRows: FileRowResponse { + allFileRows.filter({$0.1 == selectedFolder}) + } + + // data to render based upon scoped preference + var fileRows: FileRowResponse { + scopedSearchPreference == .all ? allFileRows : filteredFileRows + } + var selectedFolder: String @State private var searchText = "" @@ -42,23 +56,21 @@ struct FileRowView: View { var body: some View { List(filteredFileNames, id:\.0) { fileId, folderName, fileName, content in - if folderName == selectedFolder { - NavigationLink(destination: DetailView(fileContents: content)) { - Label { - Text(fileName) - .truncationMode(.middle) - .lineLimit(1) - } icon: { - Image(systemName: "doc") - .symbolVariant(.fill) - .foregroundColor(.accentColor) - } + NavigationLink(destination: DetailView(fileContents: content, folderName: folderName, fileName: fileName)) { + Label { + Text(fileName) + .truncationMode(.middle) + .lineLimit(1) + } icon: { + Image(systemName: "doc") + .symbolVariant(.fill) + .foregroundColor(.accentColor) } - .accessibilityIdentifier(Keys.Middle.Row) } + .accessibilityIdentifier(Keys.Middle.Row) + .help(scopedSearchPreference == .all ? folderName : "") } .accessibilityIdentifier(Keys.Middle.List) .searchable(text: $searchText, prompt: "Search files") } } - diff --git a/ClassDumper/Views/FilterScopeView.swift b/ClassDumper/Views/FilterScopeView.swift new file mode 100644 index 0000000..168b937 --- /dev/null +++ b/ClassDumper/Views/FilterScopeView.swift @@ -0,0 +1,28 @@ +import SwiftUI + +struct FilterScopeView: View { + @AppStorage("scopedSearchPreference") var scopedSearchPreference = Preferences.Defaults.scopedSearch + + func getFilterText(_ filter: Preferences.FilterScope) -> String { + switch filter { + case .default: "Show selected" + case .all: "Show all" + default: "Unhandled filter" + } + } + + var body: some View { + Picker("", selection: $scopedSearchPreference) { + ForEach(Preferences.FilterScope.allCases) { filter in + Text(getFilterText(filter)).tag(filter) + } + } + .accessibilityIdentifier(Keys.Filters.FilterFiles) + .pickerStyle(.menu) + .fixedSize() + } +} + +#Preview { + FilterScopeView() +} diff --git a/ClassDumperUITests/ClassDumperUITests.swift b/ClassDumperUITests/ClassDumperUITests.swift index ace0bda..a612f1a 100644 --- a/ClassDumperUITests/ClassDumperUITests.swift +++ b/ClassDumperUITests/ClassDumperUITests.swift @@ -21,6 +21,7 @@ final class ClassDumperUITests: UITestCase { .resetState() .check(.folder, exists: false) .check(.file, exists: false) + .check(.filterToggle, exists: false) .openApp(named: "Automator") .tapFirst(.folder, containing: "class-dump") .check(.folder, exists: true) @@ -29,5 +30,11 @@ final class ClassDumperUITests: UITestCase { .tapFirst(.file, containing: "CDClassDumpVisitor.h") .check(.code, exists: true) .tapFirst(.code, containing: "CDTextClassDumpVisitor.h") + .check(.pathbar, exists: true) + .tapFirst(.pathbar, containing: "CDClassDumpVisitor.h") + .check(.filterToggle, exists: true) + .tapFirst(.filterToggle, containing: "Show selected") + // TODO: CI is failing this test although it is working locally + // .selectPopupButton("Show all") } } diff --git a/ClassDumperUITests/ImportFlow.swift b/ClassDumperUITests/ImportFlow.swift index 2532016..ff65268 100644 --- a/ClassDumperUITests/ImportFlow.swift +++ b/ClassDumperUITests/ImportFlow.swift @@ -12,12 +12,16 @@ struct ImportFlow: Screen { case navbar case content case detail + case filter + } enum Component { case folder case file case code + case pathbar + case filterToggle } struct TestComponent { @@ -47,6 +51,20 @@ struct ImportFlow: Screen { section: .detail, component: app.scrollViews[Keys.Detail.CodeViewer]) } + + var pathbar: TestComponent { + TestComponent(id: Keys.Detail.PathBar, + rowId: "", + section: .detail, + component: app.scrollViews[Keys.Detail.PathBar]) + } + + var filterToggle: TestComponent { + TestComponent(id: Keys.Filters.FilterFiles, + rowId: "", + section: .filter, + component: app.popUpButtons[Keys.Filters.FilterFiles]) + } func getComponent(for element: Component) -> TestComponent { switch element { @@ -56,6 +74,10 @@ struct ImportFlow: Screen { return filelist case .code: return codeviewer + case .pathbar: + return pathbar + case .filterToggle: + return filterToggle } } @@ -75,13 +97,23 @@ struct ImportFlow: Screen { func tapFirst(_ element: Component, containing: String) -> Self { let forElement = getComponent(for: element) - tapFirstRow(label: containing, + tapFirstRow(element, + label: containing, parent: forElement.component, target: forElement.section, rowId: forElement.rowId) return self } + + @discardableResult + func selectPopupButton(_ containing: String) -> Self { + let firstPredicate = NSPredicate(format: "title BEGINSWITH '\(containing)'") + let desiredOption = filterToggle.component.menuItems.element(matching: firstPredicate) + desiredOption.tap() + + return self + } func resetState() -> Self { open(.settings) @@ -113,6 +145,7 @@ struct ImportFlow: Screen { @discardableResult private func tapFirstRow( + _ component: Component, label: String, parent: XCUIElement, target: Section, @@ -133,12 +166,22 @@ struct ImportFlow: Screen { break case .detail: row = parent.textViews + + if component == .pathbar { + row = parent.staticTexts.matching(identifier: Keys.Detail.PathBarFile) + } + guard let found = row.firstMatch.value as? String else { fatalError("Could not tap or locate: {label:\(label), parent:\(parent), target: \(target), row: \(rowId)") } XCTAssertTrue(found.contains(label)) row.firstMatch.tap() break + case .filter: + row = app.popUpButtons + XCTAssertTrue(row[Keys.Filters.FilterFiles].value as? String == label) + row.firstMatch.tap() + break } return self