Prevent NSPopUpButton to open its menu - swift

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!

Related

Swift 5 - Mac OS - NSTrackingArea overlapping views

Currently I have a little issue when it comes to buttons(NSButton) which have a tracking area and views(NSView overlay) above these buttons, this is my setup:
Custom button:
class AppButton: NSButton {
override func updateTrackingAreas() {
super.updateTrackingAreas()
let area = NSTrackingArea(
rect: self.bounds,
options: [.mouseEnteredAndExited, .activeAlways],
owner: self,
userInfo: nil
)
self.addTrackingArea(area)
}
override func mouseEntered(with event: NSEvent) {
NSCursor.pointingHand.set()
}
override func mouseExited(with event: NSEvent) {
NSCursor.arrow.set()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
An instance of this button class is used i a very basic NSView.
When I hover over the button, the cursor changes correctly.
When I click the button a new overlay(NSView) is opened above the button...
This is where the problem starts:
When I hover over the overlay where my button is placed, the cursor still changes...
I did not know that a NSTrackingArea is going through all views..
How can i solve this issue?
Can I set any property on the overlay(NSView) to somehow disable the NSTrackingArea on the button?
Thanks!!
You can subclass NSView and add local monitoring for events. Check if the event has occurred over the view and if true return nil. This will avoid propagating the events being monitored. If the event is outside the view frame you can propagate it normally returning the event monitored.
class CustomView: NSView {
override func viewWillMove(toSuperview newSuperview: NSView?) {
super.viewWillMove(toSuperview: newSuperview)
wantsLayer = true
layer?.backgroundColor = NSColor.windowBackgroundColor.cgColor
layer?.borderWidth = 1
NSEvent.addLocalMonitorForEvents(matching: [.mouseEntered, .mouseExited, .leftMouseDown]) { event in
if self.frame.contains(event.locationInWindow) {
// if cursor is over the view just return nil to do not propagate the events
return nil
}
return event
}
}
}
If you are trying to stop mouse events from a viewController, add this code in viewDidLoad
NSEvent.addLocalMonitorForEvents(matching: [.mouseEntered, .mouseExited, .leftMouseDown]) { event in
if self.view.frame.contains(event.locationInWindow) {
return nil
}
return event
}

NSTextField keep focus/first responder after NSPopover

The object of this app is to ensure the user has entered a certain text in an NSTextField. If that text is not in the field, they should not be allowed to leave the field.
Given a macOS app with a subclass text field, a button and another generic NSTextField. When the button is clicked, an NSPopover is shown which is 'attached' to the field which is controlled by an NSViewController called myPopoverVC.
For example, the user enters 3 in the top field and then clicks the Show Popover button which displays the popover and provides a hint: 'What does 1 + 1 equal'.
Note this popover has a field labelled 1st resp so when the popover shows, that field becomes the first responder. Nothing will be entered at this time - it's just for this question.
The user would click the Close button, which closes the popover. At that point what should happen if the user clicks or tabs away from the field with the '3' in it, the app should not permit that movement - perhaps emitting a Beep or some other message. But what happens when the popover closes and the user presses Tab
Even though that field with the '3' in it had a focus ring, which should indicate the first responder again in that window, the user can click or tab away from it as the textShouldEndEditing function is not called. In this case, I clicked the close button in the popover, the '3' field had a focus ring and I hit tab, which then went to the next field.
This is the function in the subclassed text field that works correctly after the text has been entered into the field. In this case, if the user types a 3 and then hits Tab, the cursor stays in that field.
override func textShouldEndEditing(_ textObject: NSText) -> Bool {
if self.aboutToShowPopover == true {
return true
}
if let editor = self.currentEditor() { //or use the textObject
let s = editor.string
if s == "2" {
return true
}
return false
}
The showPopover button code sets the aboutToShowPopover flag to true which will allow the subclass to show the popover. (set to false when the popover closes)
So the question is when the popover closes how to return the firstResponder status to the original text field? It appears to have first responder status, and it thinks it has that status although textShouldEndEditing is not called. If you type another char into the field, then everything works as it should. It's as if the window's field editor and the field with the '3' in it are disconnected so the field editor is not passing calls up to that field.
The button calls a function which contains this:
let contentSize = myPopoverVC.view.frame
theTextField.aboutToShowPopover = true
parentVC.present(myPopoverVC, asPopoverRelativeTo: contentSize, of: theTextField, preferredEdge: NSRectEdge.maxY, behavior: NSPopover.Behavior.applicationDefined)
NSApplication.shared.activate(ignoringOtherApps: true)
the NSPopover close is
parentVC.dismiss(myPopoverVC)
One other piece of information. I added this bit of code to the subclassed NSTextField control.
override func becomeFirstResponder() -> Bool {
let e = self.currentEditor()
print(e)
return super.becomeFirstResponder()
}
When the popover closes and the textField becomes the windows first responder, that code executes but prints nil. Which indicates that while it is the first responder it has no connection to the window fieldEditor and will not receive events. Why?
If anything is unclear, please ask.
Here's my attempt with help from How can one programatically begin a text editing session in a NSTextField? and How can I make my NSTextField NOT highlight its text when the application starts?:
The selected range is saved in textShouldEndEditing and restored in becomeFirstResponder. insertText(_:replacementRange:) starts an editing session.
var savedSelectedRanges: [NSValue]?
override func becomeFirstResponder() -> Bool {
if super.becomeFirstResponder() {
if self.aboutToShowPopover {
if let ranges = self.savedSelectedRanges {
if let fieldEditor = self.currentEditor() as? NSTextView {
fieldEditor.insertText("", replacementRange: NSRange(location: 0, length:0))
fieldEditor.selectedRanges = ranges
}
}
}
return true
}
return false
}
override func textShouldEndEditing(_ textObject: NSText) -> Bool {
if super.textShouldEndEditing(textObject) {
if self.aboutToShowPopover {
let fieldEditor = textObject as! NSTextView
self.savedSelectedRanges = fieldEditor.selectedRanges
return true
}
let s = textObject.string
if s == "2" {
return true
}
}
return false
}
Maybe rename aboutToShowPopover.
If you subclass each of your NSTextField, you could override the method becomeFirstResponder and make it send self to a delegate class you will create, that will keep a reference of the current first responder:
NSTextField superclass:
override func becomeFirstResponder() -> Bool {
self.myRespondersDelegate.setCurrentResponder(self)
return super.becomeFirstResponder()
}
(myRespondersDelegate: would optionally be your NSViewController)
Note: do not use the same superclass for your alerts TextFields and ViewController TextFields. Use this superclass with added functionality only for TextFields you would want to return to firstResponder after an alert is closed.
NSTextField delegate:
class MyViewController: NSViewController, MyFirstResponderDelegate {
var currentFirstResponderTextField: NSTextField?
func setCurrentResponder(textField: NSTextField) {
self.currentFirstResponderTextField = textField
}
}
Now, after your pop is dismissed, you could in viewWillAppear or create a delegate function that will be called on a pop up dismiss didDismisss (Depends how your pop up is implemented, I will show the delegate option)
Check If a TextField has existed, and re-make it, the firstResponder.
Pop up delegate:
class MyViewController: NSViewController, MyFirstResponderDelegate, MyPopUpDismissDelegate {
var currentFirstResponderTextField: NSTextField?
func setCurrentResponder(textField: NSTextField) {
self.currentFirstResponderTextField = textField
}
func didDismisssPopUp() {
guard let isLastTextField = self.currentFirstResponderTextField else {
return
}
self.isLastTextField?.window?.makeFirstResponder(self.isLastTextField)
}
}
Hope it works.
Huge thanks to Willeke for the help and an answer that lead to a pretty simple solution.
The big picture issue here was that when the popover closed, the 'focused' field was the original field. However, it appears (for some reason) that the windows field editor delegate disconnected from that field so functions such as control:textShouldEndEditing were not being passed to the subclassed field in the question.
Executing this line when the field becomes the first reponder seems to re-connect the windows field editor with this field so it will receive delegate messages
fieldEditor.insertText("", replacementRange: range)
So the final solution was a combination of the following two functions.
override func textShouldEndEditing(_ textObject: NSText) -> Bool {
if self.aboutToShowPopover == true {
return true
}
let s = textObject.string
if s == "2" {
return true
}
return false
}
override func becomeFirstResponder() -> Bool {
if super.becomeFirstResponder() == true {
if let myEditor = self.currentEditor() as? NSTextView {
let range = NSMakeRange(0, 0)
myEditor.insertText("", replacementRange: range)
}
return true
}
return false
}

NSTextField force becomeFirstResponder removes text

Using NSTextField on Custom NSTableCellView.
When the user presses anywhere on the cell, I would like NSTextField to become the first responder.
When I do, the text disappears, and the user needs to press any keyboard key for it to come back, how to disable this behavior?
Code:
override func mouseDown(with event: NSEvent) {
super.mouseDown(with: event)
if (event.clickCount == 2) {
self.beginEditing()
} else {
self.endEditing()
}
}
private func beginEditing() {
self.lblTitle.isEditable = true
self.window?.makeFirstResponder(self.lblTitle)
self.lblTitle.backgroundColor = Colors.clear
self.lblTitle.borderColor = Colors.clear
}
private func endEditing() {
self.lblTitle.isEditable = false
self.lblTitle.backgroundColor = Colors.red
}
Removing this lines solved it
self.lblTitle.backgroundColor = Colors.clear
self.lblTitle.borderColor = Colors.clear

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
}

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

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")
}