diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 20f328642..8a0807a9d 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -33,6 +33,8 @@ 3453D8EA1EC0D4ED003F9E6F /* OWSAlerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3453D8E91EC0D4ED003F9E6F /* OWSAlerts.swift */; }; 345671011E89A5F1006EE662 /* ThreadUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = 345671001E89A5F1006EE662 /* ThreadUtil.m */; }; 3456710A1E8A9F5D006EE662 /* TSGenericAttachmentAdapter.m in Sources */ = {isa = PBXBuildFile; fileRef = 345671091E8A9F5D006EE662 /* TSGenericAttachmentAdapter.m */; }; + 346B66311F4E29B200E5122F /* CropScaleImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 346B66301F4E29B200E5122F /* CropScaleImageViewController.swift */; }; + 346B66331F4F08FD00E5122F /* IMG_4187.PNG in Resources */ = {isa = PBXBuildFile; fileRef = 346B66321F4F08FD00E5122F /* IMG_4187.PNG */; }; 3471B1DA1EB7C63600F6AEC8 /* NewNonContactConversationViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 3471B1D91EB7C63600F6AEC8 /* NewNonContactConversationViewController.m */; }; 3472229F1EB22FFE00E53955 /* AddToGroupViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 3472229E1EB22FFE00E53955 /* AddToGroupViewController.m */; }; 348F2EAE1F0D21BC00D4ECE0 /* DeviceSleepManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 348F2EAD1F0D21BC00D4ECE0 /* DeviceSleepManager.swift */; }; @@ -439,6 +441,8 @@ 345671001E89A5F1006EE662 /* ThreadUtil.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ThreadUtil.m; sourceTree = ""; }; 345671081E8A9F5D006EE662 /* TSGenericAttachmentAdapter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TSGenericAttachmentAdapter.h; sourceTree = ""; }; 345671091E8A9F5D006EE662 /* TSGenericAttachmentAdapter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSGenericAttachmentAdapter.m; sourceTree = ""; }; + 346B66301F4E29B200E5122F /* CropScaleImageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CropScaleImageViewController.swift; sourceTree = ""; }; + 346B66321F4F08FD00E5122F /* IMG_4187.PNG */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = IMG_4187.PNG; sourceTree = ""; }; 3471B1D81EB7C63600F6AEC8 /* NewNonContactConversationViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NewNonContactConversationViewController.h; sourceTree = ""; }; 3471B1D91EB7C63600F6AEC8 /* NewNonContactConversationViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NewNonContactConversationViewController.m; sourceTree = ""; }; 3472229D1EB22FFE00E53955 /* AddToGroupViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AddToGroupViewController.h; sourceTree = ""; }; @@ -1000,6 +1004,7 @@ 3448BFC01EDF0EA7005B2D69 /* ConversationView */, 34B3F8401E8DF1700035BE1A /* CountryCodeViewController.h */, 34B3F8411E8DF1700035BE1A /* CountryCodeViewController.m */, + 346B66301F4E29B200E5122F /* CropScaleImageViewController.swift */, 34D8C0221ED3673300188D7C /* DebugUI */, 3497DBED1ECE2E4700DB2605 /* DomainFrontingCountryViewController.h */, 3497DBEE1ECE2E4700DB2605 /* DomainFrontingCountryViewController.m */, @@ -1555,6 +1560,7 @@ AD83FF381A73426500B5C81A /* audio_pause_button_blue.png */, AD83FF391A73426500B5C81A /* audio_pause_button_blue@2x.png */, AD83FF3A1A73426500B5C81A /* audio_play_button_blue@2x.png */, + 346B66321F4F08FD00E5122F /* IMG_4187.PNG */, AD83FF3B1A73426500B5C81A /* audio_play_button.png */, AD83FF3C1A73426500B5C81A /* audio_play_button@2x.png */, AD83FF3D1A73426500B5C81A /* audio_pause_button.png */, @@ -1974,6 +1980,7 @@ 34330A5C1E787A9800DF2FB9 /* dripicons-v2.ttf in Resources */, B633C5C41A1D190B0059AC12 /* mute_on@2x.png in Resources */, B633C5CE1A1D190B0059AC12 /* quit@2x.png in Resources */, + 346B66331F4F08FD00E5122F /* IMG_4187.PNG in Resources */, AD83FF441A73426500B5C81A /* audio_pause_button.png in Resources */, B6F509971AA53F760068F56A /* Localizable.strings in Resources */, AD41D7B51A6F6F0600241130 /* play_button.png in Resources */, @@ -2372,6 +2379,7 @@ 34C42D661F4734ED0072EC04 /* OWSContactOffersInteraction.m in Sources */, 34B3F8941E8DF1710035BE1A /* SignalsViewController.m in Sources */, 34E8BF381EE9E2FD00F5F4CA /* FingerprintViewScanController.m in Sources */, + 346B66311F4E29B200E5122F /* CropScaleImageViewController.swift in Sources */, 76EB058818170B33006006FC /* PropertyListPreferences.m in Sources */, 34330A611E788EA900DF2FB9 /* AttachmentUploadView.m in Sources */, 34B3F87D1E8DF1700035BE1A /* FullImageViewController.m in Sources */, @@ -2564,7 +2572,11 @@ "DEBUG=1", "$(inherited)", ); - "GCC_PREPROCESSOR_DEFINITIONS[arch=*]" = "DEBUG=1 $(inherited) SSK_BUILDING_FOR_TESTS=1"; + "GCC_PREPROCESSOR_DEFINITIONS[arch=*]" = ( + "DEBUG=1", + "$(inherited)", + "SSK_BUILDING_FOR_TESTS=1", + ); GCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS = YES; GCC_TREAT_INCOMPATIBLE_POINTER_TYPE_WARNINGS_AS_ERRORS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; diff --git a/Signal/Images/IMG_4187.PNG b/Signal/Images/IMG_4187.PNG new file mode 100644 index 000000000..1faf58606 Binary files /dev/null and b/Signal/Images/IMG_4187.PNG differ diff --git a/Signal/src/ViewControllers/AvatarViewHelper.m b/Signal/src/ViewControllers/AvatarViewHelper.m index 9e97d54e9..534ad97ea 100644 --- a/Signal/src/ViewControllers/AvatarViewHelper.m +++ b/Signal/src/ViewControllers/AvatarViewHelper.m @@ -98,6 +98,28 @@ NS_ASSUME_NONNULL_BEGIN } } +- (void)showCropScaleUI +{ + OWSAssert([NSThread isMainThread]); + OWSAssert(self.delegate); + + UIImagePickerController *picker = [[UIImagePickerController alloc] init]; + picker.delegate = self; + picker.sourceType = UIImagePickerControllerSourceTypeSavedPhotosAlbum; + + if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeSavedPhotosAlbum]) { + picker.mediaTypes = [[NSArray alloc] initWithObjects:(NSString *)kUTTypeImage, nil]; + [self.delegate.fromViewController presentViewController:picker + animated:YES + completion:[UIUtil modalCompletionBlock]]; + } +} +//// We resize the avatar to fill a 210x210 square. +//// +//// See: GroupCreateActivity.java in Signal-Android.java. +// UIImage *resizedAvatar = [rawAvatar resizedImageToFillPixelSize:CGSizeMake(210, 210)]; +//[self.delegate avatarDidChange:resizedAvatar]; + /* * Dismissing UIImagePickerController */ @@ -121,11 +143,7 @@ NS_ASSUME_NONNULL_BEGIN UIImage *rawAvatar = [info objectForKey:UIImagePickerControllerOriginalImage]; if (rawAvatar) { - // We resize the avatar to fill a 210x210 square. - // - // See: GroupCreateActivity.java in Signal-Android.java. - UIImage *resizedAvatar = [rawAvatar resizedImageToFillPixelSize:CGSizeMake(210, 210)]; - [self.delegate avatarDidChange:resizedAvatar]; + // [self showCropScaleUI:rawAvatar]; } [self.delegate.fromViewController dismissViewControllerAnimated:YES completion:nil]; diff --git a/Signal/src/ViewControllers/CropScaleImageViewController.swift b/Signal/src/ViewControllers/CropScaleImageViewController.swift new file mode 100644 index 000000000..599e7e7f9 --- /dev/null +++ b/Signal/src/ViewControllers/CropScaleImageViewController.swift @@ -0,0 +1,798 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +import Foundation +import MediaPlayer + +class OWSLayerView: UIView { + var layoutCallback : (() -> Void)? + + required init(frame: CGRect, layoutCallback : @escaping () -> Void) { + self.layoutCallback = layoutCallback + super.init(frame: frame) + } + + required init?(coder aDecoder: NSCoder) { + self.layoutCallback = { + } + super.init(coder: aDecoder) + } + + override var bounds: CGRect { + didSet { + guard let layoutCallback = self.layoutCallback else { + return + } + layoutCallback() + } + } + + override var frame: CGRect { + didSet { + guard let layoutCallback = self.layoutCallback else { + return + } + layoutCallback() + } + } +} + +class CropScaleImageViewController: OWSViewController { + + let TAG = "[CropScaleImageViewController]" + + // MARK: Properties + + let srcImage: UIImage + + var successCompletion: ((UIImage) -> Void)? + + var imageView: UIView? + + var imageLayer: CALayer? + + var dashedBorderLayer: CAShapeLayer? + +// var defaultCropFramePoints: CGRect? +// var currentCropFramePoints: CGRect? + + // In width/height. + let targetAspectRatio: CGFloat = 1.0 + + var srcImageSizePoints: CGSize = CGSize.zero + var unitDefaultCropSizePoints: CGSize = CGSize.zero +// var unitDefaultCropFramePoints : CGRect = CGRect.zero +// coordinate +// var maxUnitTranslation : CGPoint = CGPoint.zero + + // N = Scaled, zoomed in. + let kMaxImageScale: CGFloat = 4.0 + // 1.0 = Unscaled, cropped to fill crop rect. + let kMinImageScale: CGFloat = 1.0 + var imageScale: CGFloat = 1.0 + + // 0 +// var imageTranslation : CGPoint = CGPoint.zero + var srcTranslation: CGPoint = CGPoint.zero +// var maxImageTranslation : CGPoint = CGPoint.zero + +// +// var imageScale : CGFloat = kMinImageScale +// var imageTranslation : CGPoint = CGPoint.zero + +// var videoPlayer: MPMoviePlayerController? +// +// var audioPlayer: OWSAudioAttachmentPlayer? +// var audioStatusLabel: UILabel? +// var audioPlayButton: UIButton? +// var isAudioPlayingFlag = false +// var isAudioPaused = false +// var audioProgressSeconds: CGFloat = 0 +// var audioDurationSeconds: CGFloat = 0 + + // MARK: Initializers + + @available(*, unavailable, message:"use attachment: constructor instead.") + required init?(coder aDecoder: NSCoder) { + self.srcImage = UIImage(named:"fail")! + super.init(coder: aDecoder) + owsFail("\(self.TAG) invalid constructor") + + configureCropAndScale() + } + + required init(srcImage: UIImage, successCompletion : @escaping (UIImage) -> Void) { + self.srcImage = srcImage + self.successCompletion = successCompletion + super.init(nibName: nil, bundle: nil) + + configureCropAndScale() + } + + // MARK: Cropping and Scaling + + private func configureCropAndScale() { + // Size of bounding box that reflects the target aspect ratio, whose longer side = 1. + let unitSquareHeight: CGFloat = (targetAspectRatio >= 1.0 ? 1.0 : 1.0 / targetAspectRatio) + let unitSquareWidth: CGFloat = (targetAspectRatio >= 1.0 ? targetAspectRatio * unitSquareHeight : 1.0) + let unitSquareSize = CGSize(width: unitSquareWidth, height: unitSquareHeight) + + let imageSizePoints = srcImage.size + guard + (imageSizePoints.width > 0 && imageSizePoints.height > 0) else { + return + } + self.srcImageSizePoints = imageSizePoints + + Logger.error("----") + Logger.error("imageSizePoints: \(imageSizePoints)") + Logger.error("unitSquareWidth: \(unitSquareWidth)") + Logger.error("unitSquareHeight: \(unitSquareHeight)") + + // Default + + // The "default" (no scaling, no translation) crop frame, expressed in + // srcImage's coordinate system. + unitDefaultCropSizePoints = defaultCropSizePoints(dstSizePoints:unitSquareSize) +// unitDefaultCropFramePoints = defaultCropFramePoints(dstSizePoints:unitSquareSize) + assert(imageSizePoints.width >= unitDefaultCropSizePoints.width) + assert(imageSizePoints.height >= unitDefaultCropSizePoints.height) + +// maxUnitTranslation = CGPoint(x: + Logger.error("unitDefaultCropSizePoints: \(unitDefaultCropSizePoints)") + srcTranslation = CGPoint(x:(imageSizePoints.width - unitDefaultCropSizePoints.width) * 0.5, + y:(imageSizePoints.height - unitDefaultCropSizePoints.height) * 0.5) + Logger.error("srcTranslation: \(srcTranslation)") +// let maxSrcTranslation = CGPoint(x:(imageSizePoints.width - unitDefaultCropSizePoints.width) * 0.5, +// y:(imageSizePoints.height - unitDefaultCropSizePoints.height) * 0.5) +// srcTranslation = + +// self.defaultCropFramePoints = defaultCropFramePoints +// let maxCropSizePoints = CGSize(width:defaultCropFramePoints.width, +// height:defaultCropFramePoints.height) +// let minCropSizePoints = CGSize(width:defaultCropFramePoints.width / CropScaleImageViewController.kMaxImageScale, +// height:defaultCropFramePoints.height / CropScaleImageViewController.kMaxImageScale) +// Logger.error("defaultCropFramePoints: \(defaultCropFramePoints)") +// Logger.error("maxCropSizePoints: \(maxCropSizePoints)") +// Logger.error("minCropSizePoints: \(minCropSizePoints)") +// +// if currentCropFramePoints == nil { +// currentCropFramePoints = defaultCropFramePoints +// } +// var cropFramePoints = currentCropFramePoints! + } + + private func defaultCropSizePoints(dstSizePoints: CGSize) -> (CGSize) { + assert(srcImageSizePoints.width > 0) + assert(srcImageSizePoints.height > 0) + + let imageAspectRatio = srcImageSizePoints.width / srcImageSizePoints.height + let dstAspectRatio = dstSizePoints.width / dstSizePoints.height + + var dstCropSizePoints = CGSize.zero + if imageAspectRatio > dstAspectRatio { + dstCropSizePoints = CGSize(width: dstSizePoints.width / dstSizePoints.height * srcImageSizePoints.height, height: srcImageSizePoints.height) + } else { + dstCropSizePoints = CGSize(width: srcImageSizePoints.width, height: dstSizePoints.height / dstSizePoints.width * srcImageSizePoints.width) + } + return dstCropSizePoints + } + +// private func defaultCropFramePoints(dstSizePoints: CGSize) -> (CGRect) { +// assert(imageSizePoints.width > 0) +// assert(imageSizePoints.height > 0) +// +// let imageAspectRatio = imageSizePoints.width / imageSizePoints.height +// let dstAspectRatio = dstSizePoints.width / dstSizePoints.height +// +// var dstCropSizePoints = CGSize.zero +// if imageAspectRatio > dstAspectRatio { +// dstCropSizePoints = CGSize(width: dstSizePoints.width / dstSizePoints.height * imageSizePoints.height, height: imageSizePoints.height) +// } else { +// dstCropSizePoints = CGSize(width: imageSizePoints.width, height: dstSizePoints.height / dstSizePoints.width * imageSizePoints.width) +// } +// +// let dstCropOriginPoints = CGPoint.zero +// assert(dstCropOriginPoints.x >= 0) +// assert(dstCropOriginPoints.y >= 0) +// assert(dstCropOriginPoints.x <= dstSizePoints.width - dstCropSizePoints.width) +// assert(dstCropOriginPoints.y <= dstSizePoints.height - dstCropSizePoints.height) +// return CGRect(origin:dstCropOriginPoints, size:dstCropSizePoints) +// } +// + // MARK: View Lifecycle + + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = UIColor.white + + self.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem:.stop, + target:self, + action:#selector(cancelPressed)) + self.navigationItem.title = NSLocalizedString("CROP_SCALE_IMAGE_VIEW_TITLE", + comment: "Title for the 'crop/scale image' dialog.") + + createViews() + } + +// override func viewWillAppear(_ animated: Bool) { +// super.viewWillAppear(animated) +// +// ViewControllerUtils.setAudioIgnoresHardwareMuteSwitch(true) +// } +// +// override func viewWillDisappear(_ animated: Bool) { +// super.viewWillDisappear(animated) +// +// ViewControllerUtils.setAudioIgnoresHardwareMuteSwitch(false) +// } + + // MARK: - Create Views + + private func createViews() { + let previewTopMargin: CGFloat = 30 + let previewHMargin: CGFloat = 20 + + let contentView = UIView() + self.view.addSubview(contentView) + contentView.autoPinWidthToSuperview(withMargin:previewHMargin) + contentView.autoPin(toTopLayoutGuideOf: self, withInset:previewTopMargin) + + createButtonRow(contentView:contentView) + + let imageHMargin: CGFloat = 0 + let imageView = OWSLayerView(frame:CGRect.zero, layoutCallback: {[weak self] _ in + guard let strongSelf = self else { return } + strongSelf.updateImageLayout() + }) + imageView.clipsToBounds = true + self.imageView = imageView + contentView.addSubview(imageView) + imageView.autoPinWidthToSuperview(withMargin:imageHMargin) + imageView.autoVCenterInSuperview() + imageView.autoPinToSquareAspectRatio() + + let imageLayer = CALayer() + self.imageLayer = imageLayer + imageLayer.contents = srcImage.cgImage + imageView.layer.addSublayer(imageLayer) + + let dashedBorderLayer = CAShapeLayer() + self.dashedBorderLayer = dashedBorderLayer + dashedBorderLayer.strokeColor = UIColor.ows_materialBlue().cgColor + dashedBorderLayer.lineDashPattern = [6, 6] + dashedBorderLayer.lineWidth = 2 + dashedBorderLayer.fillColor = nil + imageView.layer.addSublayer(dashedBorderLayer) + + contentView.isUserInteractionEnabled = true + contentView.addGestureRecognizer(UIPinchGestureRecognizer(target: self, action: #selector(handlePinch(sender:)))) + contentView.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(handlePan(sender:)))) + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + updateImageLayout() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + self.view.layoutSubviews() + updateImageLayout() + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + updateImageLayout() + } + +// private func imageSizeAndViewSizePoints(imageView: UIView) -> (CGSize?, CGSize?) { +// let imageSizePoints = srcImage.size +// guard +// (imageSizePoints.width > 0 && imageSizePoints.height > 0) else { +// return (nil, nil) +// } +// +// let viewSizePoints = imageView.frame.size +// guard +// (viewSizePoints.width > 0 && viewSizePoints.height > 0) else { +// return (nil, nil) +// } +// +// return (imageSizePoints, viewSizePoints) +// } + + private func defaultCropFramePoints(imageSizePoints: CGSize, viewSizePoints: CGSize) -> (CGRect) { + let imageAspectRatio = imageSizePoints.width / imageSizePoints.height + let viewAspectRatio = viewSizePoints.width / viewSizePoints.height + + var defaultCropSizePoints = CGSize.zero + if imageAspectRatio > viewAspectRatio { + defaultCropSizePoints = CGSize(width: viewSizePoints.width / viewSizePoints.height * imageSizePoints.height, height: imageSizePoints.height) + } else { + defaultCropSizePoints = CGSize(width: imageSizePoints.width, height: viewSizePoints.height / viewSizePoints.width * imageSizePoints.width) + } + + let defaultCropOriginPoints = CGPoint(x: (imageSizePoints.width - defaultCropSizePoints.width) * 0.5, + y: (imageSizePoints.height - defaultCropSizePoints.height) * 0.5) + assert(defaultCropOriginPoints.x >= 0) + assert(defaultCropOriginPoints.y >= 0) + assert(defaultCropOriginPoints.x <= imageSizePoints.width - defaultCropSizePoints.width) + assert(defaultCropOriginPoints.y <= imageSizePoints.height - defaultCropSizePoints.height) + return CGRect(origin:defaultCropOriginPoints, size:defaultCropSizePoints) + } + +// private func imageBaseSizeAndOffset(imageSize:CGSize,viewSize:CGSize) -> (CGSize, CGPoint) +// { +// let imageAspectRatio = imageSize.width / imageSize.height +// let viewAspectRatio = viewSize.width / viewSize.height +// +// var imageBaseSize = CGSize.zero +// if imageAspectRatio > viewAspectRatio { +// imageBaseSize = CGSize(width: imageSize.width / imageSize.height * viewSize.height, height: viewSize.height) +// } else { +// imageBaseSize = CGSize(width: viewSize.width, height: imageSize.height / imageSize.width * viewSize.width) +// } +// +// let imageBaseOffset = CGPoint(x: (imageBaseSize.width - viewSize.width) * -0.5, +// y: (imageBaseSize.height - viewSize.height) * -0.5) +// +// return (imageBaseSize, imageBaseOffset) +// } + + private func updateImageLayout() { + guard let imageView = self.imageView else { + return + } + guard let imageLayer = self.imageLayer else { + return + } + guard let dashedBorderLayer = self.dashedBorderLayer else { + return + } + guard srcImageSizePoints.width > 0 && srcImageSizePoints.height > 0 else { + return + } + guard unitDefaultCropSizePoints.width > 0 && unitDefaultCropSizePoints.height > 0 else { + return + } + +// var imageSizePoints : CGSize = CGSize.zero +// var unitDefaultCropSizePoints : CGSize = CGSize.zero + +// let imageSizePoints = srcImage.size +// guard +// (imageSizePoints.width > 0 && imageSizePoints.height > 0) else { +// return +// } + + let viewSizePoints = imageView.frame.size + guard + (viewSizePoints.width > 0 && viewSizePoints.height > 0) else { + return + } + +// let (srcSizePointsOptional, viewSizePointsOptional) = imageSizeAndViewSizePoints(imageView:imageView) +// guard let srcSizePoints = srcSizePointsOptional else { +// return +// } +// guard let viewSizePoints = viewSizePointsOptional else { +// return +// } + Logger.error("----") + Logger.error("srcImageSizePoints: \(srcImageSizePoints)") + Logger.error("viewSizePoints: \(viewSizePoints)") + +// let viewDefaultCropSizePoints = defaultCropSizePoints(dstSizePoints:viewSizePoints) +// assert(viewDefaultCropSizePoints.width >= unitDefaultCropSizePoints.width) +// assert(viewDefaultCropSizePoints.height >= unitDefaultCropSizePoints.height) +// +// Logger.error("viewDefaultCropSizePoints: \(viewDefaultCropSizePoints)") + + Logger.error("imageScale: \(imageScale)") + imageScale = max(kMinImageScale, min(kMaxImageScale, imageScale)) + Logger.error("imageScale (normalized): \(imageScale)") + + let srcCropSizePoints = CGSize(width:unitDefaultCropSizePoints.width / imageScale, + height:unitDefaultCropSizePoints.height / imageScale) + + Logger.error("srcCropSizePoints: \(srcCropSizePoints)") + + let minSrcTranslationPoints = CGPoint.zero + let maxSrcTranslationPoints = CGPoint(x:srcImageSizePoints.width - srcCropSizePoints.width, + y:srcImageSizePoints.height - srcCropSizePoints.height + ) + + Logger.error("minSrcTranslationPoints: \(minSrcTranslationPoints)") + Logger.error("maxSrcTranslationPoints: \(maxSrcTranslationPoints)") + + Logger.error("srcTranslation: \(srcTranslation)") + srcTranslation = CGPoint(x: max(minSrcTranslationPoints.x, min(maxSrcTranslationPoints.x, srcTranslation.x)), + y: max(minSrcTranslationPoints.y, min(maxSrcTranslationPoints.y, srcTranslation.y))) + Logger.error("srcTranslation (normalized): \(srcTranslation)") + + let srcToViewRatio = viewSizePoints.width / srcCropSizePoints.width + Logger.error("srcToViewRatio: \(srcToViewRatio)") + + let imageViewFrame = CGRect(origin: CGPoint(x:srcTranslation.x * -srcToViewRatio, + y:srcTranslation.y * -srcToViewRatio), + size: CGSize(width:srcImageSizePoints.width * +srcToViewRatio, + height:srcImageSizePoints.height * +srcToViewRatio + )) + Logger.error("imageViewFrame: \(imageViewFrame)") + imageLayer.removeAllAnimations() + imageLayer.frame = imageViewFrame + +// // maxUnitTranslation = CGPoint(x: +// Logger.error("unitDefaultCropSizePoints: \(unitDefaultCropSizePoints)") +// srcTranslation = CGPoint(x:(imageSizePoints.width - unitDefaultCropSizePoints.width) * 0.5, +// y:(imageSizePoints.height - unitDefaultCropSizePoints.height) * 0.5) +// Logger.error("srcTranslation: \(srcTranslation)") +// +// +// // Default +// +// let defaultCropFramePoints = self.defaultCropFramePoints(imageSizePoints:imageSizePoints, viewSizePoints:viewSizePoints) +// self.defaultCropFramePoints = defaultCropFramePoints +// let maxCropSizePoints = CGSize(width:defaultCropFramePoints.width, +// height:defaultCropFramePoints.height) +// let minCropSizePoints = CGSize(width:defaultCropFramePoints.width / CropScaleImageViewController.kMaxImageScale, +// height:defaultCropFramePoints.height / CropScaleImageViewController.kMaxImageScale) +// Logger.error("defaultCropFramePoints: \(defaultCropFramePoints)") +// Logger.error("maxCropSizePoints: \(maxCropSizePoints)") +// Logger.error("minCropSizePoints: \(minCropSizePoints)") +// +// if currentCropFramePoints == nil { +// currentCropFramePoints = defaultCropFramePoints +// } +// var cropFramePoints = currentCropFramePoints! +// +// // Ensure the crop frame has valid origin and size.0 +// cropFramePoints.size.width = max(minCropSizePoints.width, min(maxCropSizePoints.width, cropFramePoints.size.width)) +// cropFramePoints.size.height = max(minCropSizePoints.height, min(maxCropSizePoints.height, cropFramePoints.size.height)) +// let minCropOriginPoints = CGPoint.zero +// let maxCropOriginPoints = CGPoint(x:imageSizePoints.width - cropFramePoints.size.width, +// y:imageSizePoints.height - cropFramePoints.size.height +// ) +// cropFramePoints.origin.x = max(minCropOriginPoints.x, min(maxCropOriginPoints.x, cropFramePoints.origin.x)) +// cropFramePoints.origin.y = max(minCropOriginPoints.y, min(maxCropOriginPoints.y, cropFramePoints.origin.y)) +// +// // Update the property. +// currentCropFramePoints = cropFramePoints +// Logger.error("cropFramePoints: \(cropFramePoints)") +// +// let displayScaleWidth = viewSizePoints.width / cropFramePoints.width +// let displayScaleHeight = viewSizePoints.height / cropFramePoints.height +// let displayScale = (displayScaleWidth + displayScaleHeight) * 0.5 +// let displayFramePoints = CGRect(origin: CGPoint(x:cropFramePoints.origin.x * -displayScale, y:cropFramePoints.origin.y * -displayScale), +// size: CGSize(width:imageSizePoints.width * displayScale, +// height:imageSizePoints.height * displayScale)) +// Logger.error("displayScaleWidth: \(displayScaleWidth)") +// Logger.error("displayScaleHeight: \(displayScaleHeight)") +// Logger.error("displayScale: \(displayScale)") +// Logger.error("displayFramePoints: \(displayFramePoints)") +// imageLayer.frame = displayFramePoints +// Logger.error("imageView: \(imageView.frame)") +// Logger.error("imageLayer: \(displayFramePoints)") + +//// let minCropFramePoints = +//// static let kMaxImageScale : CGFloat = 4.0 +//// static let kMinImageScale : CGFloat = 1.0 +// +// let (imageBaseSize, imageBaseOffset) = imageBaseSizeAndOffset(imageSize:imageSize, viewSize:viewSize) +// +// if cropFramePoints == nil || defaultCropFramePoints == nil { +// defaultCropFramePoints = +// CGRect(origin: imageBaseOffset, size: imageBaseSize) +// cropFramePoints = defaultCropFramePoints +// } +// +//// guard let imageView = self.imageView else { +//// return +//// } +//// guard let imageLayer = self.imageLayer else { +//// return +//// } +//// guard let dashedBorderLayer = self.dashedBorderLayer else { +//// return +//// } +// +//// let imageSize = srcImage.size +//// guard +//// (imageSize.width > 0 && imageSize.height > 0) else { +//// return +//// } +//// +//// let viewSize = imageView.frame.size +//// guard +//// (viewSize.width > 0 && viewSize.height > 0) else { +//// return +//// } +// +// // Base +// +//// let imageAspectRatio = imageSize.width / imageSize.height +//// let viewAspectRatio = viewSize.width / viewSize.height +//// +//// var imageBaseSize = CGSize.zero +//// if imageAspectRatio > viewAspectRatio { +//// imageBaseSize = CGSize(width: imageSize.width / imageSize.height * viewSize.height, height: viewSize.height) +//// } else { +//// imageBaseSize = CGSize(width: viewSize.width, height: imageSize.height / imageSize.width * viewSize.width) +//// } +//// +//// let imageBaseOffset = CGPoint(x: (imageBaseSize.width - viewSize.width) * -0.5, +//// y: (imageBaseSize.height - viewSize.height) * -0.5) +// +// // Display +// +//// assert(imageScale >= CropScaleImageViewController.kMinImageScale) +//// assert(imageScale <= CropScaleImageViewController.kMaxImageScale) +// +//// static let kMaxImageScale : CGFloat = 4.0 +//// static let kMinImageScale : CGFloat = 1.0 +//// +//// var imageScale : CGFloat = kMinImageScale +// +// let imageDisplaySize = CGSize(width: imageBaseSize.width * imageScale, +// height: imageBaseSize.height * imageScale) +// let imageDisplayOffset = CGPoint(x: imageBaseOffset.x + imageTranslation.x * imageScale, +// y: imageBaseOffset.y + imageTranslation.y * imageScale) +// // TODO: Assert that imageDisplayOffset is valid. +//// var imageScale : CGFloat = 1.0 +//// var imageTranslation : CGPoint = CGPoint.zero +// +// imageLayer.frame = CGRect(origin: imageDisplayOffset, size: imageDisplaySize) +// Logger.error("imageView: \(NSStringFromCGRect(imageView.frame))") +// Logger.error("imageLayer: \(imageLayer.frame)") + + dashedBorderLayer.frame = imageView.bounds + dashedBorderLayer.path = UIBezierPath(rect: imageView.bounds).cgPath + } + + var srcTranslationAtPinchStart: CGPoint = CGPoint.zero + var imageScaleAtPinchStart: CGFloat = 0 + +// var currentCropFramePointsAtPinchStart: CGRect = CGRect.zero + var lastPinchLocation: CGPoint = CGPoint.zero + var lastPinchScale: CGFloat = 1.0 +// var isPinching = false + + func handlePinch(sender: UIPinchGestureRecognizer) { + Logger.error("pinch scale: \(sender.scale)") + switch (sender.state) { + case .possible: + break + case .began: + srcTranslationAtPinchStart = srcTranslation + imageScaleAtPinchStart = imageScale + +// guard let currentCropFramePoints = currentCropFramePoints else { +// isPinching = false +// return +// } +// currentCropFramePointsAtPinchStart = currentCropFramePoints +// isPinching = true + lastPinchLocation = + sender.location(in: sender.view) + lastPinchScale = sender.scale + break + case .changed, .ended: + guard let imageView = self.imageView else { + return + } +// guard isPinching else { +// return +// } +// guard let imageView = self.imageView else { +// return +// } +// +// let (_, viewSizePointsOptional) = imageSizeAndViewSizePoints(imageView:imageView) +// guard let viewSizePoints = viewSizePointsOptional else { +// return +// } + +// guard let imageView = self.imageView else { +// return +// } +// let viewSizePoints = imageView.frame.size +// Logger.error("viewSizePoints: \(viewSizePoints)") +// let srcCropSizePoints = CGSize(width:unitDefaultCropSizePoints.width / imageScale, +// height:unitDefaultCropSizePoints.height / imageScale) +// Logger.error("srcCropSizePoints: \(srcCropSizePoints)") +// +// let srcToViewRatio = viewSizePoints.width / srcCropSizePoints.width + + let location = + sender.location(in: sender.view) + let scaleDiff = sender.scale / lastPinchScale + Logger.error("scaling \(lastPinchScale) \(sender.scale) -> \(scaleDiff)") + +// unitDefaultCropSizePoints = defaultCropSizePoints(dstSizePoints:unitSquareSize) +// // unitDefaultCropFramePoints = defaultCropFramePoints(dstSizePoints:unitSquareSize) +// assert(imageSizePoints.width >= unitDefaultCropSizePoints.width) +// assert(imageSizePoints.height >= unitDefaultCropSizePoints.height) + + // maxUnitTranslation = CGPoint(x: +// Logger.error("unitDefaultCropSizePoints: \(unitDefaultCropSizePoints)") +// srcTranslation = CGPoint(x:(imageSizePoints.width - unitDefaultCropSizePoints.width) * 0.5, +// y:(imageSizePoints.height - unitDefaultCropSizePoints.height) * 0.5) + + // Update the scaling + let srcCropSizeBeforeScalePoints = CGSize(width:unitDefaultCropSizePoints.width / imageScale, + height:unitDefaultCropSizePoints.height / imageScale) + imageScale = max(kMinImageScale, min(kMaxImageScale, imageScale * scaleDiff)) + let srcCropSizeAfterScalePoints = CGSize(width:unitDefaultCropSizePoints.width / imageScale, + height:unitDefaultCropSizePoints.height / imageScale) + // Since the translation state reflects the "upper left" corner of the crop region, we need to + // adjust the translation when scaling. + srcTranslation.x += (srcCropSizeBeforeScalePoints.width - srcCropSizeAfterScalePoints.width) * 0.5 + srcTranslation.y += (srcCropSizeBeforeScalePoints.height - srcCropSizeAfterScalePoints.height) * 0.5 + + // Update translation + + let viewSizePoints = imageView.frame.size + Logger.error("viewSizePoints: \(viewSizePoints)") + let srcCropSizePoints = CGSize(width:unitDefaultCropSizePoints.width / imageScale, + height:unitDefaultCropSizePoints.height / imageScale) + Logger.error("srcCropSizePoints: \(srcCropSizePoints)") + + let srcToViewRatio = viewSizePoints.width / srcCropSizePoints.width + Logger.error("srcToViewRatio: \(srcToViewRatio)") + let viewToSrcRatio = 1 / srcToViewRatio + Logger.error("viewToSrcRatio: \(viewToSrcRatio)") + + let gestureTranslation = CGPoint(x:location.x - lastPinchLocation.x, + y:location.y - lastPinchLocation.y) + + Logger.error("gestureTranslation: \(gestureTranslation)") + + // var cropFramePoints = currentCropFramePointsAtPanStart + // cropFramePoints.origin.x += +gestureTranslation.x / viewSizePoints.width * currentCropFramePointsAtPanStart.width + // cropFramePoints.origin.y += -gestureTranslation.y / viewSizePoints.height * currentCropFramePointsAtPanStart.height + // self.currentCropFramePoints = cropFramePoints + + srcTranslation = CGPoint(x:srcTranslation.x + gestureTranslation.x * -viewToSrcRatio, + y:srcTranslation.y + gestureTranslation.y * -viewToSrcRatio) + +// let translationOffset = CGPoint(x:location.x - lastPinchLocation.x, +// y:location.y - lastPinchLocation.y) +// let oldCropFramePoints = self.currentCropFramePoints! +// var newCropFramePoints = oldCropFramePoints +// newCropFramePoints.size.width /= scaleDiff +// newCropFramePoints.size.height /= scaleDiff +// newCropFramePoints.origin.x += (oldCropFramePoints.size.width - newCropFramePoints.size.width) * 0.5 +// newCropFramePoints.origin.y += (oldCropFramePoints.size.height - newCropFramePoints.size.height) * 0.5 +//// cropFramePoints.origin.y += -gestureTranslation.y / viewSizePoints.height * currentCropFramePointsAtPinchStart.height +//// cropFramePoints.origin.x += +gestureTranslation.x / viewSizePoints.width * currentCropFramePointsAtPinchStart.width +//// cropFramePoints.origin.y += -gestureTranslation.y / viewSizePoints.height * currentCropFramePointsAtPinchStart.height +// self.currentCropFramePoints = newCropFramePoints + + lastPinchLocation = location + lastPinchScale = sender.scale + +// if sender.state == .ended { +// isPinching = false +// } + break + case .cancelled, .failed: + srcTranslation = srcTranslationAtPinchStart + imageScale = imageScaleAtPinchStart +// guard isPinching else { +// return +// } +// currentCropFramePoints +// = currentCropFramePointsAtPinchStart +// isPinching = false + break + } + + updateImageLayout() + } + + var srcTranslationAtPanStart: CGPoint = CGPoint.zero + + func handlePan(sender: UIPanGestureRecognizer) { + switch (sender.state) { + case .possible: + break + case .began: + srcTranslationAtPanStart = srcTranslation + break + case .changed, .ended: + guard let imageView = self.imageView else { + return + } + let viewSizePoints = imageView.frame.size + Logger.error("viewSizePoints: \(viewSizePoints)") + let srcCropSizePoints = CGSize(width:unitDefaultCropSizePoints.width / imageScale, + height:unitDefaultCropSizePoints.height / imageScale) + Logger.error("srcCropSizePoints: \(srcCropSizePoints)") + + let srcToViewRatio = viewSizePoints.width / srcCropSizePoints.width + Logger.error("srcToViewRatio: \(srcToViewRatio)") + let viewToSrcRatio = 1 / srcToViewRatio + Logger.error("viewToSrcRatio: \(viewToSrcRatio)") + + let gestureTranslation = + sender.translation(in: sender.view) + + Logger.error("gestureTranslation: \(gestureTranslation)") + +// var cropFramePoints = currentCropFramePointsAtPanStart +// cropFramePoints.origin.x += +gestureTranslation.x / viewSizePoints.width * currentCropFramePointsAtPanStart.width +// cropFramePoints.origin.y += -gestureTranslation.y / viewSizePoints.height * currentCropFramePointsAtPanStart.height +// self.currentCropFramePoints = cropFramePoints + + srcTranslation = CGPoint(x:srcTranslationAtPanStart.x + gestureTranslation.x * -viewToSrcRatio, + y:srcTranslationAtPanStart.y + gestureTranslation.y * -viewToSrcRatio) + break + case .cancelled, .failed: + srcTranslation + = srcTranslationAtPanStart + break + } + + updateImageLayout() + } + + private func createButtonRow(contentView: UIView) { + let buttonTopMargin = ScaleFromIPhone5To7Plus(30, 40) + let buttonBottomMargin = ScaleFromIPhone5To7Plus(25, 40) + + let buttonRow = UIView() + self.view.addSubview(buttonRow) + buttonRow.autoPinWidthToSuperview() + buttonRow.autoPinEdge(toSuperviewEdge:.bottom, withInset:buttonBottomMargin) + buttonRow.autoPinEdge(.top, to:.bottom, of:contentView, withOffset:buttonTopMargin) + + let doneButton = createButton(title: NSLocalizedString("BUTTON_DONE", + comment: "Label for generic done button."), + color : UIColor.ows_materialBlue(), + action: #selector(donePressed)) + buttonRow.addSubview(doneButton) + doneButton.autoPinEdge(toSuperviewEdge:.top) + doneButton.autoPinEdge(toSuperviewEdge:.bottom) + doneButton.autoHCenterInSuperview() + } + + private func createButton(title: String, color: UIColor, action: Selector) -> UIButton { + let buttonFont = UIFont.ows_mediumFont(withSize:ScaleFromIPhone5To7Plus(18, 22)) + let buttonCornerRadius = ScaleFromIPhone5To7Plus(4, 5) + let buttonWidth = ScaleFromIPhone5To7Plus(110, 140) + let buttonHeight = ScaleFromIPhone5To7Plus(35, 45) + + let button = UIButton() + button.setTitle(title, for:.normal) + button.setTitleColor(UIColor.white, for:.normal) + button.titleLabel!.font = buttonFont + button.backgroundColor = color + button.layer.cornerRadius = buttonCornerRadius + button.clipsToBounds = true + button.addTarget(self, action:action, for:.touchUpInside) + button.autoSetDimension(.width, toSize:buttonWidth) + button.autoSetDimension(.height, toSize:buttonHeight) + return button + } + + // MARK: - Event Handlers + + func cancelPressed(sender: UIButton) { + dismiss(animated: true, completion:nil) + } + + func donePressed(sender: UIButton) { + let successCompletion = self.successCompletion + dismiss(animated: true, completion: { + // TODO + let dstImage = self.srcImage + successCompletion?(dstImage) + }) + } +} diff --git a/Signal/src/ViewControllers/SignalsViewController.m b/Signal/src/ViewControllers/SignalsViewController.m index 86d63a7be..e5ccbbcdc 100644 --- a/Signal/src/ViewControllers/SignalsViewController.m +++ b/Signal/src/ViewControllers/SignalsViewController.m @@ -271,6 +271,17 @@ typedef NS_ENUM(NSInteger, CellState) { kArchiveState, kInboxState }; } [self updateBarButtonItems]; + + dispatch_async(dispatch_get_main_queue(), ^{ + UIImage *srcDmage = [UIImage imageNamed:@"IMG_4187.PNG"]; + OWSAssert(srcDmage); + CropScaleImageViewController *vc = + [[CropScaleImageViewController alloc] initWithSrcImage:srcDmage + successCompletion:^(UIImage *_Nonnull dstImage){ + }]; + OWSNavigationController *navigationController = [[OWSNavigationController alloc] initWithRootViewController:vc]; + [self presentTopLevelModalViewController:navigationController animateDismissal:NO animatePresentation:YES]; + }); } - (void)updateBarButtonItems { diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index dc0785757..8cdef9d5b 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -205,6 +205,9 @@ /* Title format for action sheet that offers to block an unknown user.Embeds {{the unknown user's name or phone number}}. */ "BLOCK_OFFER_ACTIONSHEET_TITLE_FORMAT" = "Block %@?"; +/* Label for generic done button. */ +"BUTTON_DONE" = "Done"; + /* Alert message when calling and permissions for microphone are missing */ "CALL_AUDIO_PERMISSION_MESSAGE" = "Signal requires access to your microphone to make calls and record voice messages. You can grant this permission in the Settings app."; @@ -385,6 +388,9 @@ /* Accessibility label for the create group new group button */ "CREATE_NEW_GROUP" = "Create new group"; +/* Title for the 'crop/scale image' dialog. */ +"CROP_SCALE_IMAGE_VIEW_TITLE" = "Crop Image"; + /* Subtitle shown while the app is updating its database. */ "DATABASE_VIEW_OVERLAY_SUBTITLE" = "This can take a few minutes.";