Programatically changing button text and actions when a modifier key is pressed - swift

I would like to change the text of an NSButton when the ⌥ Option key is pressed - similar to the copy dialog when colliding files are detected on OS X which changes "Keep Both" to "Merge" when the option key is held.
In my case, I would like to change a button with text, say, "delete" to "quit" when I hold the option key. Additionally, its functionality should change in accordance with its title, much like the options in the Copy dialog.
Can this be done programatically in Swift?

You can subscribe using addLocalMonitorForEvents(matching:) and detect if option key pressed like this:
var optionKeyEventMonitor: Any? // property to store reference to the event monitor
// Add the event monitor
optionKeyEventMonitor = NSEvent.addLocalMonitorForEvents(matching: .flagsChanged) { event in
if event.modifierFlags.contains(.option) {
self.button.title = "copy"
self.button.action = #selector(self.copyButtonClicked(_:))
} else {
self.button.title = "delete"
self.button.action = #selector(self.deleteButtonClicked(_:))
}
return event
}
#IBAction func copyButtonClicked(_ sender: Any) {
Swift.print("Copy button was clicked!")
}
#IBAction func deleteButtonClicked(_ sender: Any) {
Swift.print("Delete button was clicked!")
}
Remember to remove the event monitor when you are done:
deinit {
if let eventMonitor = optionKeyEventMonitor {
NSEvent.removeMonitor(eventMonitor)
}
}
If you don't want a separate method called depending on the option key state, you can check modifierFlags when button is clicked instead:
#IBAction func buttonClicked(sender: NSButton) {
if NSEvent.modifierFlags().contains(.option) {
print("option pressed")
} else {
print("option not pressed")
}
}

In Swift 3 code:
func optionKeyDown() -> Bool
{
return NSEvent.modifierFlags().contains(NSEventModifierFlags.option)
}
if optionKeyDown()
{
print ("do option code")
}
else
{
print ("do normal code")
}

Related

Swift - How to get the sender tag for an array of buttons using UILongPressGestureRecognizer?

