Skip to content

Commit

Permalink
feat: Undo/Redo Feature for Mantis (#379)
Browse files Browse the repository at this point in the history
* Undo/Redo Feature for Mantis

* Addressing comments in PR

* Add MantisExampleTests target back to project - somehow got deleted.

* Adressing Comments in Chante List:
- added "enableUndo" flag in Config
- undo/redo/reset logic checks the "enableUndo" flag in cropViewController to enable functionality
- abstracted the TransformRecord logic into the CropViewController class
- removed UIButtons from TransformDelegate
- moved TransformDelegate in to new file in the Protocols folder

* added logic to validate the Catalyst menus (disable undo menus when "enableUndo" is false in Config file.

* Address latest round of comments
- More private variables.
- Embedded controller uses proxy functions to communicate with CropViewController state.
- Defaults function implementations in delegate methods.

* Rename some delegate methods:
cropViewControllerDidEnableUndo -> cropViewControllerDidUpdateEnableStateForUndo
cropViewControllerDidEnableRedo -> cropViewControllerDidUpdateEnableStateForRedo
cropViewControllerDidEnableReset -> cropViewControllerDidUpdateEnableStateForReset

* TransformStack private now

* changed actionString declaration.

* removed unwanted space

* Removed some old code, that was not needed anymore when the function signature changed.

* removed  func cropViewControllerDidImageTransformed(_ cropViewController: CropViewController) from CropViewControllerDelegate

* Remove spaces

* removed public from some data types:
TransformType
TransformRecord
TransformStack

* TransformDelegate methods no longer public

* moved opening brackets up to end of function declarations

* removed public modifier in all variables and function signatures in TransformRecord

* removed public modifier from all variables and function signatures in TransformStack

* Placeholder localization strings for "Change Crop" and "Reset Changes"

* Localized "Change Crop" and "Reset Changes" using Google Translate.

* updated the README.md file to describe Undo/Redo support.

* Changed Chinese language localization strings

* Refactoring work:
- rename TransformDelegate methods
- make TransformStack init a private method

* - Fixed a localization bug
- Removed a dead deprecation directive

* cleaned up property list for TransformRecord

* made dictionary passed to TransformRecord have strong type in value field

* changed transformDelegate to an optional

* made private some variables in TransformStack

* Add new files to Mantis.xcodeproj:
- TransformStack.swift
- TransformRecord.swift
- TransformDelegate.swift

* remove dead code

* Rename delegate methods in CropViewControllerDelegate:
- cropViewControllerDidUpdateEnableStateForUndo(_ enable: Bool) -> cropViewController(_ cropViewController: CropViewController, didUpdateEnableStateForUndo enable: Bool)

- cropViewControllerDidUpdateEnableStateForRedo(_ enable: Bool) -> cropViewController(_ cropViewController: CropViewController, didUpdateEnableStateForRedo enable: Bool)

- cropViewControllerDidUpdateEnableStateForReset(_ enable: Bool) -> cropViewController(_ cropViewController: CropViewController, didUpdateEnableStateForReset enable: Bool)

* Remove blanl line

* Simplify an optional value boolean test

* Remove three unused variables from TransformStack

* Address a SwiftLint warning in TransformRecord

* Address a SwiftLint warning in TransformRecord

* Fixed Unit Tests in Mantis.xcodeproj related to new API for undo/redo feature.

* remove first call to transform() in applyCropState() and just set the viewModel's cropBoxFrame to the transform.maskFrame

* moved function "pushTransformRecordOntoStack" out of CropViewController and in to TransformStack class

* added bounds check in TransformStack.popTransformStack()

* added Unit Test for TransformStack

* Updated TransformStack Unit Test.
  • Loading branch information
rickshane committed Mar 24, 2024
1 parent 3cc3707 commit 9a1e67a
Show file tree
Hide file tree
Showing 42 changed files with 957 additions and 179 deletions.
37 changes: 37 additions & 0 deletions Example/AppDelegate.swift
Expand Up @@ -40,3 +40,40 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}
}

