670 lines
24 KiB
Swift
670 lines
24 KiB
Swift
//
|
|
// Copyright 2022 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
import SignalServiceKit
|
|
import UIKit
|
|
|
|
// Base class for all tool view controllers.
|
|
|
|
class ImageEditorViewController: OWSViewController {
|
|
|
|
let model: ImageEditorModel
|
|
private weak var stickerSheetDelegate: StickerPickerSheetDelegate?
|
|
|
|
// 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
|
|
case sticker
|
|
}
|
|
|
|
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.colorPickerView.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 = .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 = .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()
|
|
updateTopBar()
|
|
}
|
|
}
|
|
var currentStrokeSamples = [ImageEditorStrokeItem.StrokeSample]()
|
|
func currentStrokeUnitWidth() -> CGFloat {
|
|
let unitStrokeWidth = ImageEditorStrokeItem.unitStrokeWidth(forStrokeType: currentStrokeType,
|
|
widthAdjustmentFactor: CGFloat(strokeWidthSlider.value))
|
|
return unitStrokeWidth / model.currentTransform().scaling
|
|
}
|
|
|
|
// Text UI
|
|
var textUIInitialized = false
|
|
var startEditingTextOnViewAppear = false
|
|
var discardTextEditsOnEditingEnd = false
|
|
var currentTextItem: (textItem: ImageEditorTextItem, isNewItem: Bool)?
|
|
var pinchFontSizeStart: CGFloat = ImageEditorTextItem.defaultFontSize
|
|
var textViewContainerBottomConstraint: NSLayoutConstraint? // to bottom of self.view
|
|
lazy var textViewContainer: UIView = {
|
|
let view = UIView(frame: view.bounds)
|
|
view.preservesSuperviewLayoutMargins = true
|
|
view.alpha = 0
|
|
return view
|
|
}()
|
|
lazy var textView: MediaTextView = {
|
|
let textView = MediaTextView()
|
|
textView.delegate = self
|
|
return textView
|
|
}()
|
|
lazy var textViewWrapperView = UIView()
|
|
lazy var textViewBackgroundView = UIView()
|
|
lazy var textViewAccessoryToolbar: TextStylingToolbar = {
|
|
let toolbar = TextStylingToolbar(currentColor: currentTextItem?.textItem.color)
|
|
toolbar.preservesSuperviewLayoutMargins = true
|
|
toolbar.addTarget(self, action: #selector(textColorDidChange), for: .valueChanged)
|
|
toolbar.textStyleButton.addTarget(self, action: #selector(didTapTextStyleButton(sender:)), for: .touchUpInside)
|
|
toolbar.decorationStyleButton.addTarget(self, action: #selector(didTapDecorationStyleButton(sender:)), for: .touchUpInside)
|
|
toolbar.doneButton.addTarget(self, action: #selector(didTapTextEditingDoneButton(sender:)), for: .touchUpInside)
|
|
return toolbar
|
|
}()
|
|
|
|
init(model: ImageEditorModel, stickerSheetDelegate: StickerPickerSheetDelegate?) {
|
|
self.model = model
|
|
self.stickerSheetDelegate = stickerSheetDelegate
|
|
self.imageEditorView = ImageEditorView(model: model, delegate: nil)
|
|
self.firstUndoOperationId = model.currentUndoOperationId()
|
|
|
|
super.init()
|
|
|
|
model.add(observer: self)
|
|
}
|
|
|
|
override func viewDidLoad() {
|
|
view.backgroundColor = .black
|
|
|
|
imageEditorView.configureSubviews()
|
|
view.addSubview(imageEditorView)
|
|
imageEditorView.autoPinWidthToSuperview()
|
|
imageEditorView.autoPinEdge(toSuperviewSafeArea: .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(_:))))
|
|
|
|
updateUIForCurrentMode()
|
|
}
|
|
|
|
override func viewWillAppear(_ animated: Bool) {
|
|
super.viewWillAppear(animated)
|
|
|
|
UIView.performWithoutAnimation {
|
|
transitionUI(toState: .initial, animated: false)
|
|
}
|
|
}
|
|
|
|
override func viewDidAppear(_ animated: Bool) {
|
|
super.viewDidAppear(animated)
|
|
|
|
transitionUI(toState: .final, animated: true) { finished in
|
|
guard finished else { return }
|
|
if self.startEditingTextOnViewAppear && self.canBeginTextEditingOnViewAppear {
|
|
self.beginTextEditing()
|
|
}
|
|
self.startEditingTextOnViewAppear = false
|
|
}
|
|
}
|
|
|
|
override func keyboardFrameDidChange(_ newFrame: CGRect, animationDuration: TimeInterval, animationOptions: UIView.AnimationOptions) {
|
|
super.keyboardFrameDidChange(newFrame, animationDuration: animationDuration, animationOptions: animationOptions)
|
|
|
|
updateTextViewContainerBottomLayoutConstraint(forKeyboardFrame: newFrame)
|
|
}
|
|
|
|
override var prefersStatusBarHidden: Bool {
|
|
!UIDevice.current.hasIPhoneXNotch && !UIDevice.current.isIPad && !DependenciesBridge.shared.currentCallProvider.hasCurrentCall
|
|
}
|
|
|
|
override var preferredStatusBarStyle: UIStatusBarStyle {
|
|
.lightContent
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
private func updateUIForCurrentMode() {
|
|
switch mode {
|
|
case .draw, .blur:
|
|
strokeWidthSliderContainer.isHidden = false
|
|
finishTextEditing()
|
|
imageEditorView.textInteractionModes = .select
|
|
case .text, .sticker:
|
|
strokeWidthSliderContainer.isHidden = true
|
|
imageEditorView.textInteractionModes = .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, .sticker:
|
|
return imageEditorView.shouldHideControls
|
|
}
|
|
}
|
|
|
|
private var canUndo: Bool {
|
|
model.canUndo() && firstUndoOperationId != model.currentUndoOperationId()
|
|
}
|
|
|
|
func updateControlsVisibility() {
|
|
setControls(hidden: shouldHideControls, animated: true, slideButtonsInOut: false)
|
|
}
|
|
|
|
private func setControls(hidden: Bool, animated: Bool, slideButtonsInOut: Bool, completion: ((Bool) -> Void)? = nil) {
|
|
if animated {
|
|
UIView.animate(withDuration: 0.15,
|
|
animations: {
|
|
self.setControls(hidden: hidden, slideButtonsInOut: slideButtonsInOut)
|
|
|
|
// Animate layout changes made within bottomBar.setControls(hidden:).
|
|
if slideButtonsInOut {
|
|
self.bottomBar.setNeedsDisplay()
|
|
self.bottomBar.layoutIfNeeded()
|
|
}
|
|
},
|
|
completion: completion)
|
|
} else {
|
|
setControls(hidden: hidden, slideButtonsInOut: slideButtonsInOut)
|
|
completion?(true)
|
|
}
|
|
}
|
|
|
|
private func setControls(hidden: Bool, slideButtonsInOut: Bool) {
|
|
let alpha: CGFloat = hidden ? 0 : 1
|
|
topBar.alpha = alpha
|
|
bottomBar.alpha = alpha
|
|
if slideButtonsInOut {
|
|
bottomBar.setControls(hidden: hidden)
|
|
}
|
|
|
|
switch mode {
|
|
case .draw:
|
|
updateDrawToolControlsVisibility()
|
|
|
|
case .blur:
|
|
updateBlurToolControlsVisibility()
|
|
|
|
case .text, .sticker:
|
|
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() {
|
|
guard canUndo else {
|
|
owsFailDebug("Can't undo.")
|
|
return
|
|
}
|
|
model.undo()
|
|
}
|
|
|
|
private func clearAll() {
|
|
if mode == .text {
|
|
finishTextEditing(discardEdits: true)
|
|
}
|
|
|
|
while canUndo {
|
|
model.undo()
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Presenting / Dismissing {
|
|
|
|
extension ImageEditorViewController {
|
|
|
|
private func prepareToDismiss(completion: ((Bool) -> Void)?) {
|
|
if mode == .text {
|
|
finishTextEditing(discardEdits: true)
|
|
}
|
|
transitionUI(toState: .initial, animated: true, completion: completion)
|
|
}
|
|
|
|
private func prepareToFinish(completion: ((Bool) -> Void)?) {
|
|
if mode == .text {
|
|
finishTextEditing()
|
|
}
|
|
transitionUI(toState: .initial, 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, theme: .translucentDark)
|
|
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)
|
|
}
|
|
|
|
private enum UIState {
|
|
case initial
|
|
case final
|
|
}
|
|
|
|
private func transitionUI(toState state: UIState, animated: Bool, completion: ((Bool) -> Void)? = nil) {
|
|
setControls(hidden: state == .initial, animated: animated, slideButtonsInOut: true, completion: completion)
|
|
imageEditorView.setHasRoundCorners(state == .initial, animationDuration: animated ? 0.15 : 0)
|
|
}
|
|
}
|
|
|
|
// 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) {
|
|
discardAndDismiss()
|
|
}
|
|
|
|
@objc
|
|
private func didTapDone(sender: UIButton) {
|
|
completeAndDismiss()
|
|
}
|
|
|
|
@objc
|
|
private func didTapPen(sender: UIButton) {
|
|
// Second tap on Pen icon switches editor to "text" mode.
|
|
mode = (mode == .draw) ? .text : .draw
|
|
}
|
|
|
|
@objc
|
|
private func didTapAddText(sender: UIButton) {
|
|
let decorationStyle = textViewAccessoryToolbar.decorationStyle
|
|
let textColor = textViewAccessoryToolbar.currentColorPickerValue
|
|
let textItem = imageEditorView.createNewTextItem(withColor: textColor, decorationStyle: decorationStyle)
|
|
selectTextItem(textItem, isNewItem: true, startEditing: true)
|
|
}
|
|
|
|
@objc
|
|
private func didTapAddSticker(sender: UIButton) {
|
|
let stickerPicker: StickerPickerSheet
|
|
if UIAccessibility.isReduceTransparencyEnabled {
|
|
stickerPicker = StickerPickerSheet(backgroundColor: Theme.darkThemeBackgroundColor)
|
|
} else {
|
|
stickerPicker = StickerPickerSheet(blurEffect: .init(style: .dark))
|
|
}
|
|
|
|
stickerPicker.pickerDelegate = self
|
|
stickerPicker.sheetDelegate = stickerSheetDelegate
|
|
|
|
present(stickerPicker, animated: true)
|
|
}
|
|
|
|
@objc
|
|
private func didTapBlur(sender: UIButton) {
|
|
// Second tap on Blur icon switches editor to "text" mode.
|
|
mode = (mode == .blur) ? .text : .blur
|
|
}
|
|
|
|
@objc
|
|
private func textColorDidChange(sender: TextStylingToolbar) {
|
|
let textItemColor = sender.currentColorPickerValue
|
|
imageEditorView.updateSelectedTextItem(withColor: textItemColor)
|
|
if textView.isFirstResponder {
|
|
updateTextViewAttributes(using: textViewAccessoryToolbar)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Bottom Bar
|
|
|
|
extension ImageEditorViewController: ImageEditorBottomBarButtonProvider {
|
|
|
|
var middleButtons: [UIButton] {
|
|
let penButton = RoundMediaButton(
|
|
image: UIImage(imageLiteralResourceName: "edit-28"),
|
|
backgroundStyle: .solid(.clear)
|
|
)
|
|
penButton.tag = Mode.draw.rawValue
|
|
penButton.addTarget(self, action: #selector(didTapPen(sender:)), for: .touchUpInside)
|
|
|
|
let textButton = RoundMediaButton(
|
|
image: UIImage(imageLiteralResourceName: "text-28"),
|
|
backgroundStyle: .solid(.clear)
|
|
)
|
|
textButton.addTarget(self, action: #selector(didTapAddText(sender:)), for: .touchUpInside)
|
|
|
|
let stickerButton = RoundMediaButton(
|
|
image: UIImage(imageLiteralResourceName: "sticker-smiley-28"),
|
|
backgroundStyle: .solid(.clear)
|
|
)
|
|
stickerButton.addTarget(self, action: #selector(didTapAddSticker(sender:)), for: .touchUpInside)
|
|
|
|
let blurButton = RoundMediaButton(
|
|
image: UIImage(imageLiteralResourceName: "blur-28"),
|
|
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.withTintColor(.ows_black, renderingMode: .alwaysOriginal)
|
|
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: ColorPickerBarViewDelegate {
|
|
|
|
func colorPickerBarView(_ pickerView: ColorPickerBarView, didSelectColor color: ColorPickerBarColor) {
|
|
switch mode {
|
|
case .draw:
|
|
model.color = color
|
|
updateStrokeWidthPreviewColor()
|
|
|
|
default:
|
|
owsAssertDebug(false, "Invalid mode [\(mode)]")
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - StickerPickerDelegate
|
|
|
|
extension ImageEditorViewController: StickerPickerDelegate {
|
|
var storyStickerConfiguration: StoryStickerConfiguration {
|
|
.showWithDelegate(self)
|
|
}
|
|
|
|
func didSelectSticker(stickerInfo: StickerInfo) {
|
|
let stickerItem = imageEditorView.createNewStickerItem(with: .regular(stickerInfo))
|
|
selectStickerItem(stickerItem)
|
|
dismiss(animated: true)
|
|
}
|
|
}
|
|
|
|
extension ImageEditorViewController: StoryStickerPickerDelegate {
|
|
func didSelect(storySticker: EditorSticker.StorySticker) {
|
|
let stickerItem = imageEditorView.createNewStickerItem(with: .story(storySticker))
|
|
selectStickerItem(stickerItem)
|
|
dismiss(animated: true)
|
|
}
|
|
}
|