I have buttons in the storyboard that I put into a Referencing Outlet Collection. I'm using UITapGestureRecognizer and UILongPressGestureRecognizer for all of these buttons, but how can I print exactly which button gets tapped? Bellow is what I tried but doesn't work. I get an error that says "Value of type 'UILongPressGestureRecognizer' has no member 'tag'." I'm trying to build the button grid for the Minesweeper game. Thank you for your help.
class ViewController: UIViewController {
#IBOutlet var testButtons: [UIButton]! // There are 100 buttons in this array
override func viewDidLoad() {
super.viewDidLoad()
let testButtonPressed = UILongPressGestureRecognizer(target: self, action: #selector(testPressed))
testButtonPressed.minimumPressDuration = 0.5
// These indexes are just to test how to recognize which button gets pressed
testButtons[0].addGestureRecognizer(testButtonPressed)
testButtons[1].addGestureRecognizer(testButtonPressed)
}
#objc func testPressed(_ sender: UILongPressGestureRecognizer) {
print("Test button was pressed")
print(sender.tag) // THIS DOESN'T WORK, BUT CONCEPTUALLY THIS IS WHAT I WANT TO DO
}
This error occurs because UILongPressGestureRecognizer object has no tag property
You can access sender's button in a way like that:
#objc func testPressed(_ sender: UILongPressGestureRecognizer) {
guard let button = sender.view as? UIButton else { return }
print(button.tag)
}
I think that the best solution to handle button's actions is to add #IBAction
(you can add it like #IBOutlet with a minor change - set Action connection type)
And then in #IBAction block you cann access all button properties (like tag and others)
instead of using gesture I think it would be better to use #IBAction and connect the buttons With it here is a small example
UILongPressGestureRecognizer which is a subclass of UIGestureRecognizer, can be used only once per button or view. Because UILongPressGestureRecognizer has only a single view property. In your code, it will always be testButtons[1] calling the testPressed action. So you have to first modify the viewDidLoad code like this :-
for button in testButtons {
let testButtonPressed = UILongPressGestureRecognizer(target: self, action: #selector(testPressed))
testButtonPressed.minimumPressDuration = 0.5
button.addGestureRecognizer(testButtonPressed)
button.addGestureRecognizer(testButtonPressed)
}
Then you can access the button from testPressed like this (I hope you've already set the tag in the storyboard) :-
#objc func testPressed(_ sender: UILongPressGestureRecognizer) {
if sender.state == .began {
if let button = sender.view as? UIButton {
print(button.tag)
}
}
}
You need to set tags before pressing!
On the viewDidLoad() method you must add something like:
testButtons.enumerated().forEach {
let testButtonPressed = UILongPressGestureRecognizer(target: self, action: #selector(testPressed))
testButtonPressed.minimumPressDuration = 0.5
$0.element.addGestureRecognizer(testButtonPressed)
$0.element.tag = $0.offset
}
And when the long press is receiving you need to get a tag from view not from the sender!
print(sender.view?.tag)
Since a gesture recognizer should only be associated with a single view, and doesn't directly support using an identity tag to match it with buttons. When creating an array of buttons for a keyboard, with a single gesture response function, I found it easier to use the gesture recognizer "name" property to identify the associated button.
var allNames: [String] = []
// MARK: Long Press Gesture
func addButtonGestureRecognizer(button: UIButton, name: String) {
let longPrssRcngr = UILongPressGestureRecognizer.init(target: self, action: #selector(longPressOfButton(gestureRecognizer:)))
longPrssRcngr.minimumPressDuration = 0.5
longPrssRcngr.numberOfTouchesRequired = 1
longPrssRcngr.allowableMovement = 10.0
longPrssRcngr.name = name
allNames.append(name)
button.addGestureRecognizer(longPrssRcngr)
}
// MARK: Long Key Press
#objc func longPressOfButton(gestureRecognizer: UILongPressGestureRecognizer) {
print("\nLong Press Button => \(String(describing: gestureRecognizer.name)) : State = \(gestureRecognizer.state)\n")
if gestureRecognizer.state == .began || gestureRecognizer.state == .changed {
if let keyName = gestureRecognizer.name {
if allNames.contains(keyName) {
insertKeyText(key: keyName)
} else {
print("No action available for key")
}
}
}
}
To implement, call the addButtonGestureRecognizer function after creating the button, and provide a name for the button (I used the button text) e.g.
addButtonGestureRecognizer(button: keyButton, name: buttonText)
The button name is stored in the "allNames" string array so that it can be matched later in "longPressOfButton".
When the button name is matched in the "longPressOfButton" response function, it passes it to "addKeyFunction" where it is processed.

Prevent NSPopUpButton to open its menu

I need to prevent the popup's menu to be opened when some conditions are met, so I implemented "willOpenMenu" to know when the menu is about to be opened:
class MyPopUpButton: NSPopUpButton {
override func willOpenMenu(_ menu: NSMenu, with event: NSEvent) {
print("willOpenMenu")
// it fires, but what I can do to prevent menu to be opened??
}
}
How can I prevent, now, the menu to not show up?
EDIT
below, in order, what happens when you click on a popup (Type column), when a "Value" column is under editing
Finally I found the way:
override func willOpenMenu(_ menu: NSMenu, with event: NSEvent) {
if (self.outline != nil) {
/*
the following is needed when user click on the type column:
He can have ongoing edits on some rows, so changing first responder
We can aesily inform the outline that We're playing with something else
and have to force ask the user in case edits are wrong.
This also prevent a crash.
*/
if (self.outline?.wrongValue)! {
// this grant the popup menu to not showup (or disappear so quickly)
menu.cancelTrackingWithoutAnimation()
}
self.window?.makeFirstResponder(self)
self.window?.makeFirstResponder(self.outline)
}
}
self.outline is a get/set variable. "wrongvalue" works this way:
override func controlTextDidBeginEditing(_ obj: Notification) {
if obj.object is MyTextField {
self.outline.wrongValue = true
}
}
override func controlTextDidEndEditing(_ obj: Notification) {
if obj.object is MyTextField {
/*
some code here to check if stringValue is ok for the selected tag, otherwise
show the relative alert, the popup menu will not show up anymore
because cancelTracking is called on it
*/
self.outline.wrongValue = false
}
}
cancelTracking() was the key!

Best strategy in swift to detect keyboad input in NSViewController

I want to detect keyboard input in my NSViewController.
The idea is to have several actions performed if the user presses certain keys followed by ENTER/RETURN.
I have checked if keyDown would be a appropriate way. But I would receive an event any time the user has pressed a key.
I also have though on using an NSTextField, set it to hidden and let it have the focus.
But maybe there are other better solution.
Any ideas?
Thanks
I've finally got a solution that I like.
First it has nothing todo with any hidden UI Elements but rather let the viewcontroller detect keyboard input.
var monitor: Any?
var text = ""
override func viewDidLoad() {
super.viewDidLoad()
self.monitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown, handler: myKeyDownEvent)
}
override func viewWillDisappear() {
//Clean up in case your ViewController can be closed an reopened
if let monitor = self.monitor {
NSEvent.removeMonitor(monitor)
}
}
// Detect each keyboard event
func myKeyDownEvent(event: NSEvent) -> NSEvent {
// keyCode 36 is for detect RETURN/ENTER
if event.keyCode == 36 {
print(text)
text = ""
} else {
text.append( event.characters! )
}
return event
}

use a slider while pressing button swift

Using Xcode 8, Swift 3 and Interface Builder with Storyboards and a very basic view controller featuring only a button (Play) and a slider (volume).
I'm trying to use my NSSlider while pressing a button at the same time. For instance, I need to be able to keep pressing "Play" while slowly lowering the volume. What happens instead is that only one button can be in use at a time. So, as long as I am pressing "Play" using the return key, I can't adjust the volume and vice versa.
In this example, the Play button is triggered by the "return" key.
Here is the code in ViewController.swift. This is just an example app I created quickly to study the problem.
Is there an Interface Builder setting on the NSSLider that I am missing, or can I do something in my code to achieve what I am trying to do?
import Cocoa
import AVFoundation
class ViewController: NSViewController {
var audioPlayer = AVAudioPlayer()
var sliderValue: Float = 0
#IBOutlet weak var volumeSliderOutlet: NSSlider!
override func viewDidLoad() {
super.viewDidLoad()
do{
audioPlayer = try AVAudioPlayer(contentsOf: URL.init(fileURLWithPath: Bundle.main.path(forResource: "bell ding", ofType: "wav")!))
}
catch {
print(error)
}
audioPlayer.prepareToPlay()
// Do any additional setup after loading the view.
}
#IBAction func playButtonAction(_ sender: NSButton) {
if audioPlayer.isPlaying {
audioPlayer.currentTime = 0.0
audioPlayer.play()
} else {
audioPlayer.prepareToPlay()
audioPlayer.play()
}
}
#IBAction func volumeSliderAction(_ sender: NSSlider) {
if audioPlayer.isPlaying {
sliderValue = self.volumeSliderOutlet.floatValue
audioPlayer.volume = sliderValue
} else {
return
}
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
}
I've also tried this, based on advice from #inspector_60, but it doesn't work either. Still can't handle the slider while continuing to press the shortcut key for the button "return".
override func keyDown(with event: NSEvent) {
if (event.keyCode == 36) {
self.playButtonAction(nil)
}
else {
return
}
}
func playButtonAction(_ sender: NSButton?) {
if audioPlayer.isPlaying {
audioPlayer.currentTime = 0.0
audioPlayer.play()
} else {
audioPlayer.prepareToPlay()
audioPlayer.play()
}
}
IBAction are UI events so not suitable for multiple simultaneous clicks (you have only one mouse pointer). Use handling key events in order to handle keyboard events for simultaneous actions.
You need to also implement the mouse events, these links should help:
Apple document about mouse events and specifically Filtering Out Key Events During Mouse-Tracking Operations
This stack overflow answer on how to implement this approach
This post that will give you more information and example - Ron's answer links to a question not that different from yours

How do I capture a button event type in swift

I'm having trouble finding information on how to capture touch events. Event.type and event.description does not help me, and the documentation does not have examples as far as I can tell.
I'm creating an IBAction on several buttons and want the (shared) action to deal with named buttons (sender.currentTitle) and (events.??) to add code for whether the button was tapped or long pressed.
#IBAction func ChangeSort(sender: UIButton, forEvent event: UIEvent!) {
// Trying to capture the button action? Type?? Description?
switch event.type {
case "Touch Up Inside":
// Switching on currenttitle works fine.
switch sender.currentTitle! {
case "Amount":
MyQueries.QppQueryOrderBy = "Order by Amount"
// Mylabel.text = "sss"
default: ...
}
case "Touch Down Repeat":
print("In long press")
switch sender.currentTitle! {
case "Amount":
MyQueries.QppQueryOrderBy = "Order by Amount desc"
default: ...
}
default:
print("in def")
switch sender.currentTitle! {
case "Amount":...
MyQueries.QppQueryOrderBy = "Order by LastActivityDate desc"
default:
MyQueries.QppQueryOrderBy = "Order by Amount desc"
}
}
self.theTableView.reloadData()
}
The UIEvent you are getting back is not going to tell you what you want. UIEvent will only tell you Touches, Motion, or Remote Control. You are looking for UIControlEvent which is pretty hard to get ahold of from within the iBAction. The more standard way to solve this problem would be to make multiple #IBAction func and assign the different types of control events to the different ones.
So for all of your named buttons, you would assign the touch up inside event to the same sortByTitle func.
#IBAction func sortByTitle(sender: UIButton) {
// This is associated with TouchUpInside in IB
switch sender.currentTitle! {
case "Amount":
MyQueries.QppQueryOrderBy = "Order by Amount"
// Mylabel.text = "sss"
default: ...
}
self.theTableView.reloadData()
}
#IBAction func sortByTitleDescending(sender: UIButton) {
//This is associated with Touch Down Repeat
print("In long press")
switch sender.currentTitle! {
case "Amount":
MyQueries.QppQueryOrderBy = "Order by Amount desc"
default: ...
}
self.theTableView.reloadData()
}
Basically if you were to create your button in code, when you initialized it, you would add a func for each of the control events you wanted to watch for using the addTarget method on UIControl/UIButton. In addTarget you specify a different func for each one. In IB you are just doing this in graphical form.