mirror of https://github.com/oxen-io/session-ios
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
268 lines
8.6 KiB
Swift
268 lines
8.6 KiB
Swift
//
|
|
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
|
//
|
|
|
|
import UIKit
|
|
|
|
@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 = .black
|
|
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)
|
|
}
|
|
}
|