How to detect which NSTextField is clicked in Cocoa? - swift

I have two NSTextField objects which I want to highlight when user clicks on it.
The initial text field is already highlighted on NSWindow load. I am able to get mouse down event for text field click, but unable to distinguish which textfield did the user tapped.
I tried using hitTest on the text field using the NSPoint obtained from the NSEvent object, but the NSView returned is nil. The view it returns is that of the window's view and not that text field.
class SettingsViewController: NSViewController {
private var sview: SettingsView?
override func viewDidLoad() {
initEvents()
}
override func loadView() {
if let settingsView = SettingsView.createFromNib() {
self.view = settingsView
self.sview = settingsView as? SettingsView
}
}
func initEvents() {
self.sview!.emailTextField.delegate = self
}
}
extension SettingsViewController: NSTextFieldDelegate, NSTextDelegate {
override func mouseDown(with event: NSEvent) {
self.log.debug("mouse down: \(event.buttonNumber), \(event.eventNumber), \(event.locationInWindow)")
// How to know which text field triggered this?
}
func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
self.log.debug("control delegate")
return false
}
func textField(_ textField: NSTextField, textView: NSTextView, shouldSelectCandidateAt index: Int) -> Bool {
self.log.debug("text field should select")
return true
}
func textShouldBeginEditing(_ textObject: NSText) -> Bool {
self.log.debug("text field should being editing")
return true
}
}
class SettingsView: NSView {
private let log = Logger()
private static var topLevelObjects: NSArray?
#IBOutlet weak var emailTextField: ASTextField!
#IBOutlet weak var passwordTextField: NSSecureTextField!
// ...
}
I am adding delegate to only one text field.
self.sview!.emailTextField.delegate = self
But when I click on the passwordTextField, I am getting the mouse click event as well. Why is this happening?
How to distinguish NSTextField mouse click and highlight the text field?
I tried subclassing NSTextField and adding click handler, but it is not working.
class ASTextField: NSTextField {
private let log = Logger()
required init?(coder: NSCoder) {
super.init(coder: coder)
bootstrap()
}
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
bootstrap()
}
override func awakeFromNib() {
super.awakeFromNib()
bootstrap()
}
func bootstrap() {
self.delegate = self
}
}
extension ASTextField: NSTextFieldDelegate {
override func mouseDown(with event: NSEvent) {
// This is not working
self.log.debug("mouse down")
super.mouseDown(with: event)
}
}

