diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 416e1822a..5d89294e2 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -15,6 +15,7 @@ 34080F0022282C880087E99F /* AttachmentCaptionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34080EFF22282C880087E99F /* AttachmentCaptionViewController.swift */; }; 34080F02222853E30087E99F /* ImageEditorBrushViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34080F01222853E30087E99F /* ImageEditorBrushViewController.swift */; }; 34080F04222858DC0087E99F /* OWSViewController+ImageEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34080F03222858DC0087E99F /* OWSViewController+ImageEditor.swift */; }; + 340872BF22393CFA00CB25B0 /* UIGestureRecognizer+OWS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 340872BE22393CF900CB25B0 /* UIGestureRecognizer+OWS.swift */; }; 340B02BA1FA0D6C700F9CFEC /* ConversationViewItemTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 340B02B91FA0D6C700F9CFEC /* ConversationViewItemTest.m */; }; 340FC8A9204DAC8D007AEB0F /* NotificationSettingsOptionsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 340FC87B204DAC8C007AEB0F /* NotificationSettingsOptionsViewController.m */; }; 340FC8AA204DAC8D007AEB0F /* NotificationSettingsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 340FC87C204DAC8C007AEB0F /* NotificationSettingsViewController.m */; }; @@ -644,6 +645,7 @@ 34080EFF22282C880087E99F /* AttachmentCaptionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentCaptionViewController.swift; sourceTree = ""; }; 34080F01222853E30087E99F /* ImageEditorBrushViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageEditorBrushViewController.swift; sourceTree = ""; }; 34080F03222858DC0087E99F /* OWSViewController+ImageEditor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OWSViewController+ImageEditor.swift"; sourceTree = ""; }; + 340872BE22393CF900CB25B0 /* UIGestureRecognizer+OWS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIGestureRecognizer+OWS.swift"; sourceTree = ""; }; 340B02B61F9FD31800F9CFEC /* he */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = he; path = translations/he.lproj/Localizable.strings; sourceTree = ""; }; 340B02B91FA0D6C700F9CFEC /* ConversationViewItemTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ConversationViewItemTest.m; sourceTree = ""; }; 340FC87B204DAC8C007AEB0F /* NotificationSettingsOptionsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NotificationSettingsOptionsViewController.m; sourceTree = ""; }; @@ -1585,12 +1587,12 @@ 4C948FF62146EB4800349F0D /* BlockListCache.swift */, 343D3D991E9283F100165CA4 /* BlockListUIUtils.h */, 343D3D9A1E9283F100165CA4 /* BlockListUIUtils.m */, - 451777C71FD61554001225FF /* FullTextSearcher.swift */, 3466087120E550F300AFFE73 /* ConversationStyle.swift */, 34480B4D1FD0A7A300BC14EF /* DebugLogger.h */, 34480B4E1FD0A7A300BC14EF /* DebugLogger.m */, 348F2EAD1F0D21BC00D4ECE0 /* DeviceSleepManager.swift */, 344F248C2007CCD600CFB4F4 /* DisplayableText.swift */, + 451777C71FD61554001225FF /* FullTextSearcher.swift */, 346129AC1FD1F34E00532771 /* ImageCache.swift */, 34BEDB1421C80BC9007B0EAE /* OWSAnyTouchGestureRecognizer.h */, 34BEDB1521C80BCA007B0EAE /* OWSAnyTouchGestureRecognizer.m */, @@ -1617,6 +1619,7 @@ 45360B8C1F9521F800FA666C /* Searcher.swift */, 346129BD1FD2068600532771 /* ThreadUtil.h */, 346129BE1FD2068600532771 /* ThreadUtil.m */, + 340872BE22393CF900CB25B0 /* UIGestureRecognizer+OWS.swift */, 4C858A51212DC5E1001B45D3 /* UIImage+OWS.swift */, B97940251832BD2400BD66CB /* UIUtil.h */, B97940261832BD2400BD66CB /* UIUtil.m */, @@ -3416,6 +3419,7 @@ 346941A3215D2EE400B5BFAD /* Theme.m in Sources */, 4C23A5F2215C4ADE00534937 /* SheetViewController.swift in Sources */, 34BBC84D220B2D0800857249 /* ImageEditorPinchGestureRecognizer.swift in Sources */, + 340872BF22393CFA00CB25B0 /* UIGestureRecognizer+OWS.swift in Sources */, 34080F02222853E30087E99F /* ImageEditorBrushViewController.swift in Sources */, 34AC0A14211B39EA00997B47 /* ContactCellView.m in Sources */, 34AC0A15211B39EA00997B47 /* ContactsViewHelper.m in Sources */, diff --git a/SignalMessaging/Views/ImageEditor/ImageEditorCropViewController.swift b/SignalMessaging/Views/ImageEditor/ImageEditorCropViewController.swift index 2afb844b6..3a78f54f6 100644 --- a/SignalMessaging/Views/ImageEditor/ImageEditorCropViewController.swift +++ b/SignalMessaging/Views/ImageEditor/ImageEditorCropViewController.swift @@ -214,7 +214,6 @@ class ImageEditorCropViewController: OWSViewController { } public func updateNavigationBar() { - // TODO: Change this asset. let resetButton = navigationBarButton(imageName: "image_editor_undo", selector: #selector(didTapReset(sender:))) let doneButton = navigationBarButton(imageName: "image_editor_checkmark_full", diff --git a/SignalMessaging/Views/ImageEditor/ImageEditorPaletteView.swift b/SignalMessaging/Views/ImageEditor/ImageEditorPaletteView.swift index 746ba5303..7101b333d 100644 --- a/SignalMessaging/Views/ImageEditor/ImageEditorPaletteView.swift +++ b/SignalMessaging/Views/ImageEditor/ImageEditorPaletteView.swift @@ -54,6 +54,10 @@ public class ImageEditorColor: NSObject { return color.cgColor }) } + + static func ==(left: ImageEditorColor, right: ImageEditorColor) -> Bool { + return left.palettePhase.fuzzyEquals(right.palettePhase) + } } // MARK: - diff --git a/SignalMessaging/Views/ImageEditor/ImageEditorTextItem.swift b/SignalMessaging/Views/ImageEditor/ImageEditorTextItem.swift index fe08e1adc..4d6c015aa 100644 --- a/SignalMessaging/Views/ImageEditor/ImageEditorTextItem.swift +++ b/SignalMessaging/Views/ImageEditor/ImageEditorTextItem.swift @@ -131,42 +131,54 @@ public class ImageEditorTextItem: ImageEditorItem { } @objc - public func copy(withUnitCenter newUnitCenter: CGPoint) -> ImageEditorTextItem { + public func copy(unitCenter: CGPoint) -> ImageEditorTextItem { return ImageEditorTextItem(itemId: itemId, text: text, color: color, font: font, fontReferenceImageWidth: fontReferenceImageWidth, - unitCenter: newUnitCenter, + unitCenter: unitCenter, + unitWidth: unitWidth, + rotationRadians: rotationRadians, + scaling: scaling) + } + + @objc + public func copy(scaling: CGFloat, + rotationRadians: CGFloat) -> ImageEditorTextItem { + return ImageEditorTextItem(itemId: itemId, + text: text, + color: color, + font: font, + fontReferenceImageWidth: fontReferenceImageWidth, + unitCenter: unitCenter, unitWidth: unitWidth, rotationRadians: rotationRadians, scaling: scaling) } @objc - public func copy(withUnitCenter newUnitCenter: CGPoint, - scaling newScaling: CGFloat, - rotationRadians newRotationRadians: CGFloat) -> ImageEditorTextItem { + public func copy(unitWidth: CGFloat) -> ImageEditorTextItem { return ImageEditorTextItem(itemId: itemId, text: text, color: color, font: font, fontReferenceImageWidth: fontReferenceImageWidth, - unitCenter: newUnitCenter, + unitCenter: unitCenter, unitWidth: unitWidth, - rotationRadians: newRotationRadians, - scaling: newScaling) + rotationRadians: rotationRadians, + scaling: scaling) } @objc - public func copy(withUnitCenter newUnitCenter: CGPoint, unitWidth newUnitWidth: CGFloat) -> ImageEditorTextItem { + public func copy(font: UIFont) -> ImageEditorTextItem { return ImageEditorTextItem(itemId: itemId, text: text, color: color, font: font, fontReferenceImageWidth: fontReferenceImageWidth, - unitCenter: newUnitCenter, - unitWidth: newUnitWidth, + unitCenter: unitCenter, + unitWidth: unitWidth, rotationRadians: rotationRadians, scaling: scaling) } @@ -174,4 +186,16 @@ public class ImageEditorTextItem: ImageEditorItem { public override func outputScale() -> CGFloat { return scaling } + + static func ==(left: ImageEditorTextItem, right: ImageEditorTextItem) -> Bool { + return (left.text == right.text && + left.color == right.color && + left.font.fontName == right.font.fontName && + left.font.pointSize.fuzzyEquals(right.font.pointSize) && + left.fontReferenceImageWidth.fuzzyEquals(right.fontReferenceImageWidth) && + left.unitCenter.fuzzyEquals(right.unitCenter) && + left.unitWidth.fuzzyEquals(right.unitWidth) && + left.rotationRadians.fuzzyEquals(right.rotationRadians) && + left.scaling.fuzzyEquals(right.scaling)) + } } diff --git a/SignalMessaging/Views/ImageEditor/ImageEditorTextViewController.swift b/SignalMessaging/Views/ImageEditor/ImageEditorTextViewController.swift index 8292582de..b99dfee94 100644 --- a/SignalMessaging/Views/ImageEditor/ImageEditorTextViewController.swift +++ b/SignalMessaging/Views/ImageEditor/ImageEditorTextViewController.swift @@ -92,7 +92,8 @@ private class VAlignTextView: UITextView { @objc public protocol ImageEditorTextViewControllerDelegate: class { - func textEditDidComplete(textItem: ImageEditorTextItem, text: String?, color: ImageEditorColor) + func textEditDidComplete(textItem: ImageEditorTextItem) + func textEditDidDelete(textItem: ImageEditorTextItem) func textEditDidCancel() } @@ -195,6 +196,10 @@ public class ImageEditorTextViewController: OWSViewController, VAlignTextViewDel // This will determine the text view's size. paletteView.autoPinEdge(.leading, to: .trailing, of: textView, withOffset: 0) + let pinchGestureRecognizer = ImageEditorPinchGestureRecognizer(target: self, action: #selector(handlePinchGesture(_:))) + pinchGestureRecognizer.referenceView = view + view.addGestureRecognizer(pinchGestureRecognizer) + updateNavigationBar() } @@ -230,6 +235,35 @@ public class ImageEditorTextViewController: OWSViewController, VAlignTextViewDel updateNavigationBar(navigationBarItems: navigationBarItems) } + // MARK: - Pinch Gesture + + private var pinchFontStart: UIFont? + + @objc + public func handlePinchGesture(_ gestureRecognizer: ImageEditorPinchGestureRecognizer) { + AssertIsOnMainThread() + + switch gestureRecognizer.state { + case .began: + pinchFontStart = textView.font + case .changed, .ended: + guard let pinchFontStart = pinchFontStart else { + return + } + var pointSize: CGFloat = pinchFontStart.pointSize + if gestureRecognizer.pinchStateLast.distance > 0 { + pointSize *= gestureRecognizer.pinchStateLast.distance / gestureRecognizer.pinchStateStart.distance + } + let minPointSize: CGFloat = 12 + let maxPointSize: CGFloat = 64 + pointSize = max(minPointSize, min(maxPointSize, pointSize)) + let font = pinchFontStart.withSize(pointSize) + textView.font = font + default: + pinchFontStart = nil + } + } + // MARK: - Events @objc func didTapUndo(sender: UIButton) { @@ -268,14 +302,39 @@ public class ImageEditorTextViewController: OWSViewController, VAlignTextViewDel imageSize: model.srcImageSizePixels, transform: model.currentTransform()) let unitWidth = textView.width() / imageFrame.width + newTextItem = textItem.copy(unitCenter: textCenterImageUnit).copy(unitWidth: unitWidth) + } + + var font = textItem.font + if let newFont = textView.font { + font = newFont + } else { + owsFailDebug("Missing font.") + } + newTextItem = newTextItem.copy(font: font) - newTextItem = textItem.copy(withUnitCenter: textCenterImageUnit, unitWidth: unitWidth) + guard let text = textView.text?.ows_stripped(), + text.count > 0 else { + self.delegate?.textEditDidDelete(textItem: textItem) + + self.dismiss(animated: false) { + // Do nothing. + } + + return } + newTextItem = newTextItem.copy(withText: text, color: paletteView.selectedValue) + // Hide the text view immediately to avoid animation glitches in the dismiss transition. textView.isHidden = true - self.delegate?.textEditDidComplete(textItem: newTextItem, text: textView.text, color: paletteView.selectedValue) + if textItem == newTextItem { + // No changes were made. Cancel to avoid dirtying the undo stack. + self.delegate?.textEditDidCancel() + } else { + self.delegate?.textEditDidComplete(textItem: newTextItem) + } self.dismiss(animated: false) { // Do nothing. diff --git a/SignalMessaging/Views/ImageEditor/ImageEditorView.swift b/SignalMessaging/Views/ImageEditor/ImageEditorView.swift index 3acac8e1c..cfbebc097 100644 --- a/SignalMessaging/Views/ImageEditor/ImageEditorView.swift +++ b/SignalMessaging/Views/ImageEditor/ImageEditorView.swift @@ -260,9 +260,8 @@ public class ImageEditorView: UIView { let newRotationRadians = textItem.rotationRadians + gestureRecognizer.pinchStateLast.angleRadians - gestureRecognizer.pinchStateStart.angleRadians - let newItem = textItem.copy(withUnitCenter: unitCenter, - scaling: newScaling, - rotationRadians: newRotationRadians) + let newItem = textItem.copy(unitCenter: unitCenter).copy(scaling: newScaling, + rotationRadians: newRotationRadians) if pinchHasChanged { model.replace(item: newItem, suppressUndo: true) @@ -348,7 +347,7 @@ public class ImageEditorView: UIView { transform: self.model.currentTransform()) let gestureDeltaImageUnit = gestureNowImageUnit.minus(gestureStartImageUnit) let unitCenter = CGPointClamp01(movingTextStartUnitCenter.plus(gestureDeltaImageUnit)) - let newItem = textItem.copy(withUnitCenter: unitCenter) + let newItem = textItem.copy(unitCenter: unitCenter) if movingTextHasMoved { model.replace(item: newItem, suppressUndo: true) @@ -515,26 +514,25 @@ extension ImageEditorView: ImageEditorModelObserver { extension ImageEditorView: ImageEditorTextViewControllerDelegate { - public func textEditDidComplete(textItem: ImageEditorTextItem, text: String?, color: ImageEditorColor) { + public func textEditDidComplete(textItem: ImageEditorTextItem) { AssertIsOnMainThread() - guard let text = text?.ows_stripped(), - text.count > 0 else { - if model.has(itemForId: textItem.itemId) { - model.remove(item: textItem) - } - return - } - // Model items are immutable; we _replace_ the item rather than modify it. - let newItem = textItem.copy(withText: text, color: color) if model.has(itemForId: textItem.itemId) { - model.replace(item: newItem, suppressUndo: false) + model.replace(item: textItem, suppressUndo: false) } else { - model.append(item: newItem) + model.append(item: textItem) } - self.currentColor = color + self.currentColor = textItem.color + } + + public func textEditDidDelete(textItem: ImageEditorTextItem) { + AssertIsOnMainThread() + + if model.has(itemForId: textItem.itemId) { + model.remove(item: textItem) + } } public func textEditDidCancel() { diff --git a/SignalMessaging/categories/UIView+OWS.swift b/SignalMessaging/categories/UIView+OWS.swift index fb171452c..4c2d93ec7 100644 --- a/SignalMessaging/categories/UIView+OWS.swift +++ b/SignalMessaging/categories/UIView+OWS.swift @@ -145,6 +145,10 @@ public extension CGFloat { } public static let halfPi: CGFloat = CGFloat.pi * 0.5 + + public func fuzzyEquals(_ other: CGFloat, tolerance: CGFloat = 0.001) -> Bool { + return abs(self - other) < tolerance + } } public extension Int { @@ -213,6 +217,11 @@ public extension CGPoint { public func applyingInverse(_ transform: CGAffineTransform) -> CGPoint { return applying(transform.inverted()) } + + public func fuzzyEquals(_ other: CGPoint, tolerance: CGFloat = 0.001) -> Bool { + return (x.fuzzyEquals(other.x, tolerance: tolerance) && + y.fuzzyEquals(other.y, tolerance: tolerance)) + } } public extension CGRect { diff --git a/SignalMessaging/utils/UIGestureRecognizer+OWS.swift b/SignalMessaging/utils/UIGestureRecognizer+OWS.swift new file mode 100644 index 000000000..01dad6400 --- /dev/null +++ b/SignalMessaging/utils/UIGestureRecognizer+OWS.swift @@ -0,0 +1,12 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +import Foundation + +extension UIGestureRecognizer { + @objc + public var stateString: String { + return NSStringForUIGestureRecognizerState(state) + } +}