How to get the pressed keys in Swift - macos

I am trying to learn the Swift language, and I followed a lot of tutorials on Youtube. Since most of them are for iOS, I wanted to make the OSX version of this one: https://www.youtube.com/watch?v=8PrVHrs10to (SideScroller game)
I followed it carefully but I am stuck at about 22 minutes of the video when I have to update the player position. My first problem was to get the pressed keys. I come from C# language, so I wanted to find something like
If(Keyboard.GetState().IsKeyDown(Keys.Right))
man.Position.x += 5 //player position
but this doesn't exist, so I figured out this:
override func keyDown(theEvent: NSEvent!)
{
updateManPosition(theEvent)
}
func updateManPosition(theEvent:NSEvent)
{
if theEvent.keyCode == 123
{
man.position.x -= 2
}
else if theEvent.keyCode == 124
{
man.position.x += 2
}
else if theEvent.keyCode == 126
{
println("jump")
}
}
I found the corresponding value(123/124/126) by using println(theEvent.keyCode) but it's not very useful if I have to recognize a lot of keys.
Anyway, it works for this game, the position of the player changes. But I have another probem which is that the function keyDown doesn't seem to be called at each update (so 60 times per seconds) which prevent the player to move smoothly.
SO, here is my question: How can I have keyDown called at each update, and does anybody have a cleaner way to get the pressed Keys ?
Thank you

Okay I found how to update, so I post it here 'cause it might help others. The idea is to use a boolean:
var manIsMoving:Bool = false
override func keyDown(theEvent: NSEvent!) // A key is pressed
{
if theEvent.keyCode == 123
{
direction = "left" //get the pressed key
}
else if theEvent.keyCode == 124
{
direction = "right" //get the pressed key
}
else if theEvent.keyCode == 126
{
println("jump")
}
manIsMoving = true //setting the boolean to true
}
override func keyUp(theEvent: NSEvent!)
{
manIsMoving = false
}
override func update(currentTime: CFTimeInterval)
{
if manIsMoving
{
updateManPosition(direction)
}
else
{
cancelMovement()
}
}
It may help others.
But getting the pressed key's characters is still unclear...

In combination with the above method I have created a method to change a key to a character:
private func returnChar(theEvent: NSEvent) -> Character?{
let s: String = theEvent.characters!
for char in s{
return char
}
return nil
}
This will return a character (from the key that was pressed).
Then you can do something like this:
override func keyUp(theEvent: NSEvent) {
let s: String = String(self.returnChar(theEvent)!)
switch(s){
case "w":
println("w")
break
case "s":
println("s")
break
case "d":
println("d")
break
case "a":
println("a")
break
default:
println("default")
}
}
You could combine this method and the method from above to create something like a key listener.

Related

didChangeAutomaticCapitalizationNotification not triggered

What am I doing wrong? I don't get this notification. I have this function:
#objc func onAutocorrection (_ notification: Foundation.Notification) {
Swift.print("\(notification)")
}
later in the same class I do use it as follows:
NotificationCenter.default.addObserver(
self,
selector: #selector(onAutocorrection(_:)),
name: NSSpellChecker.didChangeAutomaticCapitalizationNotification,
object: nil)
The addObserver is executed, but the function is never called even when the application is capitalising in an NSTextView.
Why? Many thanks in advance!
It looks like I misunderstood the notification. It is not meant to be triggered when automatic capitalisation happens but when the systems preference of your Mac is changing.
See the comment of ever helpful Willeke and see Notification of autocorrect
In order to get to the intended result of reacting to autocapitalisation did I implement this function in the NSTextViewDelegate:
public func textView(_ view: NSTextView, didCheckTextIn range: NSRange, types checkingTypes: NSTextCheckingTypes, options: [NSSpellChecker.OptionKey : Any] = [:], results: [NSTextCheckingResult], orthography: NSOrthography, wordCount: Int) -> [NSTextCheckingResult] {
if !range.contains(0){
return results
}
var newResult = [NSTextCheckingResult]()
for result in results {
if let textToChange = view.string[range].components(separatedBy: " ").first, let replacement = result.replacementString?.components(separatedBy: " ").first {
let firstLetterCap = textToChange.capitalizingFirstLetter()
if replacement == firstLetterCap {
continue //don't add to results
}
}
newResult.append(result)
}
return newResult
}
This function will prevent that the first character will be capitalised.
Ultimately, I check whether the capitalised version of the first word of the range that must include position "0" is equal to the first word of the replacement string. And if it is then I remove that result/suggestion from the result list.

NSPressGestureRecognizer called before minimumPressDuration

