This change also fixes a bug when bottom buttons in the crop screen were not visible. That happened because ImageEditorBottomBar by default had its controls hidden (with the intent that they are animated in once view controller's view is visible). That was a poor design choice on my end and is fixed in this commit.
657 lines
23 KiB
Swift
657 lines
23 KiB
Swift
//
|
|
// Copyright (c) 2022 Open Whisper Systems. All rights reserved.
|
|
//
|
|
|
|
import SignalCoreKit
|
|
import SignalMessaging
|
|
import UIKit
|
|
|
|
// Base class for all tool view controllers.
|
|
|
|
class ImageEditorViewController: OWSViewController {
|
|
|
|
let model: ImageEditorModel
|
|
|
|
// 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?
|
|
|
|
let imageEditorView: ImageEditorView
|
|
|
|
let topBar = ImageEditorTopBar()
|
|
|
|
lazy var bottomBar: ImageEditorBottomBar = ImageEditorBottomBar(buttonProvider: self)
|
|
|
|
enum Mode: Int {
|
|
case draw = 1
|
|
case blur
|
|
case text
|
|
}
|
|
|
|
var mode: Mode = .draw {
|
|
didSet {
|
|
if oldValue != mode && isViewLoaded {
|
|
updateUIForCurrentMode()
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns maximum width for the area with tool-specific UI elements in the toolbar at the bottom.
|
|
* Such tool-specific elements are: color picker (for both text and drawing tools), text style selection button etc.
|
|
* This maximum width is calculated as:
|
|
* iPhone: screen width in portrait orientation minus standard horizontal margins.
|
|
* iPad: value from iPhone 13 Max (428 - 2x20)
|
|
*/
|
|
static let preferredToolbarContentWidth: CGFloat = {
|
|
if UIDevice.current.isIPad {
|
|
return 388
|
|
} else {
|
|
let screenWidth = min(UIScreen.main.bounds.width, UIScreen.main.bounds.height)
|
|
let inset: CGFloat = UIDevice.current.isPlusSizePhone ? 20 : 16
|
|
return screenWidth - 2*inset
|
|
}
|
|
}()
|
|
|
|
// Pen Tool UI
|
|
var drawToolUIInitialized = false
|
|
lazy var drawToolbar: DrawToolbar = {
|
|
let toolbar = DrawToolbar(currentColor: model.color)
|
|
toolbar.preservesSuperviewLayoutMargins = true
|
|
toolbar.paletteView.delegate = self
|
|
toolbar.strokeTypeButton.addTarget(self, action: #selector(strokeTypeButtonTapped(sender:)), for: .touchUpInside)
|
|
return toolbar
|
|
}()
|
|
lazy var drawToolGestureRecognizer: ImageEditorPanGestureRecognizer = {
|
|
let gestureRecognizer = ImageEditorPanGestureRecognizer(target: self, action: #selector(handleDrawToolGesture(_:)))
|
|
gestureRecognizer.maximumNumberOfTouches = 1
|
|
gestureRecognizer.referenceView = imageEditorView.gestureReferenceView
|
|
gestureRecognizer.delegate = self
|
|
return gestureRecognizer
|
|
}()
|
|
|
|
// Blur Tool UI
|
|
var blurToolUIInitialized = false
|
|
lazy var blurToolbar: UIStackView = {
|
|
let drawAnywhereHint = UILabel()
|
|
drawAnywhereHint.font = .ows_dynamicTypeCaption1
|
|
drawAnywhereHint.textColor = Theme.darkThemePrimaryColor
|
|
drawAnywhereHint.textAlignment = .center
|
|
drawAnywhereHint.numberOfLines = 0
|
|
drawAnywhereHint.lineBreakMode = .byWordWrapping
|
|
drawAnywhereHint.text = OWSLocalizedString("IMAGE_EDITOR_BLUR_HINT",
|
|
comment: "The image editor hint that you can draw blur")
|
|
drawAnywhereHint.layer.shadowColor = UIColor.black.cgColor
|
|
drawAnywhereHint.layer.shadowRadius = 2
|
|
drawAnywhereHint.layer.shadowOpacity = 0.66
|
|
drawAnywhereHint.layer.shadowOffset = .zero
|
|
|
|
let stackView = UIStackView()
|
|
stackView.alignment = .center
|
|
stackView.axis = .vertical
|
|
stackView.spacing = 14
|
|
stackView.addArrangedSubviews([ faceBlurContainer, drawAnywhereHint ])
|
|
return stackView
|
|
}()
|
|
lazy var faceBlurContainer: UIView = {
|
|
let containerView = PillView()
|
|
containerView.layoutMargins = UIEdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 8)
|
|
|
|
let blurBackgroundView = UIVisualEffectView(effect: UIBlurEffect(style: .dark))
|
|
containerView.addSubview(blurBackgroundView)
|
|
blurBackgroundView.autoPinEdgesToSuperviewEdges()
|
|
|
|
let autoBlurLabel = UILabel()
|
|
autoBlurLabel.text = OWSLocalizedString("IMAGE_EDITOR_BLUR_SETTING",
|
|
comment: "The image editor setting to blur faces")
|
|
autoBlurLabel.font = .ows_dynamicTypeSubheadlineClamped
|
|
autoBlurLabel.textColor = Theme.darkThemePrimaryColor
|
|
|
|
let stackView = UIStackView(arrangedSubviews: [ autoBlurLabel, faceBlurSwitch ])
|
|
stackView.spacing = 12
|
|
stackView.alignment = .center
|
|
stackView.axis = .horizontal
|
|
containerView.addSubview(stackView)
|
|
stackView.autoPinEdgesToSuperviewMargins()
|
|
|
|
return containerView
|
|
}()
|
|
lazy var faceBlurSwitch: UISwitch = {
|
|
let faceBlurSwitch = UISwitch()
|
|
faceBlurSwitch.addTarget(self, action: #selector(didToggleAutoBlur), for: .valueChanged)
|
|
faceBlurSwitch.isOn = currentAutoBlurItem != nil
|
|
return faceBlurSwitch
|
|
}()
|
|
lazy var blurToolGestureRecognizer: ImageEditorPanGestureRecognizer = {
|
|
let gestureRecognizer = ImageEditorPanGestureRecognizer(target: self, action: #selector(handleBlurToolGesture(_:)))
|
|
gestureRecognizer.maximumNumberOfTouches = 1
|
|
gestureRecognizer.referenceView = imageEditorView.gestureReferenceView
|
|
gestureRecognizer.delegate = self
|
|
return gestureRecognizer
|
|
}()
|
|
// We persist an auto blur identifier for this session so
|
|
// we can keep the toggle switch in sync with undo/redo behavior
|
|
static let autoBlurItemIdentifier = "autoBlur"
|
|
var currentAutoBlurItem: ImageEditorBlurRegionsItem? {
|
|
return model.item(forId: ImageEditorViewController.autoBlurItemIdentifier) as? ImageEditorBlurRegionsItem
|
|
}
|
|
|
|
// Pen / Blur Drawing
|
|
lazy var strokeWidthSlider: ImageEditorSlider = {
|
|
let slider = ImageEditorSlider()
|
|
slider.minimumValue = 0.2
|
|
slider.maximumValue = 2
|
|
slider.value = 1
|
|
slider.addTarget(self, action: #selector(handleSliderTouchEvents(slider:)), for: .allTouchEvents)
|
|
slider.addTarget(self, action: #selector(handleSliderValueChanged(slider:)), for: .valueChanged)
|
|
return slider
|
|
}()
|
|
lazy var strokeWidthSliderContainer = UIView()
|
|
lazy var strokeWidthPreviewDot: UIView = {
|
|
let view = CircleView()
|
|
view.layer.borderColor = UIColor.white.cgColor
|
|
view.layer.borderWidth = 2
|
|
strokeWidthPreviewDotSize = view.autoSetDimension(.width, toSize: 20)
|
|
view.autoPinToSquareAspectRatio()
|
|
return view
|
|
}()
|
|
var strokeWidthPreviewDotSize: NSLayoutConstraint?
|
|
var strokeWidthSliderIsTrackingObservation: NSKeyValueObservation?
|
|
var strokeWidthSliderRevealed = false
|
|
var hideStrokeWidthSliderTimer: Timer?
|
|
var strokeWidthSliderPosition: NSLayoutConstraint?
|
|
var strokeWidthValues: [ImageEditorStrokeItem.StrokeType: Float] = [:]
|
|
var currentStrokeType: ImageEditorStrokeItem.StrokeType = .pen {
|
|
didSet {
|
|
updateStrokeWidthSliderValue()
|
|
updateStrokeWidthPreviewSize()
|
|
updateStrokeWidthPreviewColor()
|
|
}
|
|
}
|
|
var currentStroke: ImageEditorStrokeItem? {
|
|
didSet {
|
|
updateControlsVisibility(animated: true)
|
|
updateTopBar()
|
|
}
|
|
}
|
|
var currentStrokeSamples = [ImageEditorStrokeItem.StrokeSample]()
|
|
func currentStrokeUnitWidth() -> CGFloat {
|
|
let defaultUnitStrokeWidth = ImageEditorStrokeItem.defaultUnitStrokeWidth(forStrokeType: currentStrokeType)
|
|
return defaultUnitStrokeWidth * CGFloat(strokeWidthSlider.value) / model.currentTransform().scaling
|
|
}
|
|
|
|
// Text UI
|
|
var textUIInitialized = false
|
|
var startEditingTextOnViewAppear = false
|
|
var currentTextItem: (textItem: ImageEditorTextItem, isNewItem: Bool)?
|
|
var pinchFontStart: UIFont?
|
|
lazy var textViewContainer: UIView = {
|
|
let view = UIView(frame: view.bounds)
|
|
view.preservesSuperviewLayoutMargins = true
|
|
view.alpha = 0
|
|
return view
|
|
}()
|
|
lazy var textView: VAlignTextView = {
|
|
let textView = VAlignTextView()
|
|
textView.delegate = self
|
|
textView.isEditable = true
|
|
textView.backgroundColor = .clear
|
|
textView.isOpaque = false
|
|
textView.tintColor = .white
|
|
textView.isScrollEnabled = false
|
|
textView.scrollsToTop = false
|
|
textView.isUserInteractionEnabled = true
|
|
textView.textAlignment = .center
|
|
textView.textContainerInset = .zero
|
|
textView.textContainer.lineFragmentPadding = 0
|
|
textView.contentInset = .zero
|
|
return textView
|
|
}()
|
|
lazy var textToolbar: TextToolbar = {
|
|
let toolbar = TextToolbar(currentColor: currentTextItem?.textItem.color ?? model.color)
|
|
toolbar.preservesSuperviewLayoutMargins = true
|
|
toolbar.paletteView.delegate = self
|
|
toolbar.textStyleButton.addTarget(self, action: #selector(didTapTextStyleButton(sender:)), for: .touchUpInside)
|
|
return toolbar
|
|
}()
|
|
lazy var textViewAccessoryToolbar: TextToolbar = {
|
|
let toolbar = TextToolbar(currentColor: currentTextItem?.textItem.color ?? model.color)
|
|
toolbar.preservesSuperviewLayoutMargins = true
|
|
toolbar.paletteView.delegate = self
|
|
toolbar.textStyleButton.addTarget(self, action: #selector(didTapTextStyleButton(sender:)), for: .touchUpInside)
|
|
return toolbar
|
|
}()
|
|
|
|
init(model: ImageEditorModel) {
|
|
self.model = model
|
|
self.imageEditorView = ImageEditorView(model: model, delegate: nil)
|
|
self.firstUndoOperationId = model.currentUndoOperationId()
|
|
|
|
super.init()
|
|
|
|
model.add(observer: self)
|
|
}
|
|
|
|
override func loadView() {
|
|
view = UIView()
|
|
view.backgroundColor = .black
|
|
view.isOpaque = true
|
|
|
|
imageEditorView.configureSubviews()
|
|
view.addSubview(imageEditorView)
|
|
imageEditorView.autoPinWidthToSuperview()
|
|
imageEditorView.autoPinEdge(toSuperviewEdge: .top)
|
|
|
|
// Top toolbar
|
|
updateTopBar()
|
|
topBar.undoButton.addTarget(self, action: #selector(didTapUndo(sender:)), for: .touchUpInside)
|
|
topBar.clearAllButton.addTarget(self, action: #selector(didTapClearAll(sender:)), for: .touchUpInside)
|
|
topBar.install(in: view)
|
|
|
|
// Bottom toolbar
|
|
view.addSubview(bottomBar)
|
|
bottomBar.autoPinWidthToSuperview()
|
|
bottomBar.autoPinEdge(toSuperviewEdge: .bottom)
|
|
bottomBar.autoPinEdge(.top, to: .bottom, of: imageEditorView)
|
|
bottomBar.cancelButton.addTarget(self, action: #selector(didTapCancel(sender:)), for: .touchUpInside)
|
|
bottomBar.doneButton.addTarget(self, action: #selector(didTapDone(sender:)), for: .touchUpInside)
|
|
|
|
// Stroke width slider
|
|
strokeWidthSliderContainer.addSubview(strokeWidthSlider)
|
|
strokeWidthSlider.autoPinEdgesToSuperviewMargins()
|
|
strokeWidthSliderContainer.layoutMargins = UIEdgeInsets(top: 8, leading: 0, bottom: 8, trailing: 0)
|
|
strokeWidthSliderContainer.transform = CGAffineTransform(rotationAngle: -.halfPi)
|
|
view.addSubview(strokeWidthSliderContainer)
|
|
strokeWidthSliderContainer.autoVCenterInSuperview()
|
|
strokeWidthSliderPosition = strokeWidthSliderContainer.centerXAnchor.constraint(equalTo: view.leadingAnchor)
|
|
strokeWidthSliderPosition?.autoInstall()
|
|
strokeWidthSliderContainer.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleSliderContainerTap(_:))))
|
|
}
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
|
|
updateUIForCurrentMode()
|
|
}
|
|
|
|
override func viewWillAppear(_ animated: Bool) {
|
|
super.viewWillAppear(animated)
|
|
|
|
UIView.performWithoutAnimation {
|
|
setControls(hidden: true)
|
|
}
|
|
}
|
|
|
|
override func viewDidAppear(_ animated: Bool) {
|
|
super.viewDidAppear(animated)
|
|
|
|
updateControlsVisibility(animated: true) { finished in
|
|
guard finished else { return }
|
|
if self.startEditingTextOnViewAppear && self.canBeginTextEditingOnViewAppear {
|
|
self.beginTextEditing()
|
|
}
|
|
self.startEditingTextOnViewAppear = false
|
|
}
|
|
}
|
|
|
|
override func viewSafeAreaInsetsDidChange() {
|
|
super.viewSafeAreaInsetsDidChange()
|
|
updateContentLayoutMargins()
|
|
}
|
|
|
|
override var prefersStatusBarHidden: Bool {
|
|
!UIDevice.current.hasIPhoneXNotch && !UIDevice.current.isIPad && !CurrentAppContext().hasActiveCall
|
|
}
|
|
|
|
override var preferredStatusBarStyle: UIStatusBarStyle {
|
|
.lightContent
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
private func updateContentLayoutMargins() {
|
|
var contentLayoutMargins: UIEdgeInsets = .zero
|
|
if UIDevice.current.hasIPhoneXNotch {
|
|
contentLayoutMargins.top = view.safeAreaInsets.top
|
|
}
|
|
|
|
// Unlike code in `AttachmentApprovalViewController` bottom inset is zero
|
|
// because bottom of ImageEditorView is constrained to the top of toolbar.
|
|
imageEditorView.contentLayoutMargins = contentLayoutMargins
|
|
}
|
|
|
|
private func updateUIForCurrentMode() {
|
|
switch mode {
|
|
case .draw, .blur:
|
|
strokeWidthSliderContainer.isHidden = false
|
|
finishTextEditing(applyEdits: true)
|
|
|
|
case .text:
|
|
strokeWidthSliderContainer.isHidden = true
|
|
}
|
|
|
|
imageEditorView.textInteractionModes = {
|
|
switch mode {
|
|
case .draw, .blur: return [ .select ]
|
|
case .text: return .all
|
|
}
|
|
}()
|
|
|
|
updateDrawToolUIVisibility()
|
|
updateBlurToolUIVisibility()
|
|
updateTextUIVisibility()
|
|
|
|
for button in bottomBar.buttons {
|
|
button.isSelected = mode.rawValue == button.tag
|
|
}
|
|
}
|
|
|
|
private func updateTopBar() {
|
|
let canUndo = canUndo
|
|
topBar.isUndoButtonHidden = !canUndo
|
|
topBar.isClearAllButtonHidden = !canUndo
|
|
}
|
|
|
|
private var shouldHideControls: Bool {
|
|
switch mode {
|
|
case .draw, .blur:
|
|
return currentStroke != nil
|
|
|
|
case .text:
|
|
return imageEditorView.shouldHideControls
|
|
}
|
|
}
|
|
|
|
private var canUndo: Bool {
|
|
model.canUndo() && firstUndoOperationId != model.currentUndoOperationId()
|
|
}
|
|
|
|
func updateControlsVisibility(animated: Bool, completion: ((Bool) -> Void)? = nil) {
|
|
setControls(hidden: shouldHideControls, animated: animated, completion: completion)
|
|
}
|
|
|
|
private func setControls(hidden: Bool, animated: Bool, completion: ((Bool) -> Void)? = nil) {
|
|
if animated {
|
|
UIView.animate(withDuration: 0.15,
|
|
animations: {
|
|
self.setControls(hidden: hidden)
|
|
|
|
// Animate layout changes made within bottomBar.setControls(hidden:).
|
|
self.bottomBar.setNeedsDisplay()
|
|
self.bottomBar.layoutIfNeeded()
|
|
},
|
|
completion: completion)
|
|
} else {
|
|
setControls(hidden: hidden)
|
|
if let completion = completion {
|
|
completion(true)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func setControls(hidden: Bool) {
|
|
let alpha: CGFloat = hidden ? 0 : 1
|
|
topBar.alpha = alpha
|
|
bottomBar.alpha = alpha
|
|
bottomBar.setControls(hidden: hidden)
|
|
|
|
switch mode {
|
|
case .draw:
|
|
updateDrawToolControlsVisibility()
|
|
|
|
case .blur:
|
|
updateBlurToolControlsVisibility()
|
|
|
|
case .text:
|
|
updateTextControlsVisibility()
|
|
}
|
|
}
|
|
|
|
private func modelDidChange() {
|
|
updateTopBar()
|
|
|
|
if blurToolUIInitialized {
|
|
// If we undo/redo, we may remove or re-apply the auto blur
|
|
faceBlurSwitch.isOn = currentAutoBlurItem != nil
|
|
}
|
|
}
|
|
|
|
private func undo() {
|
|
Logger.verbose("")
|
|
guard canUndo else {
|
|
owsFailDebug("Can't undo.")
|
|
return
|
|
}
|
|
model.undo()
|
|
}
|
|
|
|
private func clearAll() {
|
|
Logger.verbose("")
|
|
|
|
if mode == .text {
|
|
finishTextEditing(applyEdits: false)
|
|
}
|
|
|
|
while canUndo {
|
|
model.undo()
|
|
}
|
|
}
|
|
|
|
private func prepareToDismiss(completion: ((Bool) -> Void)?) {
|
|
if mode == .text {
|
|
finishTextEditing(applyEdits: false)
|
|
}
|
|
setControls(hidden: true, animated: true, completion: completion)
|
|
}
|
|
|
|
private func prepareToFinish(completion: ((Bool) -> Void)?) {
|
|
if mode == .text {
|
|
finishTextEditing(applyEdits: true)
|
|
}
|
|
setControls(hidden: true, animated: true, completion: completion)
|
|
}
|
|
|
|
private func discardAndDismiss() {
|
|
if canUndo {
|
|
askToDiscardAllChanges {
|
|
self.prepareToDismiss { finished in
|
|
guard finished else { return }
|
|
self.dismiss(animated: false)
|
|
}
|
|
}
|
|
} else {
|
|
prepareToDismiss { finished in
|
|
guard finished else { return }
|
|
self.dismiss(animated: false)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func completeAndDismiss() {
|
|
prepareToFinish { finished in
|
|
guard finished else { return }
|
|
self.dismiss(animated: false)
|
|
}
|
|
}
|
|
|
|
private func askToDiscardAllChanges(_ completionHandler: (() -> Void)?) {
|
|
let actionSheetTitle = OWSLocalizedString("MEDIA_EDITOR_DISCARD_ALL_CONFIRMATION_TITLE",
|
|
comment: "Media Editor: Title for the 'Discard Changes' confirmation prompt.")
|
|
let actionSheetMessage = OWSLocalizedString("MEDIA_EDITOR_DISCARD_ALL_CONFIRMATION_MESSAGE",
|
|
comment: "Media Editor: Message for the 'Discard Changes' confirmation prompt.")
|
|
let discardChangesButton = OWSLocalizedString("MEDIA_EDITOR_DISCARD_ALL_BUTTON",
|
|
comment: "Media Editor: Title for the button in 'Discard Changes' confirmation prompt.")
|
|
let actionSheet = ActionSheetController(title: actionSheetTitle, message: actionSheetMessage)
|
|
actionSheet.addAction(ActionSheetAction(title: discardChangesButton, style: .destructive, handler: { _ in
|
|
self.clearAll()
|
|
if let completionHandler = completionHandler {
|
|
completionHandler()
|
|
}
|
|
}))
|
|
actionSheet.addAction(ActionSheetAction(title: CommonStrings.cancelButton, style: .cancel, handler: nil))
|
|
presentActionSheet(actionSheet)
|
|
}
|
|
}
|
|
|
|
// MARK: - Actions
|
|
|
|
extension ImageEditorViewController {
|
|
|
|
@objc
|
|
private func didTapUndo(sender: UIButton) {
|
|
undo()
|
|
}
|
|
|
|
@objc
|
|
private func didTapClearAll(sender: UIButton) {
|
|
askToDiscardAllChanges(nil)
|
|
}
|
|
|
|
@objc
|
|
private func didTapCancel(sender: UIButton) {
|
|
Logger.verbose("")
|
|
|
|
discardAndDismiss()
|
|
}
|
|
|
|
@objc
|
|
private func didTapDone(sender: UIButton) {
|
|
Logger.verbose("")
|
|
|
|
completeAndDismiss()
|
|
}
|
|
|
|
@objc
|
|
private func didTapPen(sender: UIButton) {
|
|
Logger.verbose("")
|
|
|
|
// Second tap on Pen icon switches editor to "text" mode.
|
|
mode = (mode == .draw) ? .text : .draw
|
|
}
|
|
|
|
@objc
|
|
private func didTapAddText(sender: UIButton) {
|
|
Logger.verbose("")
|
|
|
|
let textStyle = textToolbar.textStyle
|
|
let textColor = textToolbar.paletteView.selectedValue
|
|
let textItem = imageEditorView.createNewTextItem(withColor: textColor, textStyle: textStyle)
|
|
selectTextItem(textItem, isNewItem: true, startEditing: true)
|
|
}
|
|
|
|
@objc
|
|
private func didTapAddSticker(sender: UIButton) {
|
|
Logger.verbose("")
|
|
|
|
mode = .text
|
|
}
|
|
|
|
@objc
|
|
private func didTapBlur(sender: UIButton) {
|
|
Logger.verbose("")
|
|
|
|
// Second tap on Blur icon switches editor to "text" mode.
|
|
mode = (mode == .blur) ? .text : .blur
|
|
}
|
|
}
|
|
|
|
// MARK: - Bottom Bar
|
|
|
|
extension ImageEditorViewController: ImageEditorBottomBarButtonProvider {
|
|
|
|
var middleButtons: [UIButton] {
|
|
let penButton = RoundMediaButton(image: #imageLiteral(resourceName: "media-editor-toolbar-pen"), backgroundStyle: .solid(.clear))
|
|
penButton.tag = Mode.draw.rawValue
|
|
penButton.addTarget(self, action: #selector(didTapPen(sender:)), for: .touchUpInside)
|
|
|
|
let textButton = RoundMediaButton(image: #imageLiteral(resourceName: "media-editor-toolbar-text"), backgroundStyle: .solid(.clear))
|
|
textButton.addTarget(self, action: #selector(didTapAddText(sender:)), for: .touchUpInside)
|
|
|
|
let stickerButton = RoundMediaButton(image: #imageLiteral(resourceName: "media-editor-toolbar-emoji"), backgroundStyle: .solid(.clear))
|
|
stickerButton.addTarget(self, action: #selector(didTapAddSticker(sender:)), for: .touchUpInside)
|
|
|
|
let blurButton = RoundMediaButton(image: #imageLiteral(resourceName: "media-editor-toolbar-blur"), backgroundStyle: .solid(.clear))
|
|
blurButton.tag = Mode.blur.rawValue
|
|
blurButton.addTarget(self, action: #selector(didTapBlur(sender:)), for: .touchUpInside)
|
|
|
|
let buttons = [ penButton, textButton, stickerButton, blurButton ]
|
|
for button in buttons {
|
|
button.setBackgroundColor(.ows_white, for: .highlighted)
|
|
button.setBackgroundColor(.ows_white, for: .selected)
|
|
if let image = button.image(for: .normal),
|
|
let tintedImage = image.asTintedImage(color: .ows_black) {
|
|
button.setImage(tintedImage, for: .highlighted)
|
|
button.setImage(tintedImage, for: .selected)
|
|
}
|
|
}
|
|
|
|
return buttons
|
|
}
|
|
}
|
|
|
|
// MARK: - UIGestureRecognizerDelegate
|
|
|
|
extension ImageEditorViewController: UIGestureRecognizerDelegate {
|
|
|
|
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
|
|
// Ignore touches that begin inside the control areas.
|
|
switch mode {
|
|
case .draw:
|
|
guard !drawToolbar.bounds.contains(touch.location(in: drawToolbar)) else {
|
|
return false
|
|
}
|
|
guard !strokeWidthSliderContainer.bounds.contains(touch.location(in: strokeWidthSliderContainer)) else {
|
|
return false
|
|
}
|
|
return true
|
|
|
|
case .blur:
|
|
return !blurToolbar.bounds.contains(touch.location(in: blurToolbar))
|
|
|
|
default:
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - ImageEditorModelObserver
|
|
|
|
extension ImageEditorViewController: ImageEditorModelObserver {
|
|
|
|
func imageEditorModelDidChange(before: ImageEditorContents, after: ImageEditorContents) {
|
|
modelDidChange()
|
|
}
|
|
|
|
func imageEditorModelDidChange(changedItemIds: [String]) {
|
|
modelDidChange()
|
|
}
|
|
}
|
|
|
|
// MARK: - ImageEditorPaletteViewDelegate
|
|
|
|
extension ImageEditorViewController: ImageEditorPaletteViewDelegate {
|
|
|
|
func imageEditorPaletteView(_ paletteView: ImageEditorPaletteView, didSelectColor color: ImageEditorColor) {
|
|
switch mode {
|
|
case .draw:
|
|
model.color = color
|
|
updateStrokeWidthPreviewColor()
|
|
|
|
case .text:
|
|
imageEditorView.updateSelectedTextItem(withColor: color)
|
|
if paletteView == textToolbar.paletteView {
|
|
textViewAccessoryToolbar.paletteView.selectedValue = color
|
|
} else if paletteView == textViewAccessoryToolbar.paletteView {
|
|
textToolbar.paletteView.selectedValue = color
|
|
}
|
|
if textView.isFirstResponder {
|
|
updateTextViewAttributes(withColor: color.color)
|
|
}
|
|
|
|
default:
|
|
owsAssertDebug(false, "Invalid mode [\(mode)]")
|
|
}
|
|
}
|
|
}
|