If what you're looking for is to be able to select the text when you click (focus) the text field, you can override the class to simplify your task and you won't have to worry about locating the clicked field from the delegate.
For an NSView object, when it gets focus (ie. clicking or tabbing) it will call becomeFirstResponder so we can hook in there.
When an NSTextField becomes editable (or selectable) it grabs a reusable 'field editor' and overlays it on top of your text field during the editing. If your NSTextField has focus, you can grab this field editor using the currentEditor() call on the view.
So, once you have the field editor, you can perform selectAll on the editor to select the text.
Example class :-
class AutoselectOnFocusTextField: NSTextField {
override func becomeFirstResponder() -> Bool {
guard super.becomeFirstResponder() else {
return false
}
if let editor = self.currentEditor() {
editor.perform(#selector(selectAll(_:)), with: self, afterDelay: 0)
}
return true
}
}
Hope this helps!

I updated the ASTextField as below.
class ASTextField: NSTextField {
// ...
override func mouseDown(with event: NSEvent) {
self.sendAction(#selector(didClick(_:)), to: self)
super.mouseDown(with: event)
}
#objc func didClick(_ event: NSEvent) {
self.log.debug("did click")
}
}
In the SettingsView, I missed calling super.layout(), without which the click won't work, nor the other text field will get focus when clicked.
class SettingsView: NSView {
// ...
override func layout() {
self.log.debug("layout method")
super.layout() // This is important
}
}
NSTextField delegate methods are not required.

The method you overwrote, mouseDown(with), isn't a member of the NSTextFieldDelegate or NSTextDelegate protocols. You overwrote NSViewController.mouseDown(with).
Whenever that method is called, the thing that was clicked is your SettingsViewController's view.
To react to your textfield being selected, you use NSTextFieldDelegate .textField(_:textView:shouldSelectCandidateAt:), which you already have. The value of the textView parameter is the text view that was selected.

Related

Capturing all selection change events for an NSTextView, including those caused by mouse drags

I have a NSViewRepresentable that contains an NSScrollView with a NSTextView inside.
struct MultilineTextField: NSViewRepresentable {
typealias NSViewType = NSScrollView
private let scrollView = NSScrollView()
private let textView = NSTextView()
#Binding var text: String
#Binding var loc: NSRange
func makeNSView(context: Context) -> NSScrollView {
textView.string = text
textView.delegate = context.coordinator
scrollView.documentView = textView
return scrollView
}
func updateNSView(_ scrollView: NSScrollView, context: Context) {}
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
class Coordinator: NSObject, NSTextViewDelegate {
let textField: MultilineTextField
init(_ textField: MultilineTextField) {
self.textField = textField
}
func textDidChange(_ notification: Notification) {
textField.text = textField.textView.string
}
func textViewDidChangeSelection(_ notification: Notification) {
DispatchQueue.main.async {
self.textField.loc = self.textField.textView.selectedRange()
}
}
}
}
I'm trying to extract the selectedRange of the NSTextView to be displayed in a different view, eg:
Text(String(loc.location))
It works when the selectedRange is changed with arrow keys and on mouse down and mouse up, but it seems like textViewDidChangeSelection doesn't get called when in the middle of dragging the mouse over the text. As a result, the displayed loc.location value doesn't change while dragging until the mouse is lifted up.
As a workaround, I've tried subclassing NSTextView to override the mouseMoved method, but it seems like that doesn't get called either for some reason.
Just to verify that selectedRange actually gets updated on mouse drag, I tried continually updating loc (this is in the Coordinator class):
init(_ textField: MultilineTextField) {
self.textField = textField
super.init()
updateSelection()
}
func updateSelection() {
DispatchQueue.main.async {
self.textField.loc = self.textField.textView.selectedRange()
self.updateSelection()
}
}
This code does work, but also uses 100% CPU for no reason.
Is there a way to be notified when selectedRange changes in the case of mouse dragging?

Subclass of NSDatePicker mouseDown event changes datePicker instance

I have instantiated and initialized a subclass of NSDatePicker in AppDelegate -> applicationDidFinishLaunching and overrode mouseDown(with event: NSEvent)in the datePicker subclass. When I press the mouse button in the datePicker and break in its overridden mouseDown func the instance is not the one I instantiated in applicationDidFinishLaunching and so is not initialized.
I've tried creating the instance at different entry points thinking it might be a timing thing but I've gotten nowhere. I'm out of ideas and feeling a little feeble. Any Help?
The datePicker:
import Cocoa
class AlarmIVDatePicker: NSDatePicker {
var viewController: ViewController!
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
}
override func acceptsFirstMouse(for event: NSEvent?) -> Bool {
return true
}
override func mouseDown(with event: NSEvent) {
let stop = 0
}
}
The ViewController:
class ViewController: NSViewController, NSWindowDelegate{
var alarmIVDatePicker: AlarmIVDatePicker!
override func viewDidLoad() {
super.viewDidLoad()
alarmIVDatePicker = AlarmIVDatePicker()
alarmIVDatePicker.viewController = self
}
I expected I could access the values I had set but the instance is not the one I created and all the values are nil
Okay. Here's what you can do to track this down. Add this code to your AlarmIVDatePicker class:
override init(frame frameRect: NSRect) {
Swift.print("AlarmIVDatePicker being called here")
super.init(frame: frameRect)
}
convenience init() {
self.init(frame: .zero)
}
required init?(coder: NSCoder) {
Swift.print("hey, you DID drop this into a storyboard or XIB file!")
super.init(coder: coder)
}
If you set breakpoints inside these init methods, you'll catch when and where they are being called where you did not expect.

How to set NSButton.isEnabled from subclassed NSTextField

I'm very new to Swift MacOS programming and have been learning by writing small test applications.
The aim of this application is to enable the pushbutton when the 2nd textfield has the focus, and disable it when it is not focused.
I have found that by subclassing the NSTextField I can override becomeFirstResponder() however don't know how to set the button to be disabled from the subclass.
ViewController:
class ViewController: NSViewController {
#IBOutlet public weak var pushButton: NSButton!
#IBOutlet weak var textField3: NSTextField!
#IBOutlet weak var textField2: GSTextField!
#IBOutlet weak var textField1: NSTextField!
override func viewDidLoad() {
super.viewDidLoad()
textField2.delegate = self
// Do any additional setup after loading the view.
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
func chgButton(onoff: Bool){
pushButton.isEnabled = onoff
}
}
// When the field completes editing make the pushbutton disabled.
extension ViewController: NSTextFieldDelegate {
override func controlTextDidEndEditing(_ obj: Notification) {
print("did end")
chgButton(onoff: false)
}
}
GSTextField.Swift
class GSTextField: NSTextField {
override func becomeFirstResponder() -> Bool {
print("GSTextField Firstresponder")
////*** I need to set the button to be enabled here
return super.becomeFirstResponder()
}
}
Your NSTextField subclass needs to be able to communicate with the pushButton. The easiest way to do this is to pass a reference to the pushButton to your text field and then update the push button from there.
Update your ViewController like this:
override func viewDidLoad() {
super.viewDidLoad()
textField2.delegate = self
textField2.pushButton = pushButton
// Do any additional setup after loading the view.
}
And your GSTextField like this:
class GSTextField: NSTextField {
weak var pushButton: NSButton?
override func becomeFirstResponder() -> Bool {
print("GSTextField Firstresponder")
pushButton?.isEnabled = true
return super.becomeFirstResponder()
}
override func resignFirstResponder() -> Bool {
pushButton?.isEnabled = false
return super.resignFirstResponder()
}
}
It should be noted that while this works fine in this toy example, this is a sub-optimal solution to this problem because it tightly couples the pushButton and the GSTextField. A better solution would be to use delegation to communicate the focus changes to the ViewController, and let the ViewController handle the updates.
Your GSTextField would look like this:
protocol FocusObservable: class {
func didGainFocus(sender: Any)
func didLoseFocus(sender: Any)
}
class GSTextField: NSTextField {
weak var focusDelegate: FocusObservable?
override func becomeFirstResponder() -> Bool {
print("GSTextField Firstresponder")
focusDelegate?.didGainFocus(sender: self)
return super.becomeFirstResponder()
}
override func resignFirstResponder() -> Bool {
focusDelegate?.didLoseFocus(sender: self)
return super.resignFirstResponder()
}
}
And then you would add protocol conformance to the ViewController:
extension ViewController: FocusObservable {
func didGainFocus(sender: Any) {
pushButton.isEnabled = true
}
func didLoseFocus(sender: Any) {
pushButton.isEnabled = false
}
}
and set the focusDelegate of the text field:
override func viewDidLoad() {
super.viewDidLoad()
textField2.delegate = self
textField2.focusDelegate = self
// Do any additional setup after loading the view.
}

How to catch some events on a NSControl in swift

I am writing an application for OSX in Swift and I am looking for a good way to catch events on a NSControl.
Obviously, I searched but the informations I found are often unclear or old.
In my case, I would like to catch several events on a NSTextField (key up, text changed, focus lost,...).
When I push on “Enter” in the NSTextField, it sends an action. Maybe is there a way to send an action when I click or write in the NSTextField?
You can subclass NSTextField and override textDidChange for text change, textDidEndEditing for lost focus and keyUp method for key up. Try like this:
import Cocoa
class CustomTextField: NSTextField {
override func viewWillMove(toSuperview newSuperview: NSView?) {
// customize your field here
frame = newSuperview?.frame.insetBy(dx: 50, dy: 50) ?? frame
}
override func textDidChange(_ notification: Notification) {
Swift.print("textDidChange")
}
override func textDidEndEditing(_ notification: Notification) {
Swift.print("textDidEndEditing")
}
override func keyUp(with event: NSEvent) {
Swift.print("keyUp")
}
}
View Controller sample Usage:
import Cocoa
class ViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
let textField = CustomTextField()
view.addSubview(textField)
}
}
Sample

onClick Action with Label does not work [duplicate]

I would like to make a UILabel clickable.
I have tried this, but it doesn't work:
class DetailViewController: UIViewController {
#IBOutlet weak var tripDetails: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
...
let tap = UITapGestureRecognizer(target: self, action: Selector("tapFunction:"))
tripDetails.addGestureRecognizer(tap)
}
func tapFunction(sender:UITapGestureRecognizer) {
print("tap working")
}
}
Have you tried to set isUserInteractionEnabled to true on the tripDetails label? This should work.
Swift 3 Update
Replace
Selector("tapFunction:")
with
#selector(DetailViewController.tapFunction)
Example:
class DetailViewController: UIViewController {
#IBOutlet weak var tripDetails: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
...
let tap = UITapGestureRecognizer(target: self, action: #selector(DetailViewController.tapFunction))
tripDetails.isUserInteractionEnabled = true
tripDetails.addGestureRecognizer(tap)
}
#objc
func tapFunction(sender:UITapGestureRecognizer) {
print("tap working")
}
}
SWIFT 4 Update
#IBOutlet weak var tripDetails: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
let tap = UITapGestureRecognizer(target: self, action: #selector(GameViewController.tapFunction))
tripDetails.isUserInteractionEnabled = true
tripDetails.addGestureRecognizer(tap)
}
#objc func tapFunction(sender:UITapGestureRecognizer) {
print("tap working")
}
Swift 5
Similar to #liorco, but need to replace #objc with #IBAction.
class DetailViewController: UIViewController {
#IBOutlet weak var tripDetails: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
...
let tap = UITapGestureRecognizer(target: self, action: #selector(DetailViewController.tapFunction))
tripDetails.isUserInteractionEnabled = true
tripDetails.addGestureRecognizer(tap)
}
#IBAction func tapFunction(sender: UITapGestureRecognizer) {
print("tap working")
}
}
This is working on Xcode 10.2.
Swift 3 Update
yourLabel.isUserInteractionEnabled = true
Good and convenient solution:
In your ViewController:
#IBOutlet weak var label: LabelButton!
override func viewDidLoad() {
super.viewDidLoad()
self.label.onClick = {
// TODO
}
}
You can place this in your ViewController or in another .swift file(e.g. CustomView.swift):
#IBDesignable class LabelButton: UILabel {
var onClick: () -> Void = {}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
onClick()
}
}
In Storyboard select Label and on right pane in "Identity Inspector" in field class select LabelButton.
Don't forget to enable in Label Attribute Inspector "User Interaction Enabled"
You need to enable the user interaction of that label.....
For e.g
yourLabel.userInteractionEnabled = true
For swift 3.0 You can also change gesture long press time duration
label.isUserInteractionEnabled = true
let longPress:UILongPressGestureRecognizer = UILongPressGestureRecognizer.init(target: self, action: #selector(userDragged(gesture:)))
longPress.minimumPressDuration = 0.2
label.addGestureRecognizer(longPress)
Pretty easy to overlook like I did, but don't forget to use UITapGestureRecognizer rather than UIGestureRecognizer.
Thanks researcher
Here's my solution for programmatic user interface using UIKit.
I've tried it only on Swift 5. And It worked.
Fun fact is you don't have to set isUserInteractionEnabled = true explicitly.
import UIKit
open class LabelButon: UILabel {
var onClick: () -> Void = {}
public override init(frame: CGRect) {
super.init(frame: frame)
isUserInteractionEnabled = true
}
public required init?(coder: NSCoder) {
super.init(coder: coder)
}
public convenience init() {
self.init(frame: .zero)
}
open override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
onClick()
}
}
Uses:
override func viewDidLoad() {
super.viewDidLoad()
let label = LabelButton()
label.text = "Label"
label.onClick = {
// TODO
}
}
Don't forget to set constraints. Otherwise it won't appear on view.
On top of all of the other answers, this depends on where the label is, it might be behind some subviews. You might think you tap on the label but maybe click the top view. To solve this you can bring the label view to the front with the following line.
self.view.bringSubviewToFront(lblView)
As described in the above solution
you should enable the user interaction first and add the tap gesture
this code has been tested using
Swift4 - Xcode 9.2
yourlabel.isUserInteractionEnabled = true
yourlabel.addGestureRecognizer(UITapGestureRecognizer(){
//TODO
})