diff --git a/Cores/DeltaCore b/Cores/DeltaCore index 9e14654..a20b36f 160000 --- a/Cores/DeltaCore +++ b/Cores/DeltaCore @@ -1 +1 @@ -Subproject commit 9e14654abbcf82ff415eb6fb2648b0be5091ca44 +Subproject commit a20b36f96e52bc236d243cd8b1b55fc5e1bbf76c diff --git a/Cores/SNESDeltaCore b/Cores/SNESDeltaCore index 87d9cfa..cac97f6 160000 --- a/Cores/SNESDeltaCore +++ b/Cores/SNESDeltaCore @@ -1 +1 @@ -Subproject commit 87d9cfa5574fe42db453f9b6b08b3e7190a4b7d2 +Subproject commit cac97f68eea3eb85471a16b468ae8da6fa1038e6 diff --git a/Delta.xcodeproj/project.pbxproj b/Delta.xcodeproj/project.pbxproj index 40d2e3d..8f2ed3c 100644 --- a/Delta.xcodeproj/project.pbxproj +++ b/Delta.xcodeproj/project.pbxproj @@ -25,6 +25,8 @@ BF2A53FD1BB74FC60052BD0C /* ZipZap.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF70798B1B6B464B0019077C /* ZipZap.framework */; }; BF2A53FE1BB74FC60052BD0C /* ZipZap.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BF70798B1B6B464B0019077C /* ZipZap.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; BF2B98E61C97E32F00F6D57D /* SaveStatesCollectionHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF2B98E51C97E32F00F6D57D /* SaveStatesCollectionHeaderView.swift */; }; + BF34FA071CF0F510006624C7 /* EditCheatViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF34FA061CF0F510006624C7 /* EditCheatViewController.swift */; }; + BF34FA111CF1899D006624C7 /* CheatTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF34FA101CF1899D006624C7 /* CheatTextView.swift */; }; BF353FF21C5D7FB000C1184C /* PauseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF353FF11C5D7FB000C1184C /* PauseViewController.swift */; }; BF353FF31C5D7FB000C1184C /* PauseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF353FF11C5D7FB000C1184C /* PauseViewController.swift */; }; BF353FF61C5D837600C1184C /* PauseMenu.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BF353FF41C5D837600C1184C /* PauseMenu.storyboard */; }; @@ -139,6 +141,8 @@ BF27CC941BCB7B7A00A20D89 /* GameController.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = GameController.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS9.0.sdk/System/Library/Frameworks/GameController.framework; sourceTree = DEVELOPER_DIR; }; BF27CC961BCC890700A20D89 /* GamesCollectionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GamesCollectionViewController.swift; sourceTree = ""; }; BF2B98E51C97E32F00F6D57D /* SaveStatesCollectionHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SaveStatesCollectionHeaderView.swift; path = "Pause Menu/Save States/SaveStatesCollectionHeaderView.swift"; sourceTree = ""; }; + BF34FA061CF0F510006624C7 /* EditCheatViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = EditCheatViewController.swift; path = "Pause Menu/Cheats/EditCheatViewController.swift"; sourceTree = ""; }; + BF34FA101CF1899D006624C7 /* CheatTextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CheatTextView.swift; path = "Pause Menu/Cheats/CheatTextView.swift"; sourceTree = ""; }; BF353FF11C5D7FB000C1184C /* PauseViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PauseViewController.swift; path = "Pause Menu/PauseViewController.swift"; sourceTree = ""; }; BF353FF51C5D837600C1184C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/PauseMenu.storyboard; sourceTree = ""; }; BF353FF81C5D870B00C1184C /* PauseItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PauseItem.swift; path = "Pause Menu/PauseItem.swift"; sourceTree = ""; }; @@ -388,6 +392,8 @@ isa = PBXGroup; children = ( BFC9B7381CEFCD34008629BB /* CheatsViewController.swift */, + BF34FA061CF0F510006624C7 /* EditCheatViewController.swift */, + BF34FA101CF1899D006624C7 /* CheatTextView.swift */, ); name = Cheats; sourceTree = ""; @@ -730,8 +736,10 @@ BF27CC971BCC890700A20D89 /* GamesCollectionViewController.swift in Sources */, BFFA71DD1AAC406100EE9DD1 /* AppDelegate.swift in Sources */, BF7AE81E1C2E984300B1B5BC /* GridCollectionViewCell.swift in Sources */, + BF34FA111CF1899D006624C7 /* CheatTextView.swift in Sources */, BF4566E81BC090B6007BFA1A /* Model.xcdatamodeld in Sources */, BFDE393C1BC0CEDF003F72E8 /* Game.swift in Sources */, + BF34FA071CF0F510006624C7 /* EditCheatViewController.swift in Sources */, BFC2731A1BE6152200D22B05 /* GameCollection.swift in Sources */, BFF1E5641BE04CAF000E9EF6 /* BoxArtImageView.swift in Sources */, BF2B98E61C97E32F00F6D57D /* SaveStatesCollectionHeaderView.swift in Sources */, diff --git a/Delta/Base.lproj/PauseMenu.storyboard b/Delta/Base.lproj/PauseMenu.storyboard index d4bfab4..7795544 100644 --- a/Delta/Base.lproj/PauseMenu.storyboard +++ b/Delta/Base.lproj/PauseMenu.storyboard @@ -1,7 +1,8 @@ - + + @@ -226,5 +227,143 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Delta/Emulation/EmulationViewController.swift b/Delta/Emulation/EmulationViewController.swift index 775e1db..4d91ff3 100644 --- a/Delta/Emulation/EmulationViewController.swift +++ b/Delta/Emulation/EmulationViewController.swift @@ -342,9 +342,9 @@ extension EmulationViewController: SaveStatesViewControllerDelegate /// Cheats extension EmulationViewController: CheatsViewControllerDelegate { - func cheatsViewControllerActiveGame(cheatsViewController: CheatsViewController) -> Game + func cheatsViewControllerActiveEmulatorCore(saveStatesViewController: CheatsViewController) -> EmulatorCore { - return self.emulatorCore.game as! Game + return self.emulatorCore } func cheatsViewController(cheatsViewController: CheatsViewController, didActivateCheat cheat: Cheat) throws diff --git a/Delta/Pause Menu/Cheats/CheatTextView.swift b/Delta/Pause Menu/Cheats/CheatTextView.swift new file mode 100644 index 0000000..663fc0f --- /dev/null +++ b/Delta/Pause Menu/Cheats/CheatTextView.swift @@ -0,0 +1,166 @@ +// +// CheatTextView.swift +// Delta +// +// Created by Riley Testut on 5/22/16. +// Copyright © 2016 Riley Testut. All rights reserved. +// + +import UIKit +import CoreText + +import DeltaCore + +import Roxas + +private let CheatPrefixAttribute = "prefix" + +class CheatTextView: UITextView +{ + var cheatFormat: CheatFormat? { + didSet { + self.updateAttributedFormat() + } + } + + @NSCopying private var attributedFormat: NSAttributedString? + + required init?(coder aDecoder: NSCoder) + { + super.init(coder: aDecoder) + + self.layoutManager.delegate = self + + self.textContainer.widthTracksTextView = false + self.textContainer.heightTracksTextView = false + } +} + +extension CheatTextView +{ + override func layoutSubviews() + { + super.layoutSubviews() + + if let format = self.cheatFormat, font = self.font + { + let characterWidth = ("A" as NSString).sizeWithAttributes([NSFontAttributeName: font]).width + + let width = characterWidth * CGFloat(format.format.characters.count) + self.textContainer.size = CGSize(width: width, height: 0) + } + } +} + +private extension CheatTextView +{ + func updateAttributedFormat() + { + guard let format = self.cheatFormat?.format else + { + self.attributedFormat = nil + return + } + + let attributedFormat = NSMutableAttributedString() + var prefixString: NSString? = nil + + let scanner = NSScanner(string: format) + scanner.charactersToBeSkipped = nil + + while (!scanner.atEnd) + { + var string: NSString? = nil + scanner.scanCharactersFromSet(NSCharacterSet.alphanumericCharacterSet(), intoString: &string) + + guard let scannedString = string where scannedString.length > 0 else { break } + + let attributedString = NSMutableAttributedString(string: scannedString as String) + + if let prefixString = prefixString where prefixString.length > 0 + { + attributedString.addAttribute(CheatPrefixAttribute, value: prefixString, range: NSRange(location: 0, length: 1)) + } + + attributedFormat.appendAttributedString(attributedString) + + prefixString = nil + scanner.scanUpToCharactersFromSet(NSCharacterSet.alphanumericCharacterSet(), intoString: &prefixString) + } + + self.attributedFormat = attributedFormat + + rst_dispatch_sync_on_main_thread { + self.setNeedsLayout() + self.layoutIfNeeded() + + let range = NSRange(location: 0, length: (self.text as NSString).length) + self.layoutManager.invalidateGlyphsForCharacterRange(range, changeInLength: 0, actualCharacterRange: nil) + self.layoutManager.invalidateLayoutForCharacterRange(range, actualCharacterRange: nil) + self.layoutManager.ensureGlyphsForCharacterRange(range) + self.layoutManager.ensureLayoutForCharacterRange(range) + } + } +} + +extension CheatTextView: NSLayoutManagerDelegate +{ + func layoutManager(layoutManager: NSLayoutManager, shouldGenerateGlyphs glyphs: UnsafePointer, properties props: UnsafePointer, characterIndexes charIndexes: UnsafePointer, font aFont: UIFont, forGlyphRange glyphRange: NSRange) -> Int + { + // Returning 0 = let the layoutManager do the normal logic + guard let attributedFormat = self.attributedFormat else { return 0 } + + let glyphCount = glyphRange.length + + // Ensure the buffer is long enough to hold our additional glyphs + // If we're only modifying one character, glyphCount * 2 = 2, which is not large enough if we're inserting multiple separator characters + let bufferSize = max(attributedFormat.length + 1, glyphCount * 2) + + // Allocate our replacement buffers + let glyphBuffer = UnsafeMutablePointer.alloc(bufferSize) + let propertyBuffer = UnsafeMutablePointer.alloc(bufferSize) + let characterBuffer = UnsafeMutablePointer.alloc(bufferSize) + + var offset = 0 + + for i in 0 ..< glyphCount + { + // The index the actual character maps to in the cheat format + let characterIndex = charIndexes[i] % attributedFormat.length + + if let prefix = attributedFormat.attributesAtIndex(characterIndex, effectiveRange: nil)[CheatPrefixAttribute] as? String + { + // If there is a prefix string, we insert the glyphs (and associated properties/character indexes) first + let prefixCount = prefix.characters.count + + for j in 0 ..< prefixCount + { + characterBuffer[i + offset + j] = charIndexes[i] + propertyBuffer[i + offset + j] = props[i] + + // Prepend prefix character + var prefixCharacter = (prefix as NSString).characterAtIndex(0) + CTFontGetGlyphsForCharacters(aFont as CTFont, &prefixCharacter, glyphBuffer + (i + offset + j), 1) + } + + offset += prefixCount + } + + // Copy over the information from the original buffers + characterBuffer[i + offset] = charIndexes[i] + propertyBuffer[i + offset] = props[i] + glyphBuffer[i + offset] = glyphs[i] + } + + // Replace buffers with our own buffers, and ensure length takes into account any added glpyhs + layoutManager.setGlyphs(glyphBuffer, properties: propertyBuffer, characterIndexes: characterBuffer, font: aFont, forGlyphRange: NSRange(location: glyphRange.location, length: glyphCount + offset)) + + // Clean up memory + characterBuffer.dealloc(bufferSize) + propertyBuffer.dealloc(bufferSize) + glyphBuffer.dealloc(bufferSize) + + // Return total number of glyphs + return glyphCount + offset + } +} \ No newline at end of file diff --git a/Delta/Pause Menu/Cheats/CheatsViewController.swift b/Delta/Pause Menu/Cheats/CheatsViewController.swift index 7567bbb..e931d73 100644 --- a/Delta/Pause Menu/Cheats/CheatsViewController.swift +++ b/Delta/Pause Menu/Cheats/CheatsViewController.swift @@ -15,7 +15,7 @@ import Roxas protocol CheatsViewControllerDelegate: class { - func cheatsViewControllerActiveGame(saveStatesViewController: CheatsViewController) -> Game + func cheatsViewControllerActiveEmulatorCore(saveStatesViewController: CheatsViewController) -> EmulatorCore func cheatsViewController(cheatsViewController: CheatsViewController, didActivateCheat cheat: Cheat) throws func cheatsViewController(cheatsViewController: CheatsViewController, didDeactivateCheat cheat: Cheat) throws } @@ -67,13 +67,21 @@ extension CheatsViewController } } +//MARK: - Navigation - +private extension CheatsViewController +{ + @IBAction func unwindFromEditCheatViewController(segue: UIStoryboardSegue) + { + + } +} + +//MARK: - Update - private extension CheatsViewController { - //MARK: - Update - - func updateFetchedResultsController() { - let game = self.delegate.cheatsViewControllerActiveGame(self) + let game = self.delegate.cheatsViewControllerActiveEmulatorCore(self).game as! Game let fetchRequest = Cheat.fetchRequest() fetchRequest.returnsObjectsAsFaults = false @@ -105,42 +113,8 @@ private extension CheatsViewController { @IBAction func addCheat() { - let backgroundContext = DatabaseManager.sharedManager.backgroundManagedObjectContext() - backgroundContext.performBlock { - - var game = self.delegate.cheatsViewControllerActiveGame(self) - game = backgroundContext.objectWithID(game.objectID) as! Game - - let cheat = Cheat.insertIntoManagedObjectContext(backgroundContext) - cheat.game = game - cheat.name = "Unlimited Jumps" - cheat.code = "3E2C-AF6F" - cheat.type = .gameGenie - - do - { - try self.delegate.cheatsViewController(self, didActivateCheat: cheat) - backgroundContext.saveWithErrorLogging() - } - catch EmulatorCore.CheatError.invalid - { - dispatch_async(dispatch_get_main_queue()) { - - let alertController = UIAlertController(title: NSLocalizedString("Invalid Cheat", comment: ""), message: NSLocalizedString("Please make sure you typed the cheat code in correctly and try again.", comment: ""), preferredStyle: .Alert) - alertController.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .Default, handler: nil)) - self.presentViewController(alertController, animated: true, completion: nil) - - } - - print("Invalid cheat:", cheat.name, cheat.code) - } - catch let error as NSError - { - print("Unknown Cheat Error:", error, cheat.name, cheat.code) - } - - - } + let editCheatViewController = self.makeEditCheatViewController(cheat: nil) + self.presentViewController(RSTContainInNavigationController(editCheatViewController), animated: true, completion: nil) } func deleteCheat(cheat: Cheat) @@ -156,8 +130,8 @@ private extension CheatsViewController } } -//MARK: - Content - -/// Content +//MARK: - Convenience - +/// Convenience private extension CheatsViewController { func configure(cell cell: UITableViewCell, forIndexPath indexPath: NSIndexPath) @@ -167,6 +141,17 @@ private extension CheatsViewController cell.textLabel?.font = UIFont.boldSystemFontOfSize(cell.textLabel!.font.pointSize) cell.accessoryType = cheat.enabled ? .Checkmark : .None } + + func makeEditCheatViewController(cheat cheat: Cheat?) -> EditCheatViewController + { + let editCheatViewController = self.storyboard!.instantiateViewControllerWithIdentifier("editCheatViewController") as! EditCheatViewController + editCheatViewController.delegate = self + editCheatViewController.supportedCheatFormats = self.delegate.cheatsViewControllerActiveEmulatorCore(self).supportedCheatFormats + editCheatViewController.cheat = cheat + editCheatViewController.game = self.delegate.cheatsViewControllerActiveEmulatorCore(self).game as! Game + + return editCheatViewController + } } extension CheatsViewController @@ -236,13 +221,15 @@ extension CheatsViewController override func tableView(tableView: UITableView, editActionsForRowAtIndexPath indexPath: NSIndexPath) -> [UITableViewRowAction]? { + let cheat = self.fetchedResultsController.objectAtIndexPath(indexPath) as! Cheat + let deleteAction = UITableViewRowAction(style: .Destructive, title: NSLocalizedString("Delete", comment: "")) { (action, indexPath) in - let cheat = self.fetchedResultsController.objectAtIndexPath(indexPath) as! Cheat self.deleteCheat(cheat) } let editAction = UITableViewRowAction(style: .Normal, title: NSLocalizedString("Edit", comment: "")) { (action, indexPath) in - + let editCheatViewController = self.makeEditCheatViewController(cheat: cheat) + self.presentViewController(RSTContainInNavigationController(editCheatViewController), animated: true, completion: nil) } return [deleteAction, editAction] @@ -254,6 +241,34 @@ extension CheatsViewController } } +extension CheatsViewController: EditCheatViewControllerDelegate +{ + func editCheatViewController(editCheatViewController: EditCheatViewController, activateCheat cheat: Cheat, previousCheat: Cheat?) throws + { + try self.delegate.cheatsViewController(self, didActivateCheat: cheat) + + if let previousCheat = previousCheat + { + let code = cheat.code + + previousCheat.managedObjectContext?.performBlockAndWait({ + + guard previousCheat.code != code else { return } + + do + { + try self.delegate.cheatsViewController(self, didDeactivateCheat: previousCheat) + } + catch let error as NSError + { + print(error) + } + + }) + } + } +} + //MARK: - - extension CheatsViewController: NSFetchedResultsControllerDelegate { diff --git a/Delta/Pause Menu/Cheats/EditCheatViewController.swift b/Delta/Pause Menu/Cheats/EditCheatViewController.swift new file mode 100644 index 0000000..438e3af --- /dev/null +++ b/Delta/Pause Menu/Cheats/EditCheatViewController.swift @@ -0,0 +1,329 @@ +// +// 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?) throws +} + +private extension EditCheatViewController +{ + enum ValidationError: ErrorType + { + case invalidCode + case duplicateName + case duplicateCode + } + + enum Section: Int + { + case name + case type + case code + } +} + +class EditCheatViewController: UITableViewController +{ + weak var delegate: EditCheatViewControllerDelegate? + + var cheat: Cheat? + var game: Game! + var supportedCheatFormats: [CheatFormat]! + + private var selectedCheatFormat: CheatFormat { + let cheatFormat = self.supportedCheatFormats[self.typeSegmentedControl.selectedSegmentIndex] + return cheatFormat + } + + private var mutableCheat: Cheat! + private var managedObjectContext = DatabaseManager.sharedManager.backgroundManagedObjectContext() + + @IBOutlet private var nameTextField: UITextField! + @IBOutlet private var typeSegmentedControl: UISegmentedControl! + @IBOutlet private var codeTextView: CheatTextView! +} + +extension EditCheatViewController +{ + override func viewDidLoad() + { + super.viewDidLoad() + + var name: String! + var type: CheatType! + var code: String! + + self.managedObjectContext.performBlockAndWait { + + // 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.objectWithID(cheat.objectID) as? Cheat + } + else + { + self.mutableCheat = Cheat.insertIntoManagedObjectContext(self.managedObjectContext) + self.mutableCheat.game = self.managedObjectContext.objectWithID(self.game.objectID) as! Game + self.mutableCheat.type = self.supportedCheatFormats.first!.type + self.mutableCheat.code = "" + self.mutableCheat.name = "" + } + + self.mutableCheat.enabled = true // After we save a cheat, it should be enabled + + name = self.mutableCheat.name + type = self.mutableCheat.type + code = self.mutableCheat.code.sanitized(characterSet: self.selectedCheatFormat.allowedCodeCharacters) + } + + + // Update UI + + if name.characters.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.enumerate() + { + self.typeSegmentedControl.insertSegmentWithTitle(format.name, atIndex: index, animated: false) + } + + if let index = self.supportedCheatFormats.indexOf({ $0.type == type }) + { + self.typeSegmentedControl.selectedSegmentIndex = index + } + else + { + self.typeSegmentedControl.selectedSegmentIndex = 0 + } + + self.updateCheatType(self.typeSegmentedControl) + self.updateSaveButtonState() + } + + override func viewDidLayoutSubviews() + { + super.viewDidLayoutSubviews() + + if let superview = self.codeTextView.superview + { + let layoutMargins = superview.layoutMargins + self.codeTextView.textContainerInset = layoutMargins + + self.codeTextView.textContainer.lineFragmentPadding = 0 + } + + if self.appearing + { + self.nameTextField.becomeFirstResponder() + } + } + + override func didReceiveMemoryWarning() + { + super.didReceiveMemoryWarning() + // Dispose of any resources that can be recreated. + } +} + +private extension EditCheatViewController +{ + @IBAction func updateCheatName(sender: UITextField) + { + var title = sender.text ?? "" + if title.characters.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(NSIndexSet(index: Section.type.rawValue), withRowAnimation: .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.footerViewForSection(Section.code.rawValue) + { + footerView.textLabel!.text = self.tableView(self.tableView, titleForFooterInSection: Section.code.rawValue) + footerView.sizeToFit() + } + + self.tableView.endUpdates() + } + } + + func updateSaveButtonState() + { + let isValidName = !(self.nameTextField.text ?? "").isEmpty + let isValidCode = !self.codeTextView.text.isEmpty + + self.navigationItem.rightBarButtonItem?.enabled = isValidName && isValidCode + } + + @IBAction func saveCheat(sender: UIBarButtonItem) + { + self.mutableCheat.managedObjectContext?.performBlockAndWait { + + self.mutableCheat.name = self.nameTextField.text ?? "" + self.mutableCheat.type = self.selectedCheatFormat.type + self.mutableCheat.code = self.codeTextView.text.formatted(cheatFormat: self.selectedCheatFormat) + + do + { + try self.validateCheat(self.mutableCheat) + self.mutableCheat.managedObjectContext?.saveWithErrorLogging() + self.performSegueWithIdentifier("unwindEditCheatSegue", sender: sender) + } + catch ValidationError.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 ValidationError.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 ValidationError.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 let error as NSError + { + print(error) + } + } + } + + func validateCheat(cheat: Cheat) throws + { + let name = cheat.name! + let code = cheat.code + + // Find all cheats that are for the same game, don't have the same identifier as the current cheat, but have either the same name or code + let predicate = NSPredicate(format: "%K == %@ AND %K != %@ AND (%K == %@ OR %K == %@)", Cheat.Attributes.game.rawValue, cheat.game, Cheat.Attributes.identifier.rawValue, cheat.identifier, Cheat.Attributes.code.rawValue, code, Cheat.Attributes.name.rawValue, name) + + let cheats = Cheat.instancesWithPredicate(predicate, inManagedObjectContext: self.managedObjectContext, type: Cheat.self) + for cheat in cheats + { + if cheat.name == name + { + throw ValidationError.duplicateName + } + else if cheat.code == code + { + throw ValidationError.duplicateCode + } + } + + do + { + try self.delegate?.editCheatViewController(self, activateCheat: cheat, previousCheat: self.cheat) + } + catch + { + throw ValidationError.invalidCode + } + } + + @IBAction func textFieldDidEndEditing(sender: UITextField) + { + sender.resignFirstResponder() + } + + func presentErrorAlert(title title: String, message: String, handler: (Void -> Void)?) + { + dispatch_async(dispatch_get_main_queue()) { + let alertController = UIAlertController(title: title, message: message, preferredStyle: .Alert) + alertController.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .Default, handler: { action in + handler?() + })) + self.presentViewController(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.containsString(" ") + let containsDashes = self.selectedCheatFormat.format.containsString("-") + + 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, shouldChangeTextInRange range: NSRange, replacementText text: String) -> Bool + { + defer { self.updateSaveButtonState() } + + guard text != "\n" else + { + textView.resignFirstResponder() + return false + } + + let sanitizedText = text.sanitized(characterSet: self.selectedCheatFormat.allowedCodeCharacters) + + guard sanitizedText != text else { return true } + + textView.textStorage.replaceCharactersInRange(range, withString: sanitizedText) + + return false + } +} diff --git a/External/Roxas b/External/Roxas index c795d0a..17cc7b7 160000 --- a/External/Roxas +++ b/External/Roxas @@ -1 +1 @@ -Subproject commit c795d0ace22cfe57c3b73613630e14b58d2f8f09 +Subproject commit 17cc7b771ca819cdad14c05ee9cd0529d5df1c91