session-ios/SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorBrushViewControl...

269 lines
8.7 KiB
Swift

//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
import UIKit
import SessionUIKit
@objc
public protocol ImageEditorBrushViewControllerDelegate: class {
func brushDidComplete(currentColor: ImageEditorColor)
}
// MARK: -
public class ImageEditorBrushViewController: OWSViewController {
private weak var delegate: ImageEditorBrushViewControllerDelegate?
private let model: ImageEditorModel
private let canvasView: ImageEditorCanvasView
private let paletteView: ImageEditorPaletteView
// We only want to let users undo changes made in this view.
// So we snapshot any older "operation id" and prevent
// users from undoing it.
private let firstUndoOperationId: String?
init(delegate: ImageEditorBrushViewControllerDelegate,
model: ImageEditorModel,
currentColor: ImageEditorColor) {
self.delegate = delegate
self.model = model
self.canvasView = ImageEditorCanvasView(model: model)
self.paletteView = ImageEditorPaletteView(currentColor: currentColor)
self.firstUndoOperationId = model.currentUndoOperationId()
super.init(nibName: nil, bundle: nil)
model.add(observer: self)
}
@available(*, unavailable, message: "use other init() instead.")
required public init?(coder aDecoder: NSCoder) {
notImplemented()
}
// MARK: - View Lifecycle
public override func loadView() {
self.view = UIView()
self.view.backgroundColor = Colors.navigationBarBackground
self.view.isOpaque = true
canvasView.configureSubviews()
self.view.addSubview(canvasView)
canvasView.autoPinEdgesToSuperviewEdges()
paletteView.delegate = self
self.view.addSubview(paletteView)
paletteView.autoVCenterInSuperview()
paletteView.autoPinEdge(toSuperviewEdge: .trailing, withInset: 0)
self.view.isUserInteractionEnabled = true
let brushGestureRecognizer = ImageEditorPanGestureRecognizer(target: self, action: #selector(handleBrushGesture(_:)))
brushGestureRecognizer.maximumNumberOfTouches = 1
brushGestureRecognizer.referenceView = canvasView.gestureReferenceView
brushGestureRecognizer.delegate = self
self.view.addGestureRecognizer(brushGestureRecognizer)
updateNavigationBar()
}
public override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.view.layoutSubviews()
}
public override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.view.layoutSubviews()
}
private func updateNavigationBar() {
// Hide controls during stroke.
let hasStroke = currentStroke != nil
guard !hasStroke else {
updateNavigationBar(navigationBarItems: [])
return
}
let undoButton = navigationBarButton(imageName: "image_editor_undo",
selector: #selector(didTapUndo(sender:)))
let doneButton = navigationBarButton(imageName: "image_editor_checkmark_full",
selector: #selector(didTapDone(sender:)))
// Prevent users from undo any changes made before entering the view.
let canUndo = model.canUndo() && firstUndoOperationId != model.currentUndoOperationId()
var navigationBarItems = [UIView]()
if canUndo {
navigationBarItems = [undoButton, doneButton]
} else {
navigationBarItems = [doneButton]
}
updateNavigationBar(navigationBarItems: navigationBarItems)
}
private func updateControls() {
// Hide controls during stroke.
let hasStroke = currentStroke != nil
paletteView.isHidden = hasStroke
}
@objc
public override var prefersStatusBarHidden: Bool {
return true
}
@objc
override public var canBecomeFirstResponder: Bool {
return true
}
// MARK: - Actions
@objc func didTapUndo(sender: UIButton) {
Logger.verbose("")
guard model.canUndo() else {
owsFailDebug("Can't undo.")
return
}
model.undo()
}
@objc func didTapDone(sender: UIButton) {
Logger.verbose("")
completeAndDismiss()
}
private func completeAndDismiss() {
self.delegate?.brushDidComplete(currentColor: paletteView.selectedValue)
self.dismiss(animated: false) {
// Do nothing.
}
}
// MARK: - Brush
// These properties are non-empty while drawing a stroke.
private var currentStroke: ImageEditorStrokeItem? {
didSet {
updateControls()
updateNavigationBar()
}
}
private var currentStrokeSamples = [ImageEditorStrokeItem.StrokeSample]()
@objc
public func handleBrushGesture(_ gestureRecognizer: ImageEditorPanGestureRecognizer) {
AssertIsOnMainThread()
let removeCurrentStroke = {
if let stroke = self.currentStroke {
self.model.remove(item: stroke)
}
self.currentStroke = nil
self.currentStrokeSamples.removeAll()
}
let tryToAppendStrokeSample = { (locationInView: CGPoint) in
let view = self.canvasView.gestureReferenceView
let viewBounds = view.bounds
let newSample = ImageEditorCanvasView.locationImageUnit(forLocationInView: locationInView,
viewBounds: viewBounds,
model: self.model,
transform: self.model.currentTransform())
if let prevSample = self.currentStrokeSamples.last,
prevSample == newSample {
// Ignore duplicate samples.
return
}
self.currentStrokeSamples.append(newSample)
}
let strokeColor = paletteView.selectedValue.color
let unitStrokeWidth = ImageEditorStrokeItem.defaultUnitStrokeWidth() / self.model.currentTransform().scaling
switch gestureRecognizer.state {
case .began:
removeCurrentStroke()
// Apply the location history of the gesture so that the stroke reflects
// the touch's movement before the gesture recognized.
for location in gestureRecognizer.locationHistory {
tryToAppendStrokeSample(location)
}
let locationInView = gestureRecognizer.location(in: canvasView.gestureReferenceView)
tryToAppendStrokeSample(locationInView)
let stroke = ImageEditorStrokeItem(color: strokeColor, unitSamples: currentStrokeSamples, unitStrokeWidth: unitStrokeWidth)
model.append(item: stroke)
currentStroke = stroke
case .changed, .ended:
let locationInView = gestureRecognizer.location(in: canvasView.gestureReferenceView)
tryToAppendStrokeSample(locationInView)
guard let lastStroke = self.currentStroke else {
owsFailDebug("Missing last stroke.")
removeCurrentStroke()
return
}
// Model items are immutable; we _replace_ the
// stroke item rather than modify it.
let stroke = ImageEditorStrokeItem(itemId: lastStroke.itemId, color: strokeColor, unitSamples: currentStrokeSamples, unitStrokeWidth: unitStrokeWidth)
model.replace(item: stroke, suppressUndo: true)
if gestureRecognizer.state == .ended {
currentStroke = nil
currentStrokeSamples.removeAll()
} else {
currentStroke = stroke
}
default:
removeCurrentStroke()
}
}
}
// MARK: -
extension ImageEditorBrushViewController: ImageEditorModelObserver {
public func imageEditorModelDidChange(before: ImageEditorContents,
after: ImageEditorContents) {
updateNavigationBar()
}
public func imageEditorModelDidChange(changedItemIds: [String]) {
updateNavigationBar()
}
}
// MARK: -
extension ImageEditorBrushViewController: ImageEditorPaletteViewDelegate {
public func selectedColorDidChange() {
// TODO:
}
}
// MARK: -
extension ImageEditorBrushViewController: UIGestureRecognizerDelegate {
@objc public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
// Ignore touches that begin inside the palette.
let location = touch.location(in: paletteView)
return !paletteView.bounds.contains(location)
}
}