extension AppDelegate {

override func buildMenu(with builder: UIMenuBuilder) {
super.buildMenu(with: builder)

// Ensure that the builder is modifying the menu bar system.
guard builder.system == UIMenuSystem.main else { return }

// remove items from Edit menu
builder.remove(menu: .undoRedo)

// Undo
let undoCommand = UIKeyCommand(title: "Undo",
action: #selector(EmbeddedCropViewController.undoButtonPressed(_:)),
input: "z",
modifierFlags: [.command])


// Redo
let redoCommand = UIKeyCommand(title: "Redo",
action: #selector(EmbeddedCropViewController.redoButtonPressed(_:)),
input: "z",
modifierFlags: [.shift, .command])

// Revert
let revertCommand = UIKeyCommand(title: "Revert to Original",
action: #selector(EmbeddedCropViewController.resetButtonPressed(_:)),
input: "r",
modifierFlags: [.alternate])

let undoMenu = UIMenu(title: "", identifier: .undoRedo, options: .displayInline, children: [undoCommand, redoCommand, revertCommand ])

builder.insertChild(undoMenu, atStartOfMenu: .edit)

}
}
29 changes: 26 additions & 3 deletions Example/Base.lproj/Main.storyboard
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="22505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22504"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
Expand Down Expand Up @@ -174,7 +174,7 @@
</constraints>
</stackView>
<imageView userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="sunflower" translatesAutoresizingMaskIntoConstraints="NO" id="pUS-Eo-0ui" userLabel="Cropped Image">
<rect key="frame" x="32" y="16" width="311" height="342"/>
<rect key="frame" x="32" y="36" width="311" height="322"/>
</imageView>
</subviews>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
Expand Down Expand Up @@ -247,6 +247,23 @@
<action selector="cancel:" destination="aIe-ND-Zfg" id="j3e-ai-8lv"/>
</connections>
</barButtonItem>
<barButtonItem title="Undo" id="HSP-G5-UOE">
<color key="tintColor" systemColor="systemYellowColor"/>
<connections>
<action selector="undoButtonPressed:" destination="aIe-ND-Zfg" id="8eI-4B-Rt7"/>
</connections>
</barButtonItem>
<barButtonItem title="Redo" id="l1v-fT-hP5">
<color key="tintColor" systemColor="systemYellowColor"/>
<connections>
<action selector="redoButtonPressed:" destination="aIe-ND-Zfg" id="pck-70-va2"/>
</connections>
</barButtonItem>
<barButtonItem title="Reset" id="eee-mM-zNC">
<connections>
<action selector="resetButtonPressed:" destination="aIe-ND-Zfg" id="S6Q-y0-GYz"/>
</connections>
</barButtonItem>
<barButtonItem style="plain" systemItem="flexibleSpace" id="DsY-rM-AWO"/>
<barButtonItem systemItem="bookmarks" id="sQZ-M0-ZLt">
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
Expand All @@ -270,7 +287,10 @@
<connections>
<outlet property="cancelButton" destination="5p4-Ob-R9b" id="kQq-ir-MSf"/>
<outlet property="doneButton" destination="kZP-3N-vXv" id="RPY-W3-vWu"/>
<outlet property="redoButton" destination="l1v-fT-hP5" id="jYe-4F-ear"/>
<outlet property="resetButton" destination="eee-mM-zNC" id="Jfu-qz-XAZ"/>
<outlet property="resolutionLabel" destination="NDR-Th-Zyy" id="rUQ-h6-q2i"/>
<outlet property="undoButton" destination="HSP-G5-UOE" id="BLL-45-Dfn"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="xP6-ML-BEH" userLabel="First Responder" sceneMemberID="firstResponder"/>
Expand Down Expand Up @@ -322,5 +342,8 @@
<systemColor name="systemGray6Color">
<color red="0.94901960784313721" green="0.94901960784313721" blue="0.96862745098039216" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
<systemColor name="systemYellowColor">
<color red="1" green="0.80000000000000004" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
</resources>
</document>
100 changes: 99 additions & 1 deletion Example/EmbeddedCropViewController.swift
Expand Up @@ -14,21 +14,47 @@ class EmbeddedCropViewController: UIViewController {
var image: UIImage?
var cropViewController: CropViewController?

weak var toolbarDelegate: CropToolbarDelegate?

var didGetCroppedImage: ((UIImage) -> Void)?

@IBOutlet weak var cancelButton: UIBarButtonItem!
@IBOutlet weak var doneButton: UIBarButtonItem!
@IBOutlet weak var resolutionLabel: UILabel!

@IBOutlet weak var undoButton: UIBarButtonItem!
@IBOutlet weak var redoButton: UIBarButtonItem!
@IBOutlet weak var resetButton: UIBarButtonItem!

override func viewDidLoad() {
super.viewDidLoad()

undoButton.title = "Undo"
redoButton.title = "Redo"
cancelButton.title = "Cancel"
doneButton.title = "Done"
resetButton.title = "Revert"

resolutionLabel.text = "\(getResolution(image: image) ?? "unknown")"

view.backgroundColor = .black
navigationController?.toolbar.backgroundColor = .black

self.undoButton.isEnabled = false
self.redoButton.isEnabled = false
self.resetButton.isEnabled = false
}

@IBAction func undoButtonPressed(_ sender: Any) {
toolbarDelegate?.didSelectUndo()
}

@IBAction func redoButtonPressed(_ sender: Any) {
toolbarDelegate?.didSelectRedo()
}

@IBAction func resetButtonPressed(_ sender: Any) {
cropViewController?.didSelectReset()
}

@IBAction func cancel(_ sender: Any) {
Expand All @@ -48,9 +74,14 @@ class EmbeddedCropViewController: UIViewController {

var config = Mantis.Config()
config.cropToolbarConfig.mode = .embedded
config.enableUndoRedo = true

config.cropToolbarConfig.toolbarButtonOptions = [.counterclockwiseRotate, .clockwiseRotate, .horizontallyFlip, .verticallyFlip]

Mantis.setupCropViewController(cropViewController, with: image, and: config)

self.cropViewController = cropViewController
self.toolbarDelegate = cropViewController
}

private func getResolution(image: UIImage?) -> String? {
Expand All @@ -59,9 +90,76 @@ class EmbeddedCropViewController: UIViewController {
}
return nil
}

override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {

if action == #selector(EmbeddedCropViewController.undoButtonPressed(_:)) ||
action == #selector(EmbeddedCropViewController.redoButtonPressed(_:)) ||
action == #selector(EmbeddedCropViewController.resetButtonPressed(_:)) {

guard let toolbarDelegate = toolbarDelegate else { return false }

return toolbarDelegate.isUndoSupported()
}

return true
}

override func validate(_ command: UICommand) {

guard let toolbarDelegate = toolbarDelegate else { return }

if toolbarDelegate.isUndoSupported() {

if command.action == #selector(EmbeddedCropViewController.undoButtonPressed(_:)) {

let undoString = NSLocalizedString("Undo", comment: "Undo")

command.title = self.undoButton.isEnabled ? "\(undoString) \(toolbarDelegate.undoActionName())" : undoString

if !self.undoButton.isEnabled {
command.attributes = [.disabled]
}
}

if command.action == #selector(EmbeddedCropViewController.redoButtonPressed(_:)) {

let redoString = NSLocalizedString("Redo", comment: "Redo")

command.title = self.redoButton.isEnabled ? "\(redoString) \(toolbarDelegate.redoActionName())" : redoString

if !self.redoButton.isEnabled {
command.attributes = [.disabled]
}
}

if command.action == #selector(EmbeddedCropViewController.resetButtonPressed(_:)) {

command.title = NSLocalizedString("Revert to Original", comment: "Revert to Original")

if !self.resetButton.isEnabled {
command.attributes = [.disabled]
}
}
}
}

}

extension EmbeddedCropViewController: CropViewControllerDelegate {

func cropViewController(_ cropViewController: CropViewController, didUpdateEnableStateForUndo enable: Bool) {
self.undoButton.isEnabled = enable
}

func cropViewController(_ cropViewController: CropViewController, didUpdateEnableStateForRedo enable: Bool) {
self.redoButton.isEnabled = enable
}

func cropViewController(_ cropViewController: CropViewController, didUpdateEnableStateForReset enable: Bool) {
self.resetButton.isEnabled = enable
}

func cropViewControllerDidCrop(_ cropViewController: CropViewController,
cropped: UIImage,
transformation: Transformation,
Expand All @@ -82,5 +180,5 @@ extension EmbeddedCropViewController: CropViewControllerDelegate {
let size = cropViewController.getExpectedCropImageSize()
self.resolutionLabel.text = "\(Int(size.width)) x \(Int(size.height)) pixels"
}

}

0 comments on commit 9a1e67a

Please sign in to comment.