From 11a230bed129f2b78a2ab7c546e83ff508a0ef8a Mon Sep 17 00:00:00 2001 From: Mikhail Rubanov Date: Sat, 3 Dec 2022 07:03:27 +0500 Subject: [PATCH 1/4] Add simple dragging --- ...extRepresentationController+Dragging.swift | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/VoiceOver Designer/Features/Sources/TextUI/TextRepresentationController+Dragging.swift b/VoiceOver Designer/Features/Sources/TextUI/TextRepresentationController+Dragging.swift index b0378e64..74fa901f 100644 --- a/VoiceOver Designer/Features/Sources/TextUI/TextRepresentationController+Dragging.swift +++ b/VoiceOver Designer/Features/Sources/TextUI/TextRepresentationController+Dragging.swift @@ -34,10 +34,32 @@ extension TextRepresentationController { public func outlineView(_ outlineView: NSOutlineView, acceptDrop info: NSDraggingInfo, item: Any?, childIndex toIndex: Int) -> Bool { guard toIndex != NSOutlineViewDropOnItemIndex else { return false } // When place over item, check `item` for this case. Will help lately when deal with container + guard let element = draggedNode else { return false } + + let currentParent = outlineView.parent(forItem: draggedNode) as? A11yContainer + +// document.controls.move(element, fromContainer: currentParent, +// toIndex: toIndex, toContainer: item as? A11yContainer) + + if let container = item as? A11yContainer { + if let element = draggedNode { + // Remove from list + if let from = document.controls.firstIndex(where: { control in + control === element + }) { + document.controls.remove(at: from) + } + + // Insert in container + container.elements.insert(element, at: toIndex) + outlineView.reloadData() + return true + } + } + guard document.controls.move(draggedNode!, to: toIndex) else { print("did not move to \(toIndex)") return false - } // outlineView.moveItem(at: fromIndex, inParent: nil, From 37f8f3a3a9b7136e0ab1db2bfa3dc1ec9a2f8d14 Mon Sep 17 00:00:00 2001 From: Mikhail Rubanov Date: Sat, 3 Dec 2022 07:03:56 +0500 Subject: [PATCH 2/4] Add CustomDump --- .../xcshareddata/swiftpm/Package.resolved | 24 ++++++++++++++++--- VoiceOver Designer/Features/Package.swift | 5 +++- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/VoiceOver Designer.xcworkspace/xcshareddata/swiftpm/Package.resolved b/VoiceOver Designer.xcworkspace/xcshareddata/swiftpm/Package.resolved index 8a4f7d89..c6d269e0 100644 --- a/VoiceOver Designer.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/VoiceOver Designer.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,13 +1,31 @@ { "object": { "pins": [ + { + "package": "swift-custom-dump", + "repositoryURL": "git@github.com:pointfreeco/swift-custom-dump.git", + "state": { + "branch": null, + "revision": "ead7d30cc224c3642c150b546f4f1080d1c411a8", + "version": "0.6.1" + } + }, { "package": "swift-snapshot-testing", "repositoryURL": "git@github.com:pointfreeco/swift-snapshot-testing.git", "state": { - "branch": "main", - "revision": "ad2c83170e82954d9504e4db205c43a3f493bc55", - "version": null + "branch": null, + "revision": "f29e2014f6230cf7d5138fc899da51c7f513d467", + "version": "1.10.0" + } + }, + { + "package": "xctest-dynamic-overlay", + "repositoryURL": "https://github.com/pointfreeco/xctest-dynamic-overlay", + "state": { + "branch": null, + "revision": "5a5457a744239896e9b0b03a8e1a5069c3e7b91f", + "version": "0.6.0" } } ] diff --git a/VoiceOver Designer/Features/Package.swift b/VoiceOver Designer/Features/Package.swift index 0314e9db..cb30f9c7 100644 --- a/VoiceOver Designer/Features/Package.swift +++ b/VoiceOver Designer/Features/Package.swift @@ -29,8 +29,10 @@ let package = Package( dependencies: [ .package( url: "git@github.com:pointfreeco/swift-snapshot-testing.git", - branch: "main" + .upToNextMajor(from: "1.10.0") ), + .package(url: "git@github.com:pointfreeco/swift-custom-dump.git", + .upToNextMajor(from: "0.6.1")), .package(name: "Shared", path: "./../../Shared") ], targets: [ @@ -99,6 +101,7 @@ let package = Package( name: "TextUITests", dependencies: [ "TextUI", + .productItem(name: "CustomDump", package: "swift-custom-dump"), .product(name: "Document", package: "Shared"), ] ), From 106ca1b4bffe92a28950711b36c974412c4e1b72 Mon Sep 17 00:00:00 2001 From: Mikhail Rubanov Date: Sat, 3 Dec 2022 11:50:59 +0500 Subject: [PATCH 3/4] =?UTF-8?q?Add=20all=20kinds=20of=20drag=E2=80=99n?= =?UTF-8?q?=E2=80=99drop=20to=20containers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AccessibilityView+Grouping.swift | 8 +- .../Settings/States/Parts/Settings.storyboard | 2 +- .../Features/Sources/TextUI/Array+Move.swift | 75 ++++++++++++ ...extRepresentationController+Dragging.swift | 45 +++----- .../TextUI/TextRepresentationController.swift | 2 +- .../TextUITests/VODocument+MoveTests.swift | 108 +++++++++++++++++- 6 files changed, 205 insertions(+), 35 deletions(-) diff --git a/Shared/Sources/Document/Models/AccessibilityView/AccessibilityView+Grouping.swift b/Shared/Sources/Document/Models/AccessibilityView/AccessibilityView+Grouping.swift index 0ac3803f..71cdf0cd 100644 --- a/Shared/Sources/Document/Models/AccessibilityView/AccessibilityView+Grouping.swift +++ b/Shared/Sources/Document/Models/AccessibilityView/AccessibilityView+Grouping.swift @@ -1,11 +1,13 @@ import Foundation extension Array where Element == any AccessibilityView { + + @discardableResult public mutating func wrapInContainer( _ items: [A11yDescription], label: String - ) { - guard items.count > 0 else { return } + ) -> A11yContainer? { + guard items.count > 0 else { return nil } var extractedElements = [A11yDescription]() @@ -30,6 +32,8 @@ extension Array where Element == any AccessibilityView { insert(container, at: insertIndex ?? 0) removeEmptyContainers() + + return container } /// - Returns: Element index diff --git a/VoiceOver Designer/Features/Sources/Settings/States/Parts/Settings.storyboard b/VoiceOver Designer/Features/Sources/Settings/States/Parts/Settings.storyboard index 96d1de6b..6c30c22a 100644 --- a/VoiceOver Designer/Features/Sources/Settings/States/Parts/Settings.storyboard +++ b/VoiceOver Designer/Features/Sources/Settings/States/Parts/Settings.storyboard @@ -31,7 +31,7 @@ - + diff --git a/VoiceOver Designer/Features/Sources/TextUI/Array+Move.swift b/VoiceOver Designer/Features/Sources/TextUI/Array+Move.swift index bda4ad09..7f369607 100644 --- a/VoiceOver Designer/Features/Sources/TextUI/Array+Move.swift +++ b/VoiceOver Designer/Features/Sources/TextUI/Array+Move.swift @@ -25,3 +25,78 @@ extension Array where Element == any AccessibilityView { return true } } + +extension Array where Element == A11yDescription { + /// - Returns: From and To indexes + @discardableResult + mutating func move(_ element: Element, to: Int) -> Bool { + guard let from = firstIndex(where: { control in + control === element + }) else { return false } + + if to == from + 1 { // Can't move items after themselves + return false + } + + if to == from { // Can't move to same position + return false + } + + remove(at: from) + if to > from { + insert(element, at: to - 1) + } else { + insert(element, at: to) + } + return true + } +} + +extension Array where Element == any AccessibilityView { + /// - Returns: From and To indexes + mutating func move( + _ element: A11yDescription, fromContainer: A11yContainer?, + toIndex: Int, toContainer: A11yContainer? + ) { + if fromContainer == toContainer { + if let fromContainer { + fromContainer.elements.move(element, + to: toIndex) + } else { + move(element, to: toIndex) + } + } + + if let toContainer { + // Insert in container + toContainer.elements.insert(element, at: toIndex) + } else { + insert(element, at: toIndex) + } + + if let fromContainer { + fromContainer.elements.remove(element) + } else { + remove(element) + } + } + + mutating func remove(_ element: Element) { + if let from = firstIndex(where: { control in + control === element + }) { + remove(at: from) + } + } +} + +extension Array where Element == A11yDescription { + mutating func remove(_ element: Element) { + if let from = firstIndex(where: { control in + control === element + }) { + remove(at: from) + } + } +} + diff --git a/VoiceOver Designer/Features/Sources/TextUI/TextRepresentationController+Dragging.swift b/VoiceOver Designer/Features/Sources/TextUI/TextRepresentationController+Dragging.swift index 74fa901f..7b67f284 100644 --- a/VoiceOver Designer/Features/Sources/TextUI/TextRepresentationController+Dragging.swift +++ b/VoiceOver Designer/Features/Sources/TextUI/TextRepresentationController+Dragging.swift @@ -23,7 +23,7 @@ extension TextRepresentationController { } public func outlineView(_ outlineView: NSOutlineView, draggingSession session: NSDraggingSession, willBeginAt screenPoint: NSPoint, forItems draggedItems: [Any]) { - draggedNode = draggedItems[0] as? A11yDescription + draggedNode = draggedItems[0] as? any AccessibilityView session.draggingPasteboard.setData(Data(), forType: REORDER_PASTEBOARD_TYPE) } @@ -32,36 +32,25 @@ extension TextRepresentationController { } public func outlineView(_ outlineView: NSOutlineView, acceptDrop info: NSDraggingInfo, item: Any?, childIndex toIndex: Int) -> Bool { - guard toIndex != NSOutlineViewDropOnItemIndex else { return false } // When place over item, check `item` for this case. Will help lately when deal with container - - guard let element = draggedNode else { return false } - - let currentParent = outlineView.parent(forItem: draggedNode) as? A11yContainer - -// document.controls.move(element, fromContainer: currentParent, -// toIndex: toIndex, toContainer: item as? A11yContainer) - - if let container = item as? A11yContainer { - if let element = draggedNode { - // Remove from list - if let from = document.controls.firstIndex(where: { control in - control === element - }) { - document.controls.remove(at: from) - } - - // Insert in container - container.elements.insert(element, at: toIndex) - outlineView.reloadData() - return true + if toIndex == NSOutlineViewDropOnItemIndex { + guard let onElement = item as? any AccessibilityView + else { + return false } - } - - guard document.controls.move(draggedNode!, to: toIndex) else { - print("did not move to \(toIndex)") + + document.controls.wrapInContainer( + [draggedNode!, onElement].extractElements(), + label: "Container") + return false + } else { + guard let element = draggedNode as? A11yDescription else { return false } // TODO: Move containers + + let currentParent = outlineView.parent(forItem: draggedNode) as? A11yContainer + + document.controls.move(element, fromContainer: currentParent, + toIndex: toIndex, toContainer: item as? A11yContainer) } - // outlineView.moveItem(at: fromIndex, inParent: nil, // to: toIndex, inParent: nil) outlineView.reloadData() diff --git a/VoiceOver Designer/Features/Sources/TextUI/TextRepresentationController.swift b/VoiceOver Designer/Features/Sources/TextUI/TextRepresentationController.swift index 3ad7e713..e12f2217 100644 --- a/VoiceOver Designer/Features/Sources/TextUI/TextRepresentationController.swift +++ b/VoiceOver Designer/Features/Sources/TextUI/TextRepresentationController.swift @@ -20,7 +20,7 @@ public class TextRepresentationController: NSViewController { } var document: VODesignDocument! - var draggedNode: A11yDescription? + var draggedNode: (any AccessibilityView)? private var cancellables = Set() diff --git a/VoiceOver Designer/Features/Tests/TextUITests/VODocument+MoveTests.swift b/VoiceOver Designer/Features/Tests/TextUITests/VODocument+MoveTests.swift index 931abc23..f53382bb 100644 --- a/VoiceOver Designer/Features/Tests/TextUITests/VODocument+MoveTests.swift +++ b/VoiceOver Designer/Features/Tests/TextUITests/VODocument+MoveTests.swift @@ -1,20 +1,57 @@ import Document import XCTest @testable import TextUI +import CustomDump class A11yDescriptionArrayTests: XCTestCase { var sut: [any AccessibilityView] = [] + var el1: A11yDescription! + var el2: A11yDescription! + var el3: A11yDescription! override func setUp() { super.setUp() + el1 = A11yDescription.make(label: "1") + el2 = A11yDescription.make(label: "2") + el3 = A11yDescription.make(label: "3") + sut = [ - A11yDescription.make(label: "1"), - A11yDescription.make(label: "2"), - A11yDescription.make(label: "3"), + el1, + el2, + el3, ] } +} + +extension Array where Element == any AccessibilityView { + func assert( + labels: String..., + file: StaticString = #file, line: UInt = #line + ) { + XCTAssertNoDifference(recursiveDescription(), + labels, + file: file, line: line) + } + + func recursiveDescription() -> [String] { + map { view in + if let container = view as? A11yContainer { + if container.elements.isEmpty { + return container.label + } + + let elementsDescription = (container.elements as [any AccessibilityView]).recursiveDescription().joined(separator: ", ") + return "\(container.label): \(elementsDescription)" + } else { + return view.label + } + } + } +} + +class A11yDescriptionArrayMoveTests: A11yDescriptionArrayTests { // MARK: Move first func test_move0_to0_shouldNotMove() { @@ -120,3 +157,68 @@ extension A11yDescription { ) } } + +class A11yDescriptionArrayMoveContainerTests: A11yDescriptionArrayTests { + + func test_simpleMove() { + sut.move(el1, fromContainer: nil, toIndex: 2, toContainer: nil) + sut.assert(labels: "2", "1", "3") + } + + // MARK: - Inside containers + + func test_correctDescription() { + sut.wrapInContainer([el1], label: "Container1") + sut.assert(labels: "Container1: 1", "2", "3") + } + + func test_whenMove2IntoContainer_shouldMoveToContainer() { + let container = sut.wrapInContainer([el1], label: "Container1") + + sut.move(el2, fromContainer: nil, + toIndex: 1, toContainer: container!) + + sut.assert(labels: "Container1: 1, 2", "3") + } + + // MARK: Outside containers + + func test_whenMove1OutOfContainer_shouldKeepContainerEmpty() { + let container = sut.wrapInContainer([el1], label: "Container") + + sut.move(el1, fromContainer: container, + toIndex: 1, toContainer: nil) + + sut.assert(labels: "Container", "1", "2", "3") + } + + func test_whenMoveInSameContainer() { + let container = sut.wrapInContainer([el1, el2], label: "Container") + + sut.move(el1, fromContainer: container, + toIndex: 2, toContainer: container) + + sut.assert(labels: "Container: 2, 1", "3") + } + + func test_whenMoveInSameContainerToBeginning() { + let container = sut.wrapInContainer([el2, el1], label: "Container") // TODO: Strange that I should wrap in reverse order + sut.assert(labels: "Container: 1, 2", "3") + + sut.move(el2, fromContainer: container, + toIndex: 0, toContainer: container) + + sut.assert(labels: "Container: 2, 1", "3") + } + + func test_whenMoveFromOneContainerToAnother() { + let container1 = sut.wrapInContainer([el1], label: "Container1") + let container2 = sut.wrapInContainer([el2], label: "Container2") + + sut.move(el1, fromContainer: container1, + toIndex: 0, toContainer: container2) + + sut.assert(labels: "Container1", "Container2: 1, 2", "3") + } +} + From 7cccd0e5a6ab98ab4abd03d0bca2ea4436d4866f Mon Sep 17 00:00:00 2001 From: Mikhail Rubanov Date: Sat, 3 Dec 2022 11:57:42 +0500 Subject: [PATCH 4/4] Add comment about duplication --- VoiceOver Designer/Features/Sources/TextUI/Array+Move.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/VoiceOver Designer/Features/Sources/TextUI/Array+Move.swift b/VoiceOver Designer/Features/Sources/TextUI/Array+Move.swift index 7f369607..c5c72e5b 100644 --- a/VoiceOver Designer/Features/Sources/TextUI/Array+Move.swift +++ b/VoiceOver Designer/Features/Sources/TextUI/Array+Move.swift @@ -26,6 +26,7 @@ extension Array where Element == any AccessibilityView { } } +// TODO: Remove duplication of functions extension Array where Element == A11yDescription { /// - Returns: From and To indexes @discardableResult