I need my NSButton to respond to regular clicks, as well as long presses. I am adding NSPressGestureRecognizer like so:
override func viewDidLoad() {
super.viewDidLoad()
let gr = NSPressGestureRecognizer()
gr.minimumPressDuration = 1
gr.action = #selector(handleLongPress)
button.addGestureRecognizer(gr)
}
func handleLongPress(gr: NSPressGestureRecognizer) {
if gr.state == .Began {
Swift.print("long press")
}
}
Unfortunately, the handleLongPress randomly fires even at short single clicks, or double clicks. It happens even if i set the minimumPressDuration to higher values.
I have tried playing with the shouldBeRequiredToFailByGestureRecognizer but it is not solving the problem.
Is there something i am missing with my code?
I have solved this by handling multiple gesture recognizers:
the class must implement NSGestureRecognizerDelegate
var singleClick:NSClickGestureRecognizer?
var longClick:NSPressGestureRecognizer?
override func viewDidLoad() {
super.viewDidLoad()
longClick = NSPressGestureRecognizer()
longClick!.minimumPressDuration = 1
longClick!.action = #selector(handleGestureLong)
longClick!.delegate = self
singleClick = NSClickGestureRecognizer()
singleClick?.numberOfClicksRequired = 1
singleClick!.action = #selector(handleGestureSingle)
singleClick!.delegate = self
btn.addGestureRecognizer(longClick!)
btn.addGestureRecognizer(singleClick!)
}
func gestureRecognizer(gestureRecognizer: NSGestureRecognizer, shouldRequireFailureOfGestureRecognizer otherGestureRecognizer: NSGestureRecognizer) -> Bool {
if gestureRecognizer == singleClick && otherGestureRecognizer == longClick {
return false
}
return true
}
func handleGestureSingle(gr: NSClickGestureRecognizer) {
switch gr.state {
case .Ended:
print("single click")
break
default:
break
}
}
func handleGestureLong(gr: NSPressGestureRecognizer) {
switch gr.state {
case .Began:
print("long click")
break
default:
break
}
}

How to properly add global NSEvent listener for pressure events in Swift OS X application?

I'm pretty new in OS X programming and I'm trying to write an application that will capture Force Click event at system-wide context.
Based on various sources I wrote down code listed below:
var lastClickStage = -1
func checkAssistanceSettings () -> Bool {
let checkOptPrompt = kAXTrustedCheckOptionPrompt.takeUnretainedValue() as NSString
let options = [checkOptPrompt: true]
let accessEnabled = AXIsProcessTrustedWithOptions(options)
return accessEnabled == 1
}
func processForceClick(incomingEvent: NSEvent!) -> Void {
let clickStage = incomingEvent.stage
if clickStage == lastClickStage {
return
}
if (lastClickStage == 2 && clickStage != 2) {
lastClickStage = clickStage
return
}
if (clickStage == 2 && lastClickStage != 2) {
let applicationClicked = NSWorkspace.sharedWorkspace().frontmostApplication?.bundleIdentifier
if (applicationClicked != nil) {
NSLog("ForceClicked in \(applicationClicked!)!")
}
lastClickStage = clickStage
}
}
func processForceClickLocally(incomingEvent: NSEvent!) -> NSEvent {
processForceClick(incomingEvent)
return incomingEvent
}
func applicationDidFinishLaunching(aNotification: NSNotification) {
NSLog("\(checkAssistanceSettings())")
NSEvent.addLocalMonitorForEventsMatchingMask(NSEventMaskFromType(NSEventType.EventTypePressure), handler: processForceClickLocally)
NSEvent.addGlobalMonitorForEventsMatchingMask(NSEventMaskFromType(NSEventType.EventTypePressure), handler: processForceClick)
}
When I run my application local event listener seems to work like a charm, but global event listener never calls his handler, even if XCode or a specific built application gets grant to accessibility in System Settings (AXIsProcessTrustedWithOptions(options) evaluates as "true").
Can anyone point out what's wrong with it?
EDIT: Even strange thing discovered by me: seems like global listener not working with this NSEventMask. Got no problem with NSEventMask.LeftMouseDownMask for example.
So now seems like question is transformed to "What wrong with NSEventMask.EventMaskPressure in global listeners?".

How to use NSWindowOcclusionState.Visible in Swift

I am trying to implement window toggling (something I've done many times in Objective-C), but now in Swift. It seams that I am getting the use of NSWindowOcclusionState.Visible incorrectly, but I really cannot see my problem. Only the line w.makeKeyAndOrderFront(self) is called after the initial window creation.
Any suggestions?
var fileArchiveListWindow: NSWindow? = nil
#IBAction func tougleFileArchiveList(sender: NSMenuItem) {
if let w = fileArchiveListWindow {
if w.occlusionState == NSWindowOcclusionState.Visible {
w.orderOut(self)
}
else {
w.makeKeyAndOrderFront(self)
}
}
else {
let sb = NSStoryboard(name: "FileArchiveOverview",bundle: nil)
let controller: FileArchiveOverviewWindowController = sb?.instantiateControllerWithIdentifier("FileArchiveOverviewController") as FileArchiveOverviewWindowController
fileArchiveListWindow = controller.window
fileArchiveListWindow?.makeKeyAndOrderFront(self)
}
}
Old question, but I just run into the same problem. Checking the occlusionState is done a bit differently in Swift using the AND binary operator:
if (window.occlusionState & NSWindowOcclusionState.Visible != nil) {
// visible
}
else {
// not visible
}
In recent SDKs, the NSWindowOcclusionState bitmask is imported into Swift as an OptionSet. You can use window.occlusionState.contains(.visible) to check if a window is visible or not (fully occluded).
Example:
observerToken = NotificationCenter.default.addObserver(forName: NSWindow.didChangeOcclusionStateNotification, object: window, queue: nil) { note in
let window = note.object as! NSWindow
if window.occlusionState.contains(.visible) {
// window at least partially visible, resume power-hungry calculations
} else {
// window completely occluded, throttle down timers, CPU, etc.
}
}

