GBA002/Delta/Pause Menu/Cheats/EditCheatViewController.swift
2020-04-23 16:19:05 -07:00

399 lines
15 KiB
Swift

//
// EditCheatViewController.swift
// Delta
//
// Created by Riley Testut on 5/21/16.
// Copyright © 2016 Riley Testut. All rights reserved.
//
import UIKit
import CoreData
import DeltaCore
import Roxas
protocol EditCheatViewControllerDelegate: class
{
func editCheatViewController(_ editCheatViewController: EditCheatViewController, activateCheat cheat: Cheat, previousCheat: Cheat?)
func editCheatViewController(_ editCheatViewController: EditCheatViewController, deactivateCheat cheat: Cheat)
}
private extension EditCheatViewController
{
enum Section: Int
{
case name
case type
case code
}
}
class EditCheatViewController: UITableViewController
{
var game: Game! {
didSet {
let deltaCore = Delta.core(for: self.game.type)!
self.supportedCheatFormats = deltaCore.supportedCheatFormats.sorted() { $0.name < $1.name }
}
}
var cheat: Cheat?
weak var delegate: EditCheatViewControllerDelegate?
var isPreviewing = false
private var supportedCheatFormats: [CheatFormat]!
private var selectedCheatFormat: CheatFormat {
let cheatFormat = self.supportedCheatFormats[self.typeSegmentedControl.selectedSegmentIndex]
return cheatFormat
}
private var mutableCheat: Cheat!
private var managedObjectContext = DatabaseManager.shared.newBackgroundContext()
@IBOutlet private var nameTextField: UITextField!
@IBOutlet private var typeSegmentedControl: UISegmentedControl!
@IBOutlet private var codeTextView: CheatTextView!
override var previewActionItems: [UIPreviewActionItem]
{
guard let cheat = self.cheat else { return [] }
let copyCodeAction = UIPreviewAction(title: NSLocalizedString("Copy Code", comment: ""), style: .default) { (action, viewController) in
UIPasteboard.general.string = cheat.code
}
let presentingViewController = self.presentingViewController!
let editCheatAction = UIPreviewAction(title: NSLocalizedString("Edit", comment: ""), style: .default) { (action, viewController) in
// Delaying until next run loop prevents self from being dismissed immediately
DispatchQueue.main.async {
let editCheatViewController = viewController as! EditCheatViewController
editCheatViewController.isPreviewing = false
editCheatViewController.presentWithPresentingViewController(presentingViewController)
}
}
let deleteAction = UIPreviewAction(title: NSLocalizedString("Delete", comment: ""), style: .destructive) { [unowned self] (action, viewController) in
self.delegate?.editCheatViewController(self, deactivateCheat: cheat)
DatabaseManager.shared.performBackgroundTask { (context) in
let temporaryCheat = context.object(with: cheat.objectID)
context.delete(temporaryCheat)
context.saveWithErrorLogging()
}
}
let cancelDeleteAction = UIPreviewAction(title: NSLocalizedString("Cancel", comment: ""), style: .default) { (action, viewController) in
}
let deleteActionGroup = UIPreviewActionGroup(title: NSLocalizedString("Delete", comment: ""), style: .destructive, actions: [deleteAction, cancelDeleteAction])
return [copyCodeAction, editCheatAction, deleteActionGroup]
}
}
extension EditCheatViewController
{
override func viewDidLoad()
{
super.viewDidLoad()
var name: String!
var type: CheatType!
var code: String!
self.managedObjectContext.performAndWait {
// Main Thread context is read-only, so we either create a new cheat, or get a reference to the current cheat in a new background context
if let cheat = self.cheat
{
self.mutableCheat = self.managedObjectContext.object(with: cheat.objectID) as? Cheat
}
else
{
self.mutableCheat = Cheat.insertIntoManagedObjectContext(self.managedObjectContext)
self.mutableCheat.game = self.managedObjectContext.object(with: self.game.objectID) as? Game
self.mutableCheat.type = self.supportedCheatFormats.first!.type
self.mutableCheat.code = ""
self.mutableCheat.name = ""
}
self.mutableCheat.isEnabled = true // After we save a cheat, it should be enabled
name = self.mutableCheat.name
type = self.mutableCheat.type
code = self.mutableCheat.code.sanitized(with: self.selectedCheatFormat.allowedCodeCharacters)
}
// Update UI
if name.count == 0
{
self.title = NSLocalizedString("Cheat", comment: "")
}
else
{
self.title = name
}
self.nameTextField.text = name
self.codeTextView.text = code
self.typeSegmentedControl.removeAllSegments()
for (index, format) in self.supportedCheatFormats.enumerated()
{
self.typeSegmentedControl.insertSegment(withTitle: format.name, at: index, animated: false)
}
if let index = self.supportedCheatFormats.firstIndex(where: { $0.type == type })
{
self.typeSegmentedControl.selectedSegmentIndex = index
}
else
{
self.typeSegmentedControl.selectedSegmentIndex = 0
}
self.updateCheatType(self.typeSegmentedControl)
self.updateSaveButtonState()
}
override func viewDidAppear(_ animated: Bool)
{
super.viewDidAppear(animated)
// This matters when going from peek -> pop
// Otherwise, has no effect because viewDidLayoutSubviews has already been called
if self.isAppearing && !self.isPreviewing
{
self.nameTextField.becomeFirstResponder()
}
}
override func viewDidLayoutSubviews()
{
super.viewDidLayoutSubviews()
if let superview = self.codeTextView.superview
{
let layoutMargins = superview.layoutMargins
if self.codeTextView.textContainerInset.left != layoutMargins.left
{
self.codeTextView.textContainerInset.left = layoutMargins.left // Don't change right inset because CheatTextView adjusts it as well.
self.codeTextView.textContainer.lineFragmentPadding = 0
self.codeTextView.setNeedsLayout()
}
}
if self.isAppearing && !self.isPreviewing
{
self.nameTextField.becomeFirstResponder()
}
}
override func didReceiveMemoryWarning()
{
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?)
{
self.nameTextField.resignFirstResponder()
self.codeTextView.resignFirstResponder()
}
}
internal extension EditCheatViewController
{
func presentWithPresentingViewController(_ presentingViewController: UIViewController)
{
let navigationController = RSTNavigationController(rootViewController: self)
navigationController.modalPresentationStyle = .overFullScreen // Keeps PausePresentationController active to ensure layout is not messed up
navigationController.modalPresentationCapturesStatusBarAppearance = true
presentingViewController.present(navigationController, animated: true, completion: nil)
}
}
private extension EditCheatViewController
{
@IBAction func updateCheatName(_ sender: UITextField)
{
var title = sender.text ?? ""
if title.count == 0
{
title = NSLocalizedString("Cheat", comment: "")
}
self.title = title
self.updateSaveButtonState()
}
@IBAction func updateCheatType(_ sender: UISegmentedControl)
{
self.codeTextView.cheatFormat = self.selectedCheatFormat
UIView.performWithoutAnimation {
self.tableView.reloadSections(IndexSet(integer: Section.type.rawValue), with: .none)
// Hacky-ish workaround so we can update the footer text for the code section without causing text view to resign first responder status
self.tableView.beginUpdates()
if let footerView = self.tableView.footerView(forSection: Section.code.rawValue)
{
footerView.textLabel!.text = self.tableView(self.tableView, titleForFooterInSection: Section.code.rawValue)
footerView.sizeToFit()
}
self.tableView.endUpdates()
}
self.view.setNeedsLayout()
}
func updateSaveButtonState()
{
let isValidName = !(self.nameTextField.text ?? "").isEmpty
let isValidCode = !self.codeTextView.text.isEmpty
self.navigationItem.rightBarButtonItem?.isEnabled = isValidName && isValidCode
}
@IBAction func saveCheat(_ sender: UIBarButtonItem)
{
self.mutableCheat.managedObjectContext?.performAndWait {
self.mutableCheat.name = self.nameTextField.text ?? ""
self.mutableCheat.type = self.selectedCheatFormat.type
self.mutableCheat.code = self.codeTextView.text.formatted(with: self.selectedCheatFormat)
do
{
try self.validateCheat(self.mutableCheat)
self.delegate?.editCheatViewController(self, activateCheat: self.mutableCheat, previousCheat: self.cheat)
self.mutableCheat.managedObjectContext?.saveWithErrorLogging()
self.performSegue(withIdentifier: "unwindEditCheatSegue", sender: sender)
}
catch CheatValidator.Error.invalidCode
{
self.presentErrorAlert(title: NSLocalizedString("Invalid Code", comment: ""), message: NSLocalizedString("Please make sure you typed the cheat code in correctly and try again.", comment: "")) {
self.codeTextView.becomeFirstResponder()
}
}
catch CheatValidator.Error.invalidName
{
self.presentErrorAlert(title: NSLocalizedString("Invalid Name", comment: ""), message: NSLocalizedString("Please rename this cheat and try again.", comment: "")) {
self.codeTextView.becomeFirstResponder()
}
}
catch CheatValidator.Error.duplicateCode
{
self.presentErrorAlert(title: NSLocalizedString("Duplicate Code", comment: ""), message: NSLocalizedString("A cheat already exists with this code. Please type in a different code and try again.", comment: "")) {
self.codeTextView.becomeFirstResponder()
}
}
catch CheatValidator.Error.duplicateName
{
self.presentErrorAlert(title: NSLocalizedString("Duplicate Name", comment: ""), message: NSLocalizedString("A cheat already exists with this name. Please rename this cheat and try again.", comment: "")) {
self.nameTextField.becomeFirstResponder()
}
}
catch
{
print(error)
self.presentErrorAlert(title: NSLocalizedString("Unknown Error", comment: ""), message: NSLocalizedString("An error occured. Please make sure you typed the cheat code in correctly and try again.", comment: "")) {
self.codeTextView.becomeFirstResponder()
}
}
}
}
func validateCheat(_ cheat: Cheat) throws
{
let validator = CheatValidator(format: self.selectedCheatFormat, managedObjectContext: self.managedObjectContext)
try validator.validate(cheat)
}
@IBAction func textFieldDidEndEditing(_ sender: UITextField)
{
sender.resignFirstResponder()
}
func presentErrorAlert(title: String, message: String, handler: (() -> Void)?)
{
DispatchQueue.main.async {
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: { action in
handler?()
}))
self.present(alertController, animated: true, completion: nil)
}
}
}
extension EditCheatViewController
{
override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String?
{
switch Section(rawValue: section)!
{
case .name: return super.tableView(tableView, titleForFooterInSection: section)
case .type:
let title = String.localizedStringWithFormat("Code format is %@.", self.selectedCheatFormat.format)
return title
case .code:
let containsSpaces = self.selectedCheatFormat.format.contains(" ")
let containsDashes = self.selectedCheatFormat.format.contains("-")
switch (containsSpaces, containsDashes)
{
case (true, false): return NSLocalizedString("Spaces will be inserted automatically as you type.", comment: "")
case (false, true): return NSLocalizedString("Dashes will be inserted automatically as you type.", comment: "")
case (true, true): return NSLocalizedString("Spaces and dashes will be inserted automatically as you type.", comment: "")
case (false, false): return NSLocalizedString("Code will be formatted automatically as you type.", comment: "")
}
}
}
}
extension EditCheatViewController: UITextViewDelegate
{
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool
{
defer { self.updateSaveButtonState() }
guard text != "\n" else
{
textView.resignFirstResponder()
return false
}
let sanitizedText = text.sanitized(with: self.selectedCheatFormat.allowedCodeCharacters)
guard sanitizedText != text else { return true }
// We need to manually add back the attributes when manually modifying the underlying text storage
// Otherwise, pasting text into an empty text view will result in the wrong font being used
let attributedString = NSAttributedString(string: sanitizedText, attributes: textView.typingAttributes)
textView.textStorage.replaceCharacters(in: range, with: attributedString)
// We must add attributedString.length, not range.length, in case the attributed string's length differs
textView.selectedRange = NSRange(location: range.location + attributedString.length, length: 0)
return false
}
}