diff --git a/Modules/Capabilities/DebugOverlay/Sources/OverlayPreferencesHostingController.swift b/Modules/Capabilities/DebugOverlay/Sources/OverlayPreferencesHostingController.swift new file mode 100644 index 00000000..39aed6a0 --- /dev/null +++ b/Modules/Capabilities/DebugOverlay/Sources/OverlayPreferencesHostingController.swift @@ -0,0 +1,20 @@ +// Created by Geoff Pado on 6/17/24. +// Copyright © 2024 Cocoatype, LLC. All rights reserved. + +import SwiftUI +import UIKit + +@available(iOS 15.0, *) +public class OverlayPreferencesHostingController: UIHostingController { + public init() { + super.init(rootView: OverlayPreferencesView()) + + sheetPresentationController?.detents = [.medium()] + } + + @available(*, unavailable) + required init(coder: NSCoder) { + let typeName = NSStringFromClass(type(of: self)) + fatalError("\(typeName) does not implement init(coder:)") + } +} diff --git a/Modules/Capabilities/DebugOverlay/Sources/OverlayPreferencesView.swift b/Modules/Capabilities/DebugOverlay/Sources/OverlayPreferencesView.swift new file mode 100644 index 00000000..f1f5eed2 --- /dev/null +++ b/Modules/Capabilities/DebugOverlay/Sources/OverlayPreferencesView.swift @@ -0,0 +1,42 @@ +// Created by Geoff Pado on 6/17/24. +// Copyright © 2024 Cocoatype, LLC. All rights reserved. + +import Defaults +import DesignSystem +import SwiftUI + +@available(iOS 15.0, *) +public struct OverlayPreferencesView: View { + @Defaults.Binding(key: .showDetectedTextOverlay) private var isDetectedTextOverlayEnabled: Bool + @Defaults.Binding(key: .showDetectedCharactersOverlay) private var isDetectedCharactersOverlayEnabled: Bool + @Defaults.Binding(key: .showRecognizedTextOverlay) private var isRecognizedTextOverlayEnabled: Bool + @Defaults.Binding(key: .showCalculatedOverlay) private var isCalculatedOverlayEnabled: Bool + + public var body: some View { + List { + PreferencesCell(isOn: $isDetectedTextOverlayEnabled, title: "Detected Text", color: .red) + PreferencesCell(isOn: $isDetectedCharactersOverlayEnabled, title: "Detected Characters", color: .blue) + PreferencesCell(isOn: $isRecognizedTextOverlayEnabled, title: "Recognized Text", color: .yellow) + PreferencesCell(isOn: $isCalculatedOverlayEnabled, title: "Calculated Area", color: .green) + } + } + + private struct PreferencesCell: View { + @Binding var isOn: Bool + let title: String + let color: Color + + var body: some View { + Toggle(isOn: $isOn) { + Text(title) + }.tint(color) + } + } +} + +@available(iOS 15.0, *) +enum OverlayPreferencesViewPreviews: PreviewProvider { + static var previews: some View { + OverlayPreferencesView() + } +} diff --git a/Modules/Capabilities/Defaults/Sources/DefaultsBinding.swift b/Modules/Capabilities/Defaults/Sources/DefaultsBinding.swift new file mode 100644 index 00000000..8c43b740 --- /dev/null +++ b/Modules/Capabilities/Defaults/Sources/DefaultsBinding.swift @@ -0,0 +1,76 @@ +// Created by Geoff Pado on 6/17/24. +// Copyright © 2024 Cocoatype, LLC. All rights reserved. + +import Foundation +import SwiftUI + +extension Defaults { + @propertyWrapper public struct Binding { + public var wrappedValue: ValueType { + get { + Self.object(for: key, fallback: fallback) + } + nonmutating set { + Self.userDefaults.set(newValue, forKey: key.rawValue) + NotificationCenter.default.post(name: valueDidChange, object: nil) + } + } + + public init(key: Defaults.Key, fallback: ValueType) { + self.key = key + self.fallback = fallback + let valueDidChange = Notification.Name("Defaults.valueDidChange.\(key.rawValue)") + self.binding = SwiftUI.Binding(get: { + Self.object(for: key, fallback: fallback) + }, set: { + Self.userDefaults.set($0, forKey: key.rawValue) + NotificationCenter.default.post(name: valueDidChange, object: nil) + }) + self.valueDidChange = valueDidChange + } + + public init(key: Defaults.Key) where ValueType == Bool { + self.init(key: key, fallback: false) + } + + public init(key: Defaults.Key) where ValueType == Int { + self.init(key: key, fallback: 0) + } + + public init(key: Defaults.Key) where ValueType == [ElementType] { + self.init(key: key, fallback: []) + } + + public init(key: Defaults.Key) where ValueType == [DictKey: DictValue] { + self.init(key: key, fallback: [:]) + } + + private let key: Defaults.Key + private let fallback: ValueType + + // MARK: Projected Value + + public var projectedValue: SwiftUI.Binding { + return binding + } + + private let binding: SwiftUI.Binding + + // MARK: Boilerplate + + private static func object(for key: Key, fallback: ValueType) -> ValueType { + guard let object = Self.userDefaults.object(forKey: key.rawValue) as? ValueType else { return fallback } + return object + } + + public let valueDidChange: Notification.Name + + static var userDefaults: UserDefaults { + guard ProcessInfo.processInfo.environment["IS_TEST"] == nil else { + return UserDefaults.test + } + + return UserDefaults.standard + } + } +} diff --git a/Modules/Capabilities/Defaults/Sources/DefaultsKey.swift b/Modules/Capabilities/Defaults/Sources/DefaultsKey.swift index 2016e5f2..2b304ff2 100644 --- a/Modules/Capabilities/Defaults/Sources/DefaultsKey.swift +++ b/Modules/Capabilities/Defaults/Sources/DefaultsKey.swift @@ -6,7 +6,6 @@ import Foundation extension Defaults { public enum Key: String { case numberOfSaves = "Defaults.Keys.numberOfSaves2" -// case autoRedactionsWordList = "Defaults.Keys.autoRedactionsWordList" case autoRedactionsCategoryNames = "Defaults.Keys.autoRedactionsCategoryNames" case autoRedactionsCategoryAddresses = "Defaults.Keys.autoRedactionsCategoryAddresses" case autoRedactionsCategoryPhoneNumbers = "Defaults.Keys.autoRedactionsCategoryPhoneNumbers" @@ -14,5 +13,11 @@ extension Defaults { case recentBookmarks = "Defaults.Keys.recentBookmarks" case hideDocumentScanner = "Defaults.Keys.hideDocumentScanner" case hideAutoRedactions = "Defaults.Keys.hideAutoRedactions" + + // Debug Overlay + case showDetectedTextOverlay = "Defaults.Keys.showDetectedTextOverlay" + case showDetectedCharactersOverlay = "Defaults.Keys.showDetectedCharactersOverlay" + case showRecognizedTextOverlay = "Defaults.Keys.showRecognizedTextOverlay" + case showCalculatedOverlay = "Defaults.Keys.showCalculatedOverlay" } } diff --git a/Modules/Legacy/Editing/Sources/Editing View/Observation View/Debug/PhotoEditingObservationDebugLayer.swift b/Modules/Legacy/Editing/Sources/Editing View/Observation View/Debug/PhotoEditingObservationDebugLayer.swift new file mode 100644 index 00000000..a298b112 --- /dev/null +++ b/Modules/Legacy/Editing/Sources/Editing View/Observation View/Debug/PhotoEditingObservationDebugLayer.swift @@ -0,0 +1,21 @@ +// Created by Geoff Pado on 6/17/24. +// Copyright © 2024 Cocoatype, LLC. All rights reserved. + +class PhotoEditingObservationDebugLayer: CAShapeLayer { + init(fillColor: UIColor, frame: CGRect, path: CGPath) { + super.init() + self.fillColor = fillColor.withAlphaComponent(0.3).cgColor + self.frame = frame + self.path = path + } + + override init(layer: Any) { + super.init(layer: layer) + } + + @available(*, unavailable) + required init(coder: NSCoder) { + let typeName = NSStringFromClass(type(of: self)) + fatalError("\(typeName) does not implement init(coder:)") + } +} diff --git a/Modules/Legacy/Editing/Sources/Editing View/Observation View/Debug/PhotoEditingObservationDebugView.swift b/Modules/Legacy/Editing/Sources/Editing View/Observation View/Debug/PhotoEditingObservationDebugView.swift new file mode 100644 index 00000000..4fe44e89 --- /dev/null +++ b/Modules/Legacy/Editing/Sources/Editing View/Observation View/Debug/PhotoEditingObservationDebugView.swift @@ -0,0 +1,102 @@ +// Created by Geoff Pado on 7/8/22. +// Copyright © 2022 Cocoatype, LLC. All rights reserved. + +@_implementationOnly import ClippingBezier +import Combine +import Defaults +import Observations +import UIKit + +class PhotoEditingObservationDebugView: PhotoEditingRedactionView { + override init() { + super.init() + isUserInteractionEnabled = false + subscribeToUpdates() + } + + deinit { + _ = cancellables.map(NotificationCenter.default.removeObserver(_:)) + } + + // MARK: Text Observations + + var textObservations: [TextRectangleObservation]? { + didSet { + updateDebugLayers() + setNeedsDisplay() + } + } + + var recognizedTextObservations: [RecognizedTextObservation]? { + didSet { + updateDebugLayers() + setNeedsDisplay() + } + } + + // MARK: Preferences + + @Defaults.Value(key: .showDetectedTextOverlay) private var isDetectedTextOverlayEnabled: Bool + @Defaults.Value(key: .showDetectedCharactersOverlay) private var isDetectedCharactersOverlayEnabled: Bool + @Defaults.Value(key: .showRecognizedTextOverlay) private var isRecognizedTextOverlayEnabled: Bool + @Defaults.Value(key: .showCalculatedOverlay) private var isCalculatedOverlayEnabled: Bool + private var cancellables = [any NSObjectProtocol]() + + private func subscribeToUpdates() { + let update: @MainActor @Sendable () -> Void = { [weak self] in self?.updateDebugLayers() } + cancellables.append(NotificationCenter.default.addObserver(for: _isDetectedTextOverlayEnabled, block: update)) + cancellables.append(NotificationCenter.default.addObserver(for: _isDetectedCharactersOverlayEnabled, block: update)) + cancellables.append(NotificationCenter.default.addObserver(for: _isRecognizedTextOverlayEnabled, block: update)) + cancellables.append(NotificationCenter.default.addObserver(for: _isCalculatedOverlayEnabled, block: update)) + } + + private func updateDebugLayers() { + Task { + layer.sublayers = await debugLayers + } + } + + // MARK: Debug Layers + + private var debugLayers: [CAShapeLayer] { + get async { + guard FeatureFlag.shouldShowDebugOverlay, let textObservations, let recognizedTextObservations else { return [] } + + // find words (new system) + let wordLayers: [PhotoEditingObservationDebugLayer] + if isRecognizedTextOverlayEnabled { + wordLayers = recognizedTextObservations.map { wordObservation in + PhotoEditingObservationDebugLayer(fillColor: .systemYellow, frame: bounds, path: wordObservation.path) + } + } else { wordLayers = [] } + + // find text (old system) + let textLayers = textObservations.flatMap { textObservation -> [CAShapeLayer] in + let characterObservations = textObservation.characterObservations + let characterLayers: [PhotoEditingObservationDebugLayer] + if isDetectedCharactersOverlayEnabled { + characterLayers = characterObservations.map { observation -> PhotoEditingObservationDebugLayer in + PhotoEditingObservationDebugLayer(fillColor: .systemBlue, frame: bounds, path: observation.bounds.path) + } + } else { characterLayers = [] } + + if Defaults.Value(key: .showDetectedTextOverlay).wrappedValue { + let textLayer = PhotoEditingObservationDebugLayer(fillColor: .systemRed, frame: bounds, path: CGPath(rect: textObservation.bounds.boundingBox, transform: nil)) + + return characterLayers + [textLayer] + } else { return characterLayers } + } + + let calculator = PhotoEditingObservationCalculator(detectedTextObservations: textObservations, recognizedTextObservations: recognizedTextObservations) + let calculatedObservations = await calculator.calculatedObservations + let wordCharacterLayers: [PhotoEditingObservationDebugLayer] + if isCalculatedOverlayEnabled { + wordCharacterLayers = calculatedObservations.map { (calculatedObservation: CharacterObservation) -> PhotoEditingObservationDebugLayer in + PhotoEditingObservationDebugLayer(fillColor: .systemGreen, frame: bounds, path: calculatedObservation.bounds.path) + } + } else { wordCharacterLayers = [] } + + return textLayers + wordLayers + wordCharacterLayers + } + } +} diff --git a/Modules/Legacy/Editing/Sources/Editing View/Observation View/PhotoEditingObservationDebugView.swift b/Modules/Legacy/Editing/Sources/Editing View/Observation View/PhotoEditingObservationDebugView.swift deleted file mode 100644 index 36491598..00000000 --- a/Modules/Legacy/Editing/Sources/Editing View/Observation View/PhotoEditingObservationDebugView.swift +++ /dev/null @@ -1,81 +0,0 @@ -// Created by Geoff Pado on 7/8/22. -// Copyright © 2022 Cocoatype, LLC. All rights reserved. - -@_implementationOnly import ClippingBezier -import Observations -import UIKit - -class PhotoEditingObservationDebugView: PhotoEditingRedactionView { - override init() { - super.init() - isUserInteractionEnabled = false - } - - // MARK: Text Observations - - var textObservations: [TextRectangleObservation]? { - didSet { - updateDebugLayers() - setNeedsDisplay() - } - } - - var recognizedTextObservations: [RecognizedTextObservation]? { - didSet { - updateDebugLayers() - setNeedsDisplay() - } - } - - private func updateDebugLayers() { - Task { - layer.sublayers = await debugLayers - } - } - - private var debugLayers: [CAShapeLayer] { - get async { - guard FeatureFlag.shouldShowDebugOverlay, let textObservations, let recognizedTextObservations else { return [] } - - // find words (new system) - let wordLayers = recognizedTextObservations.map { wordObservation in - let outlineLayer = CAShapeLayer() - outlineLayer.fillColor = UIColor.systemGreen.withAlphaComponent(0.0).cgColor - outlineLayer.frame = bounds - outlineLayer.path = wordObservation.path - return outlineLayer - } - - // find text (old system) - let textLayers = textObservations.flatMap { textObservation -> [CAShapeLayer] in - let characterObservations = textObservation.characterObservations - let characterLayers = characterObservations.map { observation -> CAShapeLayer in - let layer = CAShapeLayer() - layer.fillColor = UIColor.systemBlue.withAlphaComponent(0.3).cgColor - layer.frame = bounds - layer.path = observation.bounds.path - return layer - } - - let textLayer = CAShapeLayer() - textLayer.fillColor = UIColor.systemRed.withAlphaComponent(0.3).cgColor - textLayer.frame = bounds - textLayer.path = CGPath(rect: textObservation.bounds.boundingBox, transform: nil) - - return characterLayers + [textLayer] - } - - let calculator = PhotoEditingObservationCalculator(detectedTextObservations: textObservations, recognizedTextObservations: recognizedTextObservations) - let characterObservations = await calculator.calculatedObservations - let wordCharacterLayers = characterObservations.map { (characterObservation: CharacterObservation) -> CAShapeLayer in - let textLayer = CAShapeLayer() - textLayer.fillColor = UIColor.systemYellow.withAlphaComponent(0.0).cgColor - textLayer.frame = bounds - textLayer.path = characterObservation.bounds.path - return textLayer - } - - return textLayers + wordLayers + wordCharacterLayers - } - } -} diff --git a/Modules/Legacy/Editing/Sources/Editing View/PhotoEditingViewController.swift b/Modules/Legacy/Editing/Sources/Editing View/PhotoEditingViewController.swift index 7be0f04f..c1f153bf 100644 --- a/Modules/Legacy/Editing/Sources/Editing View/PhotoEditingViewController.swift +++ b/Modules/Legacy/Editing/Sources/Editing View/PhotoEditingViewController.swift @@ -2,6 +2,7 @@ // Copyright © 2019 Cocoatype, LLC. All rights reserved. import AutoRedactionsUI +import DebugOverlay import Defaults import Detections import Exporting @@ -500,6 +501,11 @@ public class PhotoEditingViewController: UIViewController, UIScrollViewDelegate, } } + @objc @MainActor public func showDebugPreferences(_ sender: Any) { + guard #available(iOS 15, *) else { return } + present(OverlayPreferencesHostingController(), animated: true) + } + // MARK: Boilerplate // tuBrute by @AdamWulf on 2024-04-29 diff --git a/Modules/Legacy/Editing/Sources/Editing View/Toolbar Items/ActionSet.swift b/Modules/Legacy/Editing/Sources/Editing View/Toolbar Items/ActionSet.swift index f369fc78..df9bf62a 100644 --- a/Modules/Legacy/Editing/Sources/Editing View/Toolbar Items/ActionSet.swift +++ b/Modules/Legacy/Editing/Sources/Editing View/Toolbar Items/ActionSet.swift @@ -30,6 +30,8 @@ struct ActionSet { // 🧑‍💻👋👋👋🧑‍💻🏠🕖🤣💜😍💜😍🕙😒😒😒🕙👋😍🤣 by @Eskeminha on 2024-05-03 // the standard set of trailing navigation items @ToolbarBuilder private var 🧑‍💻👋👋👋🧑‍💻🏠🕖🤣💜😍💜😍🕙😒😒😒🕙👋😍🤣: [UIBarButtonItem] { + if FeatureFlag.shouldShowDebugOverlay { DebugPreferencesBarButtonItem(target: target) } + ShareBarButtonItem(target: target) SeekBarButtonItem(target: target) diff --git a/Modules/Legacy/Editing/Sources/Editing View/Toolbar Items/ActionsBuilderActions.swift b/Modules/Legacy/Editing/Sources/Editing View/Toolbar Items/ActionsBuilderActions.swift index 514e566c..cf51a564 100644 --- a/Modules/Legacy/Editing/Sources/Editing View/Toolbar Items/ActionsBuilderActions.swift +++ b/Modules/Legacy/Editing/Sources/Editing View/Toolbar Items/ActionsBuilderActions.swift @@ -10,4 +10,5 @@ import Foundation func showColorPicker(_ sender: Any) func showAutoRedactAccess(_ sender: Any) func startSeeking(_ sender: Any) + func showDebugPreferences(_ sender: Any) } diff --git a/Modules/Legacy/Editing/Sources/Editing View/Toolbar Items/DebugPreferencesBarButtonItem.swift b/Modules/Legacy/Editing/Sources/Editing View/Toolbar Items/DebugPreferencesBarButtonItem.swift new file mode 100644 index 00000000..bd63ea0b --- /dev/null +++ b/Modules/Legacy/Editing/Sources/Editing View/Toolbar Items/DebugPreferencesBarButtonItem.swift @@ -0,0 +1,11 @@ +// Created by Geoff Pado on 6/17/24. +// Copyright © 2024 Cocoatype, LLC. All rights reserved. + +import UIKit + +class DebugPreferencesBarButtonItem: UIBarButtonItem { + convenience init(target: AnyObject?) { + self.init(image: UIImage(systemName: "ladybug"), style: .plain, target: target, action: #selector(ActionsBuilderActions.showDebugPreferences(_:))) + accessibilityLabel = NSLocalizedString("DebugPreferencesBarButtonItem.accessibilityLabel", comment: "Accessibility label for the debug preferences toolbar item") + } +} diff --git a/Project.swift b/Project.swift index e5f18e13..27715667 100644 --- a/Project.swift +++ b/Project.swift @@ -17,6 +17,7 @@ let project = Project( Brushes.target(sdk: .catalyst), Brushes.target(sdk: .native), Core.target, + DebugOverlay.target, Defaults.target, DesignSystem.target, Detections.target(sdk: .catalyst), diff --git a/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/DebugOverlay.swift b/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/DebugOverlay.swift new file mode 100644 index 00000000..af120f7c --- /dev/null +++ b/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/DebugOverlay.swift @@ -0,0 +1,11 @@ +import ProjectDescription + +public enum DebugOverlay { + public static let target = Target.capabilitiesTarget( + name: "DebugOverlay", + dependencies: [ + .target(Defaults.target), + .target(DesignSystem.target), + ] + ) +} diff --git a/Tuist/ProjectDescriptionHelpers/Targets/Legacy/Editing.swift b/Tuist/ProjectDescriptionHelpers/Targets/Legacy/Editing.swift index b448a68a..8f49fb34 100644 --- a/Tuist/ProjectDescriptionHelpers/Targets/Legacy/Editing.swift +++ b/Tuist/ProjectDescriptionHelpers/Targets/Legacy/Editing.swift @@ -11,6 +11,7 @@ public enum Editing { dependencies: [ .target(AutoRedactionsUI.target), .target(Brushes.target(sdk: .catalyst)), + .target(DebugOverlay.target), .target(Detections.target(sdk: .catalyst)), .target(ErrorHandling.target(sdk: .catalyst)), .target(Exporting.target),