How to get the correct callback value for NSUndoManager in Swift?

I have an NSDocument subclass, hooked up to an NSArrayController. For reference, I'm trying to translate the example from chapter 9 of Cocoa Programming for Mac OS X Fourth Edition.
It seems that from this question that I asked before, I need to use object-based undo with NSUndoManager. In order to pass two values to the method being invoked, I'm packaging them into an NSObject subclass with two instance variables.
When the KVO methods for inserting and deleting from the employees array are called by clicking on the buttons in my application, they work as expected.
However, when removeObjectFromEmployeesAtIndex is called during an undo operation, the index passed in is wildly out of bounds (for the first row, it always seems to be 55, then after that the index increases into the thousands for the next few rows).
How can I get the correct index to perform the undo action?
class Document: NSDocument {
var employee_list: Array<Person> = []
var employees: Array<Person> {
get {
return self.employee_list
}
set {
if newValue == self.employee_list {
return
}
self.employee_list = newValue
}
}
func insertObject(person: Person, inEmployeesAtIndex index: Int) {
self.undoManager.registerUndoWithTarget(self, selector: Selector("removeObjectFromEmployeesAtIndex:"), object: index)
if (!self.undoManager.undoing) {
self.undoManager.setActionName("Add Person")
}
employees.insert(person, atIndex: index)
}
func removeObjectFromEmployeesAtIndex(index: Int) {
let person = self.employees[index]
let pair = PersonIndexPair(person: person, index: index)
self.undoManager.registerUndoWithTarget(self, selector: Selector("insertPersonIndexPair:"), object: pair)
if (!self.undoManager.undoing) {
self.undoManager.setActionName("Remove Person")
}
employees.removeAtIndex(index)
}
func insertPersonIndexPair(pair: PersonIndexPair) {
insertObject(pair.person, inEmployeesAtIndex: pair.index)
}
}
Edit: I've worked around the issue by passing a string, but this seems pretty obtuse:
self.undoManager.registerUndoWithTarget(self, selector: Selector("removeObjectFromEmployeesAtStringIndex:"), object: String(index))
//...
func removeObjectFromEmployeesAtStringIndex(index: String) {
if let i = index.toInt() {
removeObjectFromEmployeesAtIndex(i)
}
}
Use NSNumber instead of Int.
self.undoManager.registerUndoWithTarget(self, selector:Selector("removeObjectFromEmployeesAtStringIndex:"), object: NSNumber(integer: index))
//...
func removeObjectFromEmployeesAtStringIndex(index: NSNumber) {
removeObjectFromEmployeesAtIndex(index.integerValue)
}
I am reading Cocoa Programming for Mac OS X now. I translate the sample Object-C code as below, only need to append it to class Document:
func insertObject(p: Person, inEmployeesAtIndex index: Int) {
//NSLog("adding %# to %#", p, employees)
let undo = self.undoManager
undo!.prepareWithInvocationTarget(self).removeObjectFromEmployeesAtIndex(index)
if !undo!.undoing {
undo!.setActionName("Add Person")
}
employees.insertObject(p, atIndex: index)
}
func removeObjectFromEmployeesAtIndex(index: Int) {
let p = employees.objectAtIndex(index) as! Person
//NSLog("Removing %# from %#", p, index)
let undo = self.undoManager
undo!.prepareWithInvocationTarget(self).insertObject(p, inEmployeesAtIndex: index)
if !undo!.undoing {
undo!.setActionName("Remove Person")
}
employees.removeObjectAtIndex(index)
}
I think it's cleanest to replace the old-fashioned Objective-C NSUndoManager method entirely:
private class SwiftUndoPerformer: NSObject {
let closure: Void -> Void
init(closure: Void -> Void) {
self.closure = closure
}
#objc func performWithSelf(retainedSelf: SwiftUndoPerformer) {
closure()
}
}
extension NSUndoManager {
func registerUndo(closure: Void -> Void) {
let performer = SwiftUndoPerformer(closure: closure)
registerUndoWithTarget(performer, selector: Selector("performWithSelf:"), object: performer)
//(Passes unnecessary object to get undo manager to retain SwiftUndoPerformer)
}
}
Then you can Swift-ly register any closure, and not worry about wrapping things up to get around an outdated interface:
undoManager.registerUndo {
self.insertObject(person, inEmployeesAtIndex: index)
}

Resources