Creating a UIImageView outside a view hierarchy now crashes on iOS 18. We can avoid this offscreen rendering technique by removing UIImage.asTintedImage(), configuring the tint colour and template rendering on the UIImage directly, and rendering with UIGraphicsImageRenderer instead of lower-level Core Graphics APIs.
671 lines
24 KiB
Swift
671 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: .alwaysTemplate)
|
|
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)
|
|
}
|
|
}
|