diff --git a/README.md b/README.md index e811706..a4fa413 100644 --- a/README.md +++ b/README.md @@ -9,3 +9,32 @@ As defined in our Alpha release project plan, below are the deliverables we have * Usability Testing Procedure and Results ([Google Drive](https://drive.google.com/drive/u/1/folders/0Bx_oMYCmW6jGZlgxR2RQTWlnYTg)) * Feature Discovery ([Google Drive](https://drive.google.com/drive/u/1/folders/0Bx_oMYCmW6jGZlgxR2RQTWlnYTg)) * "Hello World" full screen keyboard (GitHub) + +## Beta Release +As defined in our Beta release project plan, below are the deliverables we have completed. [Additional and more detailed information can be found here](https://docs.google.com/document/d/1ckNWmpqcP-tZ3jRCnp_ZpskCHK__58Kwda_WcLUEZXA/edit?usp=sharing). +* Algorithm logic + * The `Values` interface and subclasses primarily handle this logic +* Swiping/gesture setup + * Gestures registered in `KeyboardViewController.swift` include: swipe left, swipe right, swipe up, swipe down, hold, two finger swipe right, three finger swipe right, double tap, two finger hold, two finger tap, and pinch + * Gesture processing in `KeyboardViewController.swift` and primarily different mode files (i.e. `InputMode.swift`) +* Layout of keyboard + * Different UIView elements make up the visual interface within `KeyboardViewController.swift` +* Controller development and input processing + * `KeyboardViewController.swift` handles this in conjunction with different mode files (i.e. `InputMode.swift`) +* Voice prompting + * Pervasive use throughout the project of AVFoundation's `AVSpeechSynthesizer` +* Keyboard activation + * Initialization and setup upon triggered activation is found within `viewDidLoad()` of `KeyboardViewController.swift` +* "Happy path" testing: see TESTING.md + +We also went beyond these specified items in our project plan in completing the following: +* **VoiceOver Integration:** UI setup to switch between VoiceOver and VInput gestures and speech announcements +* **Tutorial and Training Modes:** informative and interactive guide and steps to using VInput +* **Multiple Alphabets:** users can two finger swipe between lowercase, uppercase, basic emoji and numeric alphabets +* **Fault Tolerance:** resiliency and correction against crashes, errors, faults by reloading in memory the last word where the user left off (allowing the user to continue where they left off) +* **Ensuring Correct Input:** correctly places the input cursor at the rightmost location in the text field to prevent accidental deletion and correct appending of additional characters +* **CoreData Implementation:** as a user types right now, the words they type are stored on-device for later use in developing prediction features +* **Code Quality:** we spent significant time in the design of our code such that it is readable, maintainable, and extendable (i.e. inheritance in different input modes and alphabet values) + +## Gama Release +https://docs.google.com/a/umich.edu/document/d/1XTVoPCVIVRVMSoKFGWaqs9isXvAEo0pUQTC9SAfd9gM/edit?usp=sharing \ No newline at end of file diff --git a/Swift/VInput Keyboard/EmojiValues.swift b/Swift/VInput Keyboard/EmojiValues.swift index ded993e..faa4e6b 100644 --- a/Swift/VInput Keyboard/EmojiValues.swift +++ b/Swift/VInput Keyboard/EmojiValues.swift @@ -8,13 +8,70 @@ import Foundation -class EmojiValues : InsertableValues { +class EmojiValues : Values { - let emojiValues : [String] = ["😑","☹","😐","😬","πŸ˜ƒ"] - let emojiValueNames: [String] = ["Angry", "Sad", "Neutral", "Grinning", "Very Happy"] + let emojiValues : [String] = ["πŸ˜ƒ","😊","😬","😐","☹","😭","😑"] + var index: Int + var valueType: ValueUtil.VALUE_TYPE - override init(values: [String] = [], valueType: ValueUtil.VALUE_TYPE = .emoji) { - super.init(values: emojiValues, valueType: valueType) + init(values: [String] = [], valueType: ValueUtil.VALUE_TYPE = .emoji) { + self.index = 0 + self.valueType = .emoji } + func shiftLeft() + { + if index > 0 + { + index -= 1 + } + } + + func shiftRight() + { + if index < emojiValues.count - 1 + { + index += 1 + } + } + + func getCurrentValue() -> String + { + return emojiValues[index] + } + + func resetIndexes() + { + index = 0 + } + + func getLeftIndex() -> Int + { + return index + } + + func getRightIndex() -> Int + { + return index + } + + func isSearchingResetAndAnounce() -> Bool + { + if index != 0 { + index = 0 + return true + } + return false + } + + func getValueType() -> ValueUtil.VALUE_TYPE + { + return valueType + } + + func isDone() -> Bool + { + return index == (emojiValues.count - 1) + } + } diff --git a/Swift/VInput Keyboard/InputMode.swift b/Swift/VInput Keyboard/InputMode.swift index 59aaf65..89409a5 100644 --- a/Swift/VInput Keyboard/InputMode.swift +++ b/Swift/VInput Keyboard/InputMode.swift @@ -17,10 +17,11 @@ class InputMode : Mode { var keyboardController: KeyboardViewController! let MODE_NAME = "InputMode" var currentWord: String = "" + var swapBack: Bool = false init(keyboardController: KeyboardViewController) { -// self.values = values self.keyboardController = keyboardController + self.swapBack = false } func initialize() { @@ -34,6 +35,25 @@ class InputMode : Mode { let textBeforeMarker: String? = keyboardController.textDocumentProxy.documentContextBeforeInput if textBeforeMarker != nil && textBeforeMarker!.characters.last != " " { currentWord = loadFromProxy() + + //Need to decrement here - This is repeat code for now - same as swipe down + let context = self.keyboardController.persistentContainer!.viewContext + let request = NSFetchRequest() + request.predicate = NSPredicate(format: "word = %@", currentWord) + request.entity = NSEntityDescription.entity(forEntityName: "TypedWord", in: context) + + do { + let results = try context.fetch(request) + let wordToInsertOrUpdate: TypedWord? + if results.count > 0 { + wordToInsertOrUpdate = (results[0] as! TypedWord) + wordToInsertOrUpdate!.frequency -= 1 + try context.save() + } + } catch { + let fetchError = error as NSError + print(fetchError) + } } VisualUtil.updateViewAndAnnounce(letter: keyboardController.currentValues.getCurrentValue()) } @@ -43,19 +63,35 @@ class InputMode : Mode { } func onSwipeLeft() { - if keyboardController.currentValues.getValueType() == ValueUtil.VALUE_TYPE.uppercase { + if swapBack && keyboardController.currentValues.getValueType() == ValueUtil.VALUE_TYPE.uppercase { ValueUtil.swapMode(keyboardController: keyboardController, valueType: ValueUtil.VALUE_TYPE.lowercase) + swapBack = false } keyboardController.currentValues.shiftLeft() - VisualUtil.updateViewAndAnnounce(letter: keyboardController.currentValues.getCurrentValue()) + if keyboardController.currentValues.isDone() { + SpeechUtil.speak(textToSpeak: keyboardController.currentValues.getCurrentValue()) + SpeechUtil.speak(textToSpeak: "Swipe up to insert. Swipe down to reset.") + let systemSoundID: SystemSoundID = 4095 + AudioServicesPlaySystemSound (systemSoundID) + } else { + VisualUtil.updateViewAndAnnounce(letter: keyboardController.currentValues.getCurrentValue()) + } } func onSwipeRight() { - if keyboardController.currentValues.getValueType() == ValueUtil.VALUE_TYPE.uppercase { + if swapBack && keyboardController.currentValues.getValueType() == ValueUtil.VALUE_TYPE.uppercase { ValueUtil.swapMode(keyboardController: keyboardController, valueType: ValueUtil.VALUE_TYPE.lowercase) + swapBack = false + } + if keyboardController.currentValues.isDone() { + SpeechUtil.speak(textToSpeak: keyboardController.currentValues.getCurrentValue()) + SpeechUtil.speak(textToSpeak: "Swipe up to insert. Swipe down to reset.") + let systemSoundID: SystemSoundID = 4095 + AudioServicesPlaySystemSound (systemSoundID) + } else { + keyboardController.currentValues.shiftRight() + VisualUtil.updateViewAndAnnounce(letter: keyboardController.currentValues.getCurrentValue()) } - keyboardController.currentValues.shiftRight() - VisualUtil.updateViewAndAnnounce(letter: keyboardController.currentValues.getCurrentValue()) } func onSwipeUp() { @@ -64,11 +100,15 @@ class InputMode : Mode { if keyboardController.currentValues.getValueType() == ValueUtil.VALUE_TYPE.uppercase{ text = "Inserting upper case " + keyboardController.currentValues.getCurrentValue() } + if keyboardController.currentValues.getValueType() == ValueUtil.VALUE_TYPE.lowercase { + text = "Inserting " + keyboardController.currentValues.getCurrentValue().uppercased() + } SpeechUtil.speak(textToSpeak: text) currentWord.append(keyboardController.currentValues.getCurrentValue()) keyboardController.textDocumentProxy.insertText(keyboardController.currentValues.getCurrentValue()) - if keyboardController.currentValues.getValueType() == ValueUtil.VALUE_TYPE.uppercase{ + if swapBack && keyboardController.currentValues.getValueType() == ValueUtil.VALUE_TYPE.uppercase{ ValueUtil.swapMode(keyboardController: keyboardController, valueType: ValueUtil.VALUE_TYPE.lowercase) + swapBack = false } keyboardController.currentValues.resetIndexes() VisualUtil.updateViewAndAnnounce(letter: keyboardController.currentValues.getCurrentValue()) @@ -117,23 +157,45 @@ class InputMode : Mode { else if keyboardController.textDocumentProxy.documentContextBeforeInput?.characters.last != " " { currentWord = loadFromProxy() + //reload word here and decrement from count + let context = self.keyboardController.persistentContainer!.viewContext + let request = NSFetchRequest() + request.predicate = NSPredicate(format: "word = %@", currentWord) + request.entity = NSEntityDescription.entity(forEntityName: "TypedWord", in: context) + + do { + let results = try context.fetch(request) + let wordToInsertOrUpdate: TypedWord? + if results.count > 0 { + wordToInsertOrUpdate = (results[0] as! TypedWord) + wordToInsertOrUpdate!.frequency -= 1 + try context.save() + } + } catch { + let fetchError = error as NSError + print(fetchError) + } } } - if keyboardController.currentValues.getValueType() == ValueUtil.VALUE_TYPE.uppercase{ + if swapBack && keyboardController.currentValues.getValueType() == ValueUtil.VALUE_TYPE.uppercase { ValueUtil.swapMode(keyboardController: keyboardController, valueType: ValueUtil.VALUE_TYPE.lowercase) + swapBack = false } VisualUtil.updateViewAndAnnounce(letter: keyboardController.currentValues.getCurrentValue()) } func onHold() { + //TODO: This needs to be refactored throughout if keyboardController.currentValues.getValueType() == .lowercase { ValueUtil.swapMode(keyboardController: keyboardController, valueType: ValueUtil.VALUE_TYPE.uppercase) VisualUtil.updateView(letter: keyboardController.currentValues.getCurrentValue()) + swapBack = true SpeechUtil.speak(textToSpeak: "Current letter upper cased" ) } else if keyboardController.currentValues.getValueType() == .uppercase { ValueUtil.swapMode(keyboardController: keyboardController, valueType: ValueUtil.VALUE_TYPE.lowercase) VisualUtil.updateView(letter: keyboardController.currentValues.getCurrentValue()) + swapBack = false SpeechUtil.speak(textToSpeak: "Current letter lower cased") } } @@ -198,10 +260,15 @@ class InputMode : Mode { func onTwoTouchTap() { - SpeechUtil.speak(textToSpeak: "Left or right of " + keyboardController.currentValues.getCurrentValue()) + if keyboardController.currentValues.getValueType() == ValueUtil.VALUE_TYPE.lowercase { + SpeechUtil.speak(textToSpeak: "Left or right of " + keyboardController.currentValues.getCurrentValue().uppercased()) + } + else { + SpeechUtil.speak(textToSpeak: "Left or right of " + keyboardController.currentValues.getCurrentValue()) + } } - func onTwoTouchHold(){ + func onTwoTouchHold() { var text = "" if (currentWord != ""){ text = "Current word: " + currentWord @@ -213,6 +280,37 @@ class InputMode : Mode { } + func onTwoFingerSwipeRight() { + + let currentValueType: ValueUtil.VALUE_TYPE = keyboardController.currentValues.getValueType() + let numValueTypes: Int = ValueUtil.VALUE_TYPE.numValueTypes(currentValueType)() + 1 + ValueUtil.swapMode(keyboardController: keyboardController, valueType: ValueUtil.VALUE_TYPE(rawValue: ((currentValueType.rawValue + 1) % numValueTypes))!) + + //TODO: Clean and refactor this + let valHolder: Int = keyboardController.currentValues.getValueType().rawValue + var text: String = "Switching to " + switch valHolder { + case 0: + text += "lower case alphabet" + case 1: + text += "upper case alphabet" + case 2: + text += "numbers 0 through 9" + case 3: + text += "emoticons" + case 4: + text += "punctuation" + case 5: + text += "your most common words" + default: + break + } + SpeechUtil.speak(textToSpeak: text) + + keyboardController.currentValues.resetIndexes() + VisualUtil.updateViewAndAnnounce(letter: keyboardController.currentValues.getCurrentValue()) + + } private func loadFromProxy() -> String { let textInDocumentProxy : [String] = keyboardController.textDocumentProxy.documentContextBeforeInput!.components(separatedBy: " ").filter{$0.isEmpty == false} diff --git a/Swift/VInput Keyboard/KeyboardViewController.swift b/Swift/VInput Keyboard/KeyboardViewController.swift index 27b7405..0f09a64 100644 --- a/Swift/VInput Keyboard/KeyboardViewController.swift +++ b/Swift/VInput Keyboard/KeyboardViewController.swift @@ -38,7 +38,7 @@ class KeyboardViewController: UIInputViewController { var currentMode: Mode? = nil var persistentContainer: NSPersistentContainer? - + override func updateViewConstraints() { super.updateViewConstraints() @@ -156,7 +156,7 @@ class KeyboardViewController: UIInputViewController { shortHoldRecognizer.addTarget(self, action: #selector(onHold)) shortHoldRecognizer.require(toFail: longHoldRecognizer) - longHoldRecognizer.minimumPressDuration = TimeInterval(4) + longHoldRecognizer.minimumPressDuration = TimeInterval(10) longHoldRecognizer.numberOfTouchesRequired = 1 longHoldRecognizer.allowableMovement = 50 longHoldRecognizer.addTarget(self, action: #selector(onLongHold)) @@ -204,12 +204,12 @@ class KeyboardViewController: UIInputViewController { var containerPath = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.VInput")! containerPath = URL(fileURLWithPath: documentsDirectory.appending("/store.sqlite"), isDirectory: false) // containerPath = containerPath.appendingPathComponent("store.sqlite") - do { - try FileManager.default.removeItem(at: containerPath) - } catch { - // nothing - print("Could not delete CD DB") - } +// do { +// try FileManager.default.removeItem(at: containerPath) +// } catch { +// // nothing +// print("Could not delete CD DB") +// } let description = NSPersistentStoreDescription(url: containerPath) container.persistentStoreDescriptions = [description] container.loadPersistentStores(completionHandler: { (storeDescription, error) in @@ -248,15 +248,13 @@ class KeyboardViewController: UIInputViewController { override func viewDidAppear(_ animated: Bool) { currentMode = InputMode(keyboardController: self) - SpeechUtil.speak(textToSpeak: "Vinput Keyboard", preDelay: 0.5) + SpeechUtil.speak(textToSpeak: "VInput Keyboard", preDelay: 0.5) currentMode!.initialize() } func onDoubleTap() { SpeechUtil.stopSpeech() -// currentMode!.doubleTap() - ValueUtil.swapMode(keyboardController: self, valueType: .numerical) - VisualUtil.updateViewAndAnnounce(letter: currentValues.getCurrentValue()) + currentMode!.doubleTap() } // TODO: Migrate Over -> Mike @@ -299,6 +297,35 @@ class KeyboardViewController: UIInputViewController { func onPinch() { SpeechUtil.stopSpeech() if pinchRecognizer.state == UIGestureRecognizerState.ended { + + //Hack for now + let textInDocumentProxy : [String] = self.textDocumentProxy.documentContextBeforeInput!.components(separatedBy: " ").filter{$0.isEmpty == false} + var lastWord = textInDocumentProxy.isEmpty ? "" : textInDocumentProxy.last! + + let context = self.persistentContainer!.viewContext + let request = NSFetchRequest() + + request.predicate = NSPredicate(format: "word = %@", lastWord) + request.entity = NSEntityDescription.entity(forEntityName: "TypedWord", in: context) + + do { + let results = try context.fetch(request) + let wordToInsertOrUpdate: TypedWord? + if results.count == 0 { + wordToInsertOrUpdate = NSEntityDescription.insertNewObject(forEntityName: "TypedWord", into: context) as! TypedWord + wordToInsertOrUpdate!.word = lastWord + wordToInsertOrUpdate!.frequency = 0 + } else { + wordToInsertOrUpdate = (results[0] as! TypedWord) + } + wordToInsertOrUpdate!.frequency += 1 + try context.save() + } catch { + let fetchError = error as NSError + print(fetchError) + } + //Code Repeat + self.dismissKeyboard() } } @@ -327,11 +354,12 @@ class KeyboardViewController: UIInputViewController { func onTwoFingerSwipeRight() { SpeechUtil.stopSpeech() - SpeechUtil.speak(textToSpeak: "Two Finger Swipe Right") + currentMode!.onTwoFingerSwipeRight() } func onThreeFingerSwipeRight() { SpeechUtil.stopSpeech() + SpeechUtil.speak(textToSpeak: "Exiting VInput.") // Renable normalVO functionality and allow user to transition // to another keyboard fullView.accessibilityTraits = UIAccessibilityTraitNone diff --git a/Swift/VInput Keyboard/Mode.swift b/Swift/VInput Keyboard/Mode.swift index 27ce6d2..ece2e08 100644 --- a/Swift/VInput Keyboard/Mode.swift +++ b/Swift/VInput Keyboard/Mode.swift @@ -32,4 +32,6 @@ protocol Mode { func onTwoTouchHold() + func onTwoFingerSwipeRight() + } diff --git a/Swift/VInput Keyboard/MostCommonValues.swift b/Swift/VInput Keyboard/MostCommonValues.swift new file mode 100644 index 0000000..1b0678f --- /dev/null +++ b/Swift/VInput Keyboard/MostCommonValues.swift @@ -0,0 +1,113 @@ +// +// MostCommonValues.swift +// VInput +// +// Created by Michael Vander Lugt on 12/11/16. +// Copyright Β© 2016 EECS481-VInput. All rights reserved. +// + +import Foundation +import CoreData + +class MostCommonValues : Values +{ + + var commonValues : [Any] + var index: Int + var valueType: ValueUtil.VALUE_TYPE + + init(values: [String] = [], valueType: ValueUtil.VALUE_TYPE = .common_words, keyboardController: KeyboardViewController) + { + self.index = 0 + self.valueType = .common_words + self.commonValues = [] + + let context = keyboardController.persistentContainer!.viewContext + let request = NSFetchRequest() + request.entity = NSEntityDescription.entity(forEntityName: "TypedWord", in: context) + request.sortDescriptors = [NSSortDescriptor(key: "frequency", ascending: false)] + + do + { + let results = try context.fetch(request) + if results.count > 0 + { + for result in results + { + let word: String = (result as! TypedWord).word! + let frequency = (result as! TypedWord).frequency + //Hack: not actually removing words from CoreData for now + if frequency > 0 + { + self.commonValues.append(word) + } + print(word) + print(frequency) + + } + } + } catch + { + let fetchError = error as NSError + print(fetchError) + } + } + + func shiftLeft() + { + if index > 0 + { + index -= 1 + } + } + + func shiftRight() + { + if index < commonValues.count - 1 + { + index += 1 + } + } + + func getCurrentValue() -> String + { + return String(describing: commonValues[index]) + } + + func resetIndexes() + { + index = 0 + } + + func getLeftIndex() -> Int + { + return index + } + + func getRightIndex() -> Int + { + return index + } + + func isSearchingResetAndAnounce() -> Bool + { + if index != 0 + { + index = 0 + return true + } + return false + } + + func getValueType() -> ValueUtil.VALUE_TYPE + { + return valueType + } + + func isDone() -> Bool + { + return index == (commonValues.count - 1) + } + + +} diff --git a/Swift/VInput Keyboard/PunctuationValues.swift b/Swift/VInput Keyboard/PunctuationValues.swift new file mode 100644 index 0000000..9ab231d --- /dev/null +++ b/Swift/VInput Keyboard/PunctuationValues.swift @@ -0,0 +1,79 @@ +// +// Punctuation.swift +// VInput +// +// Created by Michael Vander Lugt on 12/10/16. +// Copyright Β© 2016 EECS481-VInput. All rights reserved. +// + +import Foundation + +class PunctuationValues : Values { + + //Punctuation chosen based on http://mdickens.me/typing/letter_frequency.html + let puncutationValues : [String] = [",",".","-","\"","_","\'",")","(",";", "=",":", "/", "*", "!", "?", "$", "&", "@"] + var index: Int + var valueType: ValueUtil.VALUE_TYPE + + init(values: [String] = [], valueType: ValueUtil.VALUE_TYPE = .punctuation) + { + self.index = 0 + self.valueType = .punctuation + } + + func shiftLeft() + { + if index > 0 + { + index -= 1 + } + } + + func shiftRight() + { + if index < puncutationValues.count - 1 + { + index += 1 + } + } + + func getCurrentValue() -> String + { + return puncutationValues[index] + } + + func resetIndexes() + { + index = 0 + } + + func getLeftIndex() -> Int + { + return index + } + + func getRightIndex() -> Int + { + return index + } + + func isSearchingResetAndAnounce() -> Bool + { + if index != 0 { + index = 0 + return true + } + return false + } + + func getValueType() -> ValueUtil.VALUE_TYPE + { + return valueType + } + + func isDone() -> Bool + { + return index == (puncutationValues.count - 1) + } + +} diff --git a/Swift/VInput Keyboard/SpeechUtil.swift b/Swift/VInput Keyboard/SpeechUtil.swift index b5331e0..b76e55d 100644 --- a/Swift/VInput Keyboard/SpeechUtil.swift +++ b/Swift/VInput Keyboard/SpeechUtil.swift @@ -12,17 +12,45 @@ import AVFoundation class SpeechUtil { static var utterance: AVSpeechUtterance! - static let speechSynthesizer = AVSpeechSynthesizer() + static var speechSynthesizer = AVSpeechSynthesizer() + static var punctuationKeys: [String:String] = [ + "Left or right of ," : "Left or right of comma", + "Left or right of ." : "Left or right of period", + "Left or right of -" : "Left or right of hyphen", + "Left or right of \"" : "Left or right of double quotation mark", + "Left or right of _" : "Left or right of underscore", + "Left or right of \'" : "Left or right of single quotation mark", + "Left or right of )" : "Left or right of close paranthesis", + "Left or right of (" : "Left or right of open paranthesis", + "Left or right of ;" : "Left or right of semi-colon", + "Left or right of =" : "Left or right of equal sign", + "Left or right of :" : "Left or right of colon", + "Left or right of /" : "Left or right of forward slash", + "Left or right of *" : "Left or right of asteric", + "Left or right of !" : "Left or right of exclamation point", + "Left or right of ?" : "Left or right of question mark", + "Left or right of $" : "Left or right of dollar sign", + "Left or right of &" : "Left or right of ampersand", + "Left or right of @" : "Left or right of at sign" + ] static func speak(textToSpeak: String, pitchMultiplier: Float = 1.0, postDelay: TimeInterval = TimeInterval(0), - preDelay: TimeInterval = TimeInterval(0)) { + preDelay: TimeInterval = TimeInterval(0), + speechRate: Float = 0.55) { +// if textToSpeak.range(of: " a") != nil { +// +// } utterance = AVSpeechUtterance(string: textToSpeak) // TODO some of these values should be exposed as options in the Settings bundle utterance.pitchMultiplier = pitchMultiplier //utterance.postUtteranceDelay = postDelay utterance.preUtteranceDelay = preDelay - utterance.rate = 0.55 + utterance.rate = speechRate + print(textToSpeak) + if let newText = punctuationKeys[textToSpeak] { + utterance = AVSpeechUtterance(string: newText) + } speechSynthesizer.speak(utterance) } @@ -33,6 +61,7 @@ class SpeechUtil { static func stopSpeech() { if speechSynthesizer.isSpeaking { speechSynthesizer.stopSpeaking(at: AVSpeechBoundary.immediate) + speechSynthesizer = AVSpeechSynthesizer() } } diff --git a/Swift/VInput Keyboard/TrainingMode.swift b/Swift/VInput Keyboard/TrainingMode.swift index 41f2149..afd5eda 100644 --- a/Swift/VInput Keyboard/TrainingMode.swift +++ b/Swift/VInput Keyboard/TrainingMode.swift @@ -39,7 +39,7 @@ class TrainingMode : InputMode { var text = "" if providedTrainingWords != nil { if stateIndex == 1 { - text = "Try finding the letter " + providedTrainingWords![0] + ". Hold on the screen with one finger when you find it" + text = "Try finding the letter " + providedTrainingWords![0] + ". Hold on the screen with one finger when you find it." } else if (stateIndex == 3) { text = "Try to spell out " + providedTrainingWords![0] @@ -51,12 +51,12 @@ class TrainingMode : InputMode { else { text = "The first word to spell is " + defaultTrainingWords[0] } - SpeechUtil.speak(textToSpeak: text) + SpeechUtil.speak(textToSpeak: text, speechRate: 0.5) VisualUtil.updateViewAndAnnounce(letter: keyboardController.currentValues.getCurrentValue()) } override func onSwipeUp() { - SpeechUtil.speak(textToSpeak: "Inserting " + keyboardController.currentValues.getCurrentValue()) + SpeechUtil.speak(textToSpeak: "Inserting " + keyboardController.currentValues.getCurrentValue().uppercased(), speechRate: 0.5) currentWord.append(keyboardController.currentValues.getCurrentValue()) keyboardController.currentValues.resetIndexes() VisualUtil.updateViewAndAnnounce(letter: keyboardController.currentValues.getCurrentValue()) @@ -65,11 +65,11 @@ class TrainingMode : InputMode { override func swipeDown() { if !keyboardController.currentValues.isSearchingResetAndAnounce() { if trainingLevel.rawValue >= TRAINING_LEVELS.delete.rawValue && !currentWord.isEmpty { - SpeechUtil.speak(textToSpeak: "Deleting previous character") + SpeechUtil.speak(textToSpeak: "Deleting previous character", speechRate: 0.5) currentWord = currentWord.substring(to: currentWord.index(before: currentWord.endIndex)) } else { - SpeechUtil.speak(textToSpeak: "No characters in this word to delete") + SpeechUtil.speak(textToSpeak: "No characters in this word to delete", speechRate: 0.5) } } VisualUtil.updateViewAndAnnounce(letter: keyboardController.currentValues.getCurrentValue()) @@ -82,7 +82,7 @@ class TrainingMode : InputMode { keyboardController.currentValues.resetIndexes() if trainingLevel == .space && startingIndex < key!.getStrings().count { - SpeechUtil.speak(textToSpeak: "Space inserted") + SpeechUtil.speak(textToSpeak: "Space inserted", speechRate: 0.5) } if shouldSwapBack && startingIndex >= key!.getStrings().count { @@ -92,11 +92,11 @@ class TrainingMode : InputMode { var text = "The next word to spell is " text += providedTrainingWords != nil ? providedTrainingWords![startingIndex] : defaultTrainingWords[startingIndex] - SpeechUtil.speak(textToSpeak: text) + SpeechUtil.speak(textToSpeak: text, speechRate: 0.5) } override func onLongHold() { - return + onHold() } enum TRAINING_LEVELS: Int { diff --git a/Swift/VInput Keyboard/TutorialMode.swift b/Swift/VInput Keyboard/TutorialMode.swift index 4600225..bdc174b 100644 --- a/Swift/VInput Keyboard/TutorialMode.swift +++ b/Swift/VInput Keyboard/TutorialMode.swift @@ -44,11 +44,11 @@ class TutorialMode : Mode { func initialize() { KeyboardViewController.letterLabel.text = "" if tutorialIndex == 0 { - SpeechUtil.speak(textToSpeak: "You've entered tutorial mode.", postDelay: TimeInterval(4)) - SpeechUtil.speak(textToSpeak: tut[tutorialIndex], postDelay: TimeInterval(4)) + SpeechUtil.speak(textToSpeak: "You've entered tutorial mode.", postDelay: TimeInterval(4),speechRate: 0.5) + SpeechUtil.speak(textToSpeak: tut[tutorialIndex], postDelay: TimeInterval(4),speechRate: 0.5) tutorialIndex = tutorialIndex + 1 } - SpeechUtil.speak(textToSpeak: tut[tutorialIndex]) + SpeechUtil.speak(textToSpeak: tut[tutorialIndex], speechRate: 0.5) } @@ -61,7 +61,7 @@ class TutorialMode : Mode { if tutorialIndex > 0 { tutorialIndex = tutorialIndex - 1 } - SpeechUtil.speak(textToSpeak: tut[tutorialIndex]) + SpeechUtil.speak(textToSpeak: tut[tutorialIndex], speechRate: 0.5) } func onSwipeRight() { @@ -69,7 +69,7 @@ class TutorialMode : Mode { if tutorialIndex < tut.count{ tutorialIndex = tutorialIndex + 1 } - SpeechUtil.speak(textToSpeak: tut[tutorialIndex]) + SpeechUtil.speak(textToSpeak: tut[tutorialIndex], speechRate: 0.5) } func onSwipeUp() { @@ -79,13 +79,13 @@ class TutorialMode : Mode { func swipeDown() { SpeechUtil.stopSpeech() - SpeechUtil.speak(textToSpeak: "Exiting tutorial mode") + SpeechUtil.speak(textToSpeak: "Exiting tutorial mode", speechRate: 0.5) ModeUtil.swapMode(keyboardController: keyboardController, stateKey: Key(index: tutorialIndex, callingMode: .tutorial), mode: .input) } func doubleTap() { SpeechUtil.stopSpeech() - SpeechUtil.speak(textToSpeak: tut[tutorialIndex]) + SpeechUtil.speak(textToSpeak: tut[tutorialIndex], speechRate: 0.5) } func onHold() { @@ -119,11 +119,18 @@ class TutorialMode : Mode { ModeUtil.swapMode(keyboardController: keyboardController, stateKey: Key(index: tutorialIndex, trainingStrings: trainingStrings, callingMode: .tutorial), mode: .training) } + //*** STUBBED *** func onTwoTouchTap() { return } + //*** STUBBED *** func onTwoTouchHold() { return } + + //*** STUBBED *** + func onTwoFingerSwipeRight() { + return + } } diff --git a/Swift/VInput Keyboard/ValueUtil.swift b/Swift/VInput Keyboard/ValueUtil.swift index 0beb4cf..0371a97 100644 --- a/Swift/VInput Keyboard/ValueUtil.swift +++ b/Swift/VInput Keyboard/ValueUtil.swift @@ -10,13 +10,20 @@ import Foundation class ValueUtil { - enum VALUE_TYPE + enum VALUE_TYPE: Int { - case numerical - case lowercase + case lowercase = 0 case uppercase + case numerical case emoji - case training + case punctuation + case common_words + + func numValueTypes() -> Int { + //This is bad b/c hardcoded, but it's not like it even matters + return VALUE_TYPE.common_words.rawValue + } + } static func swapMode(keyboardController: KeyboardViewController, valueType: VALUE_TYPE) @@ -30,6 +37,10 @@ class ValueUtil { toSwap = CapitalAlphaValues(valueType: .uppercase, presetLeftIndex: keyboardController.currentValues.getLeftIndex(), presetRightIndex: keyboardController.currentValues.getRightIndex()) case .emoji: toSwap = EmojiValues() + case .punctuation: + toSwap = PunctuationValues() + case .common_words: + toSwap = MostCommonValues(keyboardController: keyboardController) default: toSwap = NumericalValues() } diff --git a/Swift/VInput Keyboard/VisualUtil.swift b/Swift/VInput Keyboard/VisualUtil.swift index d46699a..4cce176 100644 --- a/Swift/VInput Keyboard/VisualUtil.swift +++ b/Swift/VInput Keyboard/VisualUtil.swift @@ -13,7 +13,7 @@ class VisualUtil { static func updateViewAndAnnounce(letter: String) { KeyboardViewController.letterLabel.text = letter - let text = "Left or right of " + letter + let text = "Left or right of " + letter.uppercased() SpeechUtil.speak(textToSpeak: text) } diff --git a/Swift/VInput.xcodeproj/project.pbxproj b/Swift/VInput.xcodeproj/project.pbxproj index 63edd80..3a4c5db 100644 --- a/Swift/VInput.xcodeproj/project.pbxproj +++ b/Swift/VInput.xcodeproj/project.pbxproj @@ -23,6 +23,14 @@ CE2C0C3F1DDCDD4A0074BCEF /* TutorialMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2C0C3D1DDCDD4A0074BCEF /* TutorialMode.swift */; }; CE2C0C401DDCDD4A0074BCEF /* TutorialMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2C0C3D1DDCDD4A0074BCEF /* TutorialMode.swift */; }; CE2C0C411DDCDD4A0074BCEF /* TutorialMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2C0C3D1DDCDD4A0074BCEF /* TutorialMode.swift */; }; + CE38268B1DFD10C6000F81C3 /* PunctuationValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE38268A1DFD10C6000F81C3 /* PunctuationValues.swift */; }; + CE38268C1DFD10C6000F81C3 /* PunctuationValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE38268A1DFD10C6000F81C3 /* PunctuationValues.swift */; }; + CE38268D1DFD10C6000F81C3 /* PunctuationValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE38268A1DFD10C6000F81C3 /* PunctuationValues.swift */; }; + CE38268E1DFD10C6000F81C3 /* PunctuationValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE38268A1DFD10C6000F81C3 /* PunctuationValues.swift */; }; + CE3826901DFD2692000F81C3 /* MostCommonValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE38268F1DFD2692000F81C3 /* MostCommonValues.swift */; }; + CE3826911DFD2692000F81C3 /* MostCommonValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE38268F1DFD2692000F81C3 /* MostCommonValues.swift */; }; + CE3826921DFD2692000F81C3 /* MostCommonValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE38268F1DFD2692000F81C3 /* MostCommonValues.swift */; }; + CE3826931DFD2692000F81C3 /* MostCommonValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE38268F1DFD2692000F81C3 /* MostCommonValues.swift */; }; CED2B0A41DDD0C4E00A5827F /* Values.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED2B0A31DDD0C4E00A5827F /* Values.swift */; }; CED2B0A51DDD0C4E00A5827F /* Values.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED2B0A31DDD0C4E00A5827F /* Values.swift */; }; CED2B0A71DDD12E400A5827F /* SpeechUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED2B0A61DDD12E400A5827F /* SpeechUtil.swift */; }; @@ -106,6 +114,8 @@ CE2C0C331DDCDB790074BCEF /* Mode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Mode.swift; sourceTree = ""; }; CE2C0C381DDCDB8D0074BCEF /* InputMode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InputMode.swift; sourceTree = ""; }; CE2C0C3D1DDCDD4A0074BCEF /* TutorialMode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TutorialMode.swift; sourceTree = ""; }; + CE38268A1DFD10C6000F81C3 /* PunctuationValues.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PunctuationValues.swift; sourceTree = ""; }; + CE38268F1DFD2692000F81C3 /* MostCommonValues.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MostCommonValues.swift; sourceTree = ""; }; CED2B0A31DDD0C4E00A5827F /* Values.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Values.swift; sourceTree = ""; }; CED2B0A61DDD12E400A5827F /* SpeechUtil.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpeechUtil.swift; sourceTree = ""; }; CED2B0A91DDD18C900A5827F /* LowerAlphaValues.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LowerAlphaValues.swift; sourceTree = ""; }; @@ -191,6 +201,8 @@ CEEE678E1DE2A29C0043A4F1 /* CapitalAlphaValues.swift */, CED2B0AC1DDD18E000A5827F /* EmojiValues.swift */, CED2B0AF1DDD326000A5827F /* TrainingValues.swift */, + CE38268A1DFD10C6000F81C3 /* PunctuationValues.swift */, + CE38268F1DFD2692000F81C3 /* MostCommonValues.swift */, ); name = Values; sourceTree = ""; @@ -356,7 +368,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0800; - LastUpgradeCheck = 0800; + LastUpgradeCheck = 0810; ORGANIZATIONNAME = "EECS481-VInput"; TargetAttributes = { FE3CCB6F1DC286DE0036DEF1 = { @@ -457,7 +469,9 @@ CED2B0B01DDD326000A5827F /* TrainingValues.swift in Sources */, 1D5E48AA1DDFEBA9008AE472 /* Prediction.xcdatamodeld in Sources */, FE3CCB761DC286DE0036DEF1 /* ViewController.swift in Sources */, + CE38268B1DFD10C6000F81C3 /* PunctuationValues.swift in Sources */, FE3CCB741DC286DE0036DEF1 /* AppDelegate.swift in Sources */, + CE3826901DFD2692000F81C3 /* MostCommonValues.swift in Sources */, CEEE67841DE219860043A4F1 /* ModeUtil.swift in Sources */, CE2C0C391DDCDB8D0074BCEF /* InputMode.swift in Sources */, CEEE67951DE2D2BC0043A4F1 /* NumericalValues.swift in Sources */, @@ -479,8 +493,10 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + CE38268C1DFD10C6000F81C3 /* PunctuationValues.swift in Sources */, CE2C0C351DDCDB790074BCEF /* Mode.swift in Sources */, FE3CCB891DC286DE0036DEF1 /* VInputTests.swift in Sources */, + CE3826911DFD2692000F81C3 /* MostCommonValues.swift in Sources */, CE2C0C3F1DDCDD4A0074BCEF /* TutorialMode.swift in Sources */, CE2C0C3A1DDCDB8D0074BCEF /* InputMode.swift in Sources */, ); @@ -490,8 +506,10 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + CE38268D1DFD10C6000F81C3 /* PunctuationValues.swift in Sources */, CE2C0C361DDCDB790074BCEF /* Mode.swift in Sources */, FE3CCB941DC286DE0036DEF1 /* VInputUITests.swift in Sources */, + CE3826921DFD2692000F81C3 /* MostCommonValues.swift in Sources */, CE2C0C401DDCDD4A0074BCEF /* TutorialMode.swift in Sources */, CE2C0C3B1DDCDB8D0074BCEF /* InputMode.swift in Sources */, ); @@ -510,7 +528,9 @@ CEEE67851DE219860043A4F1 /* ModeUtil.swift in Sources */, CED2B0B11DDD326000A5827F /* TrainingValues.swift in Sources */, CED2B0BA1DDD420F00A5827F /* TrainingMode.swift in Sources */, + CE3826931DFD2692000F81C3 /* MostCommonValues.swift in Sources */, CEEE67961DE2D2BC0043A4F1 /* NumericalValues.swift in Sources */, + CE38268E1DFD10C6000F81C3 /* PunctuationValues.swift in Sources */, CED2B0AE1DDD18E000A5827F /* EmojiValues.swift in Sources */, CED2B0A51DDD0C4E00A5827F /* Values.swift in Sources */, 1D5E48AC1DDFEE0B008AE472 /* AppDelegate.swift in Sources */, @@ -581,6 +601,7 @@ CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_SUSPICIOUS_MOVES = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; @@ -630,6 +651,7 @@ CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_SUSPICIOUS_MOVES = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 0000000..684e2f3 --- /dev/null +++ b/TESTING.md @@ -0,0 +1,57 @@ +# VInput Testing +We discovered over the course of our project that the existing iOS testing environment is notably limited for 3rd party custom keyboards, and particularly for a more unconventional keyboard like ours. Therefore, we were not able to develop automated testing using these libraries as we originally hoped to. Instead, we devised a set of standard tests we could perform with major changes to test functionality. + +## Test 1: Basic Insertion +**Procedure:** Open the keyboard and swipe up. + +**Expected Behavior:** The character β€˜m’ should be inserted into the textbox and the corresponding announcement for this event is made. + +## Test 2: Bounds of the Alphabet are Constrained +**Procedure:** Swipe all the way until β€˜a’ and then keep swiping left. + +**Expected Behavior:** The user should be left with β€˜a’ as their only option. + +## Test 3: Reset Selected Letter +**Procedure:** Open the keyboard, swipe right, then swipe down. + +**Expected Behavior:** The current character should change from β€˜m’ initially, to ’s’, and then reset back to β€˜m’. + +## Test 4: Backspace Selected Letter +**Procedure:** Open the keyboard, swipe up, then swipe down. + +**Expected Behavior:** The character β€˜m’ should be inserted and then removed such that the text box is blank. + +## Test 5: Insert a Space +**Procedure:** Open the keyboard, swipe up, double tap, then swipe up. + +**Expected Behavior:** There should be two 'm's separated by a space in the text field. + +## Test 6: Repeat Current Character +**Procedure:** After opening the keyboard, double tap once. + +**Expected Behavior:** "Left or right of m" should be announced again. + +## Test 7: Repeat Previous Word +**Procedure:** Type the word "dog", and then hold with two fingers briefly anywhere on the screen. + +**Expected Behavior:** The word β€œdog” should be read aloud. + +## Test 8: Uppercase the Current Letter +**Procedure:** Open the keyboard and hold one finger briefly and then release. + +**Expected Behavior:** The letter 'm' should change from lowercase to uppercase + +## Test 9: Alphabet Toggle +**Procedure:** With the keyboard open, swipe right with two fingers. + +**Expected Behavior:** The alphabet should change to either the emoji or number alphabets. + +## Test 10: Voice Over Toggle +**Procedure:** Turn VO on, open the keyboard, and swipe right with three fingers. + +**Expected Behavior:** The β€œNext Keyboard” button should be selected; double tapping on the screen will change to the next keyboard. + +## Test 11: Close the Keyboard +**Procedure:** Open the keyboard and then pinch gesture anywhere on the screen. + +**Expected Behavior:** The keyboard should slide down and dismiss.