Swift 5 - Mac OS - NSTrackingArea overlapping views - swift

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
}

Related

(Swift)How to dectect a "fieldEditor" resignFirstResponder() (Mac/Cocoa)

What I intend to do is validate four NSTextFields, I will try to make sure that all of them are filled correctly, and completely.
textField and alert
As the picture shows: if a user types anything in the 1st NSTextField, types something wrong or leaves it blank in the 2nd NSTextField, press Enter or click outside the boundary, it should show an alert, and try to stop the user from resigning from the 2nd NSTextField, and he should confirm his input in 2nd NSTextField.
What I have tried and the problems I faced:
Use textFieldShouldEndEditing(_:)
When I type something. Wrong, show an alert and return false. It runs well.
But if I type nothing, textFieldShouldEndEditing(_:) will not invoke. (Because it won't be triggered without typing)
func control(_ control: NSControl, textShouldEndEditing fieldEditor: NSText) -> Bool {
if control.stringValue != validInput{
showAlert()
return false
}else{
return true
}
}// plz ignore the delegation
CustomNSTextField & resignFirstResponder()
When the user clicked NSTextField, NSTextField would becomeFirstResponder(), followed by ResignFirstResponder() because the FieldEditor would enter. So it is invalid to monitor resignFirstResponder().
Use textFieldShouldEndEditing(_:)
When I type something wrong or type nothing, press Enter or click outside the boundary, show an alert, but it is difficult to stop the following functions: for example, if I click the button "open", it may show an alert and an openFile dialog in the meantime.
What may solve my problems:
Whether there is a trick that invokes textFieldDidBeginEditing (_:) when NSTextField resignFirstResponder() ?
Whether there is a function that monitors the window's current fieldEditor resignFirstResponder(), and it can return false while I want the user to confirm his input.
I found it NOT as difficult as I thought to write a custom fieldEditor. Follow the tutorial here. I post the essential part of my code here for anyone who is facing similar problems.
class CustomFieldCell: NSTextFieldCell {
static var durationFieldEditor: CustomFieldEditor = {
let customFieldEditor = CustomFieldEditor()
return fieldEditor
}()
override func fieldEditor(for controlView: NSView) -> NSTextView? {
return Self.customFieldEditor
}
}
class CustomFieldEditor: NSTextView {
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
commonInit()
}
override init(frame frameRect: NSRect, textContainer container: NSTextContainer?) {
super.init(frame: frameRect, textContainer: container)
commonInit()
}
override func becomeFirstResponder() -> Bool {
// condition
return super.becomeFirstResponder()
}
override func resignFirstResponder() -> Bool {
// condition
return super.resignFirstResponder()
}
private func commonInit() {
isFieldEditor = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

KVO on SKView is not being called

I am trying to do KVO on an property of SKView but its not working.
I have tried it with .frame and it works like a charm, so why not also on .window ?
To clarify, I am using SpriteView in a SwiftUI app and I am trying to get the bounds of the View.
What I am after is to get the following before the App starts;
print (self.convertPoint(fromView: .zero) ). When I use this in
override func didMove
I'll get Nan/NaN. However Apple Code Level Support said this about using SpriteView and getting the bounds of a view.
The reason you are receiving NaN is that you are calling these methods
before the underlying SKView has been actually presented by SwiftUI,
which is an event that you have no visibility into, and no way to call
code when it happens.
However, when this event does occur, the SKScene’s view property will
have it’s window set from nil to a UIWindow. Therefore, you could use
KVO to observe when the window property is changed, and then make your
calls to convertPoint once there is a non-nil window.
I have so far this:
override func didMove(to view: SKView) {
observe.observe(object: view )
}
class Observer:SKScene {
var kvoToken: NSKeyValueObservation?
func observe(object: SKView) {
kvoToken = object.observe(\.window , options: [ .new] ) { (object, change) in
guard let value = change.newValue else { return }
print("New value is: \(value)")
print ("NEW", self.convertPoint(fromView: .zero) )
}
}
deinit {
kvoToken?.invalidate()
}
}
I have also tried to add an observer like so :
NotificationCenter.default.addObserver(view.window, selector: #selector(test(_:)), name: NSNotification.Name(rawValue: "TestNotification"), object: nil)
The above doesn't seem to do anything. So I am kinda stuck, any help would be appreciated.
The answer (probably) ..
I couldn't read the '.window' property of UIView, so I started to look for another observable property that would change as soon as UIWindow is != nil. I think I have found it in SKScene.view.frame . I am not entirely sure that this is a 100% good answer but it works.
class Observer: NSObject {
dynamic var kvoToken: NSKeyValueObservation?
func observe(object: SKScene ) {
kvoToken = object.observe(\.view?.frame , options: [ .new] ) { (object, change) in
guard let value = change.newValue else { return }
print("New value is: \(value)")
print ("CONVERTING", object.convertPoint(fromView: .zero) )
}
}
deinit {
kvoToken?.invalidate()
}
}
override func didMove(to view: SKView) {
viewer = view.scene
observe.observe(object: viewer )
}

How to detect changes in a UIImageView and change a Boolean when this happens

I'm using UIPresentationController to prevent the user from accidentally closing a UIViewController presented modally if the user has made any changes. Everything works as it should when it comes to UITextFields since I detect the changes with .editingChanged. A sample code is shown below. I have an UIImageView where the user can change to provide a profile photo. I can enable the save button (rightBarButtonItem) once the user has uploaded an image using didFinishPickingMediaWithInfo in UIImagePickerController but that would not prevent the UIViewController from closing accidentally. Ideally, I would like to change the value of the hasChanges var but it is a get-only property.
var hasChanges: Bool {
guard let customer = customer else { return false }
if
firstNameTextField.text!.isNotEmpty && firstNameTextField.text != customer.firstName
// additional textfields etc…
{
return true
}
return false
}
override func viewWillLayoutSubviews() {
// If our model has unsaved changes, prevent pull to dismiss and enable the save button
let hasChanges = self.hasChanges
isModalInPresentation = hasChanges
saveButton.isEnabled = hasChanges
}
#objc func cancel(_ sender: Any) {
if hasChanges {
// The user tapped Cancel with unsaved changes
// Confirm that they really mean to cancel
confirmCancel(showingSave: false)
} else {
// No unsaved changes, so dismiss immediately
sendDidCancel()
}
}
#objc func textFieldDidChange(_ textField: UITextField) {
if hasChanges {
navigationItem.rightBarButtonItem?.isEnabled = true
} else {
navigationItem.rightBarButtonItem?.isEnabled = false
}
}
func setupTextFieldDelegates() {
let textFields = [all the textfields are included here]
for textField in textFields {
textField.addTarget(self, action: #selector(textFieldDidChange(_:)), for: .editingChanged)
}
}
To achieve this, you have to create a custom UIImageView, where you have to create a delegate variable and a protocol with a method and need to override the image variable.
when override the image variable you have to call the delegation method inside the didSet method of variable.
class CustomImageView: UIImageView{
override var image: UIImage?{
didSet{
delegate?.didChangeImage()
}
}
var delegate: ImageViewDelegate?
}
protocol ImageViewDelegate {
func didChangeImage()
}
Next in your ViewController set the delegate.
class ImageViewController: UIViewController {
#IBOutlet weak var imageView: CustomimageView!
override func viewDidLoad() {
super.viewDidLoad()
imageView.delegate = self
}
}
If you use the outlet from the storyboard, make sure to provide the custom class name to outlet. Otherwise it will not work.

Link in an Editable NSTextView (like Apple's Notes)

I have an NSTextView that is initially set as read-only like this:
taskDescription.isEditable = false
I set a click gesture recognizer in viewDidLoad() on it like this so that when the user clicks the field, it gets set as editable.
override func viewDidLoad() {
super.viewDidLoad()
let clickGesture = NSClickGestureRecognizer(target: self, action: #selector(setDescriptionEditState))
clickGesture.numberOfClicksRequired = 1
taskDescription.addGestureRecognizer(clickGesture)
}
And here is the function called on click:
#objc func setDescriptionEditState(){
//Make it editable
taskDescription.isEditable = true
//Don't show automatic link detect while editing (I want plain text)
taskDescription.checkTextInDocument(nil)
//Put the cursor in the NSTextView
taskDescription.window?.makeFirstResponder(taskDescription)
}
Then when I load an NSAttributedString into it with a link in it, it shows up fine:
Last of all, I implement this delegate method for NSTextView:
func textView(_ textView: NSTextView, clickedOnLink link: Any, at charIndex: Int) -> Bool {
print(link) //<-- Never called
}
What's happening is the clickGesture is getting called instead of the link click. I know this because if I manually set the NSTextView to isEditable = false then the link works fine.
How can I allow the link click to happen while still allowing the user to click on the NSTextView to switch it into edit mode?
--- Update ---
I set the content of this NSTextView with an NSAttributedString. When logged to the console, it looks like this:
I want {
NSColor = "...";
NSFont = "...";
}a big link{
NSColor = "...";
NSFont = "...";
NSLink = "google.com";
} here.{
NSColor = "...";
NSFont = "...";
}
So the link is specified with an NSLink.
The key is to override the function "touchesBegan" and implement your own logic.
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first
guard let location = touch?.location(in: self.view) else { return }
if !popupContainer.frame.contains(location) {
goodTouch = true
} else {
goodTouch = false
}
}
The variable "goodTouch" is established as a class property. You can then check the status of this variable at the beginning of the functions in question and then route your application appropriately from there. (This was specifically adapted from an iOS project I wrote, but should be the same for Cocoa). Make sure if you implement this, you reset the "goodTouch" variable to false after inspecting it to be ready for the next round of touches.
EDIT
I think this is a more appropriate response to your specific example, but I didn't want to delete the code from above:
override func mouseDown(with event: NSEvent) {
let textField = NSTextField() //this is for demo only...reference your actual object
let location = event.locationInWindow
let cgPoint = CGPoint(x: location.x, y: location.y)
if textField.bounds.contains(cgPoint) {
//fire the link here, over ride the click gesture recognizer
} else {
//change the editable style of the text field
}
}

Using acceptsMouseMovedEvents for SpriteKit mouse actions with Storyboards and Swift

I have created a SpriteKit scene by referencing a custom NSView using Storyboards in Xcode. However, I cannot implement any mouseMoved events using SpriteKit because I do not know how to reference the program's NSWindowto set its acceptsMouseMovedEvents property to "true".
How can I create an #IBOutlet reference to my NSWindow in my AppDelegate.swift file so that I can change this property?
You can configure an NSTrackingArea object to track the movement of the mouse as well as when the cursor enters or exits a view. To create an NSTrackingArea object, you specify a region of a view where you want mouse events to be tracked, the owner that will receive the mouse event messages, and when the tracking will occur (e.g., in the key window). The following is an example of how to add a tracking area to a view. Add to your SKScene subclass, such as GameScene.swift.
Swift 3 and 4
override func didMove(to view: SKView) {
// Create a tracking area object with self as the owner (i.e., the recipient of mouse-tracking messages
let trackingArea = NSTrackingArea(rect: view.frame, options: [.activeInKeyWindow, .mouseMoved], owner: self, userInfo: nil)
// Add the tracking area to the view
view.addTrackingArea(trackingArea)
}
// This method will be called when the mouse moves in the view
override func mouseMoved(with theEvent: NSEvent) {
let location = theEvent.location(in: self)
print(location)
}
Swift 2
override func didMoveToView(view: SKView) {
// Create a tracking area object with self as the owner (i.e., the recipient of mouse-tracking messages
let trackingArea = NSTrackingArea(rect: view.frame, options: NSTrackingAreaOptions.ActiveInKeyWindow | NSTrackingAreaOptions.MouseMoved, owner: self, userInfo: nil)
// Add the tracking area to the view
view.addTrackingArea(trackingArea)
}
// This method will be called when the mouse moves in the view
override func mouseMoved(theEvent: NSEvent) {
let location = theEvent.locationInNode(self)
println(location)
}
An update for 0x141E's answer:
override func didChangeSize(_ oldSize: CGSize) {
guard let newRect = view?.bounds else {return}
let options = NSTrackingArea.Options(rawValue: NSTrackingArea.Options.activeInKeyWindow.rawValue | NSTrackingArea.Options.mouseMoved.rawValue)
let userInfo = ["SKMouseInput": 1]
let trackingArea = NSTrackingArea(rect: newRect, options: options, owner: self, userInfo: userInfo)
if let previousTrackingAreas = view?.trackingAreas {
for area in previousTrackingAreas {
if let theInfo = area.userInfo {
if let _ = theInfo["SKMouseInput"] {
view?.removeTrackingArea(area)
}
}
}
}
view?.addTrackingArea(trackingArea)
}
This SKScene method override will be called shortly after initialization, will allow immunity to window size changes and will clean up the old tracking area. Note that it will still require the mouseMoved override as well.