Mouse moved events during button mouse up event - swift

I want to receive information about mouse move events during button click (mouse up)
I'm adding NSTrackingArea to view that I want to track mouse move and mouse dragged events on, but I still don't receive these events.
I assume that mouseDown in NSButton is blocking mouse events, so the only solution I come up with is overriding mouseDown function for NSButton and not calling super.mouseDown, but then I need to handle button selection manually and I'm not sure if this is right approach for this.
Is this a right solution for my problem? Will there be no problems? Is there a better solution?
Here is code for test, just add button to new project and assign TestButton class to it.
class TestButton: NSButton {
override func mouseDown(with event: NSEvent) {
print("Mouse up")
super.mouseDown(with: event) // After removing events works.
print("Mouse down")
}
}
class ViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
let trackingArea = NSTrackingArea(rect: view.bounds, options: [.activeAlways, .mouseMoved, .enabledDuringMouseDrag], owner: self, userInfo: nil)
view.addTrackingArea(trackingArea)
}
override func mouseMoved(with event: NSEvent) {
print("Mouse moved")
super.mouseMoved(with: event)
}
override func mouseDragged(with event: NSEvent) {
print("Mouse dragged")
super.mouseDragged(with: event)
}
}

I had a similar requirement for my project. So initially, I started out with NSButton just like you. But, it turned out to be more hectic than I thought. Handling the action and selected state where the main concerns I faced. You could proceed with NSButton if it actually meets up with your requirement. But, I eventually moved to NSView and customised it. So, few of my implementation for handling move and drag event implementation I've open-sourced here is the link.

The traditional way to do this is to use a custom subclass of NSButtonCell and override continueTracking(last:current:in:) (inherited from NSCell). Your override should generally call through to super and return what it returns, but you can do something else in addition, to respond to the mouse movements. The issue is that NSCell and its subclasses have been "soft deprecated" for a while.
That said, it would be very surprising to me as a user that anything other than the button would react to the mouse movements while I'm interacting with the button (clicked in it and dragging).

Related

Why keyDown method does not work in Cocoa macOS?

I want use keyDown method to see which key pressed in keyboard but it does not work also my computer makes sounds to tell the key even does not work.
import Cocoa
class ViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
override func keyDown(with event: NSEvent) {
print(event)
}
}
Only the responders in the current responder chain get a shot at handling events like key events, and then only if some other responder in the chain doesn't handle it first. So, you'll need to make sure that your view controller is managing a view that's the keyboard focus when the event actually takes place, and that the focused view doesn't handle the event itself.

NSComboBox disappears immediately after click

I'm working on an application which behaves similarly to Spotlight. However, currently I'm having a trouble with NSComboBox.
To show the application after hitting a hotkey I use activate(ignoringOtherApps: true) and NSWindowController(window: window).showWindow(self). Then when user hits Escape I do window.close() and hide(self).
Everything works great, previous application gets focus. However, when I open again the window, first click on NSComboBox causes a very strange behavior (like on the movie below). First time it instantly disappears.
I found out that it happens because of NSApp.hide. When I don't call it, everything works well. However, I need to call it, because I want the previous app to get the focus.
To workaround this issue I can replace NSWindow with nonactivating NSPanel. It resolves the problem. However, it's not possible in my case because I need to use it also with presentAsModalWindow and presentAsSheet where I can't control whether it's a window or panel.
I also discovered that a single click on window's background before clicking on ComboBox helps. So it seems like this window doesn't have focus, but looks like focused. I also tried all methods like makeKeyAndOrderFront, becomeFirstResponder, NSApp.unhide etc. etc. Nothing helps.
Under the hood NSComboBox has its own window NSComboBoxWindow, so my guess is that when I click it opens its window and then it receives information that the parent window took focus and dismisses itself for some reason.
I'm not sure if this is Cocoa bug or what. Is there any way to fix it?
Minimum Reproducible Example
Create new macOS project with NSComboBox and NSButton. Connect button to IBAction.
import Cocoa
class ViewController: NSViewController {
#IBAction func close(_ sender: Any) {
view.window?.close()
NSApp.hide(self)
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) {
self.view.window?.makeKeyAndOrderFront(self)
NSApp.activate(ignoringOtherApps: true)
}
}
}
Workaround
Finally, I managed to create a workaround. It's ugly but it works. I cover arrow part with a transparent view, intercept click and invoke two times expand via accessibility...
import Cocoa
final class ClickView: NSView {
var onMouseDown: () -> (Bool) = { return false }
override func mouseDown(with event: NSEvent) {
if !onMouseDown() {
super.mouseDown(with: event)
}
}
}
final class FixedComboBox: NSComboBox {
private let clickView = ClickView()
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
fix()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
override func awakeFromNib() {
super.awakeFromNib()
fix()
}
private func fix() {
clickView.onMouseDown = { [weak self] in
guard let cell = self?.cell else { return false }
// first expand will be immediately closed because of Cocoa bug
cell.setAccessibilityExpanded(true)
// we need to schedule another one with a small delay to let it close the first call
// this one almost immediately to avoid blinking
DispatchQueue.main.async { cell.setAccessibilityExpanded(true) }
// in case the first one didn't "catch" the right moment (sometimes it happens)
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) { cell.setAccessibilityExpanded(true) }
return true
}
addSubview(clickView)
clickView.snp.makeConstraints { make in
make.width.equalTo(20)
make.trailing.top.bottom.equalToSuperview()
}
}
}

How to disable mouse clicks and mouse drags on a NSView?

I am making a mac app using Swift and this app has a custom view (a class extending NSView and overriding its draw method). Now, I want to disable all mouse clicks and mouse drags on this view and pass them on to the other applications running beneath my application.
I have tried the following ways (gleaned from Apple documentation and other SO questions) to disable clicks on my view and nothing worked for me so far:
1. Overriding hitTest inside my custom View class
override func hitTest(_ point: NSPoint) -> NSView? {
let view = super.hitTest(point)
return view == self ? nil : view
}
2. Overriding acceptsFirstMouse inside my custom View class
override func acceptsFirstMouse(for event: NSEvent?) -> Bool {
return false
}
3. Overriding mouseDown in ViewController as well as in my custom View class
override func mouseDown(with event: NSEvent) {
// do nothing
}
4. Overriding mouseDragged in ViewController as well as in my custom View class
override func mouseDragged(with event: NSEvent) {
// do nothing
}
Am I missing something?
This isn't handled at the view level, it's handled at the window level. You can set the ignoresMouseEvents property of the window to true.
The issue is that the Window Server will only dispatch an event to a single process. So, once it has arrived in your app, it's not going to another. And there's no feasible way for your app to forward it along, either.

Swift: How to determine which control was clicked on mouseDown

I am trying to catch mouse down events on some of my controls in the (Cocoa with Storyboards) application window.
If I override mouseDown(with event: NSEvent) in my ViewController class, I face two issues:
It is not possible to identify (directly) which exactly control in the window has been clicked. To do so, I use the following:
Swift 4:
override func mouseDown(with event: NSEvent)
{
let point: NSPoint = event.locationInWindow
let view: NSView = self.view.hitTest(point)!
if type(of:view) == NSTextField.self // with tags is also fine
{
// do something with the control (in the example NSTextField)
// (view as! NSTextField).backgroundColor = NSColor.systemPink
}
}
I am a little bit puzzled, why for such a basic GUI operation, there isn't a "native" event handler, as provided for the mouse click (by creating an #IBAction).
Am I missing something, or this is the way to catch and handle the mouse down events?
For some controls, e.g. NSLevelIndicator, my overridden method is not called. Why?
You must override NSView mouseDown, not NSViewController mouseDown.

NSTextField Focus

How to remove focus on NSTextField if I'm not focus in the textField?
I have a NSTextField and I set the Action: Sent On End Editing. After clicking the textField, the FOCUS will always in my NSTextField when I click other places on the view.
Furthermore, I use makeFirstResponder and resignFirstResponder but all in vain. Any idea?
#IBAction func endOfEditing(sender: NSTextField) {
view.window?.makeFirstResponder(nil)
view.window?.resignFirstResponder()
}
For illustration, the FOCUS will always be in textField no matter where I pressed. (Even when I pressed the button)
How can I remove the focus when I click outside the textField?
Just try this: change "view.window?" to "sender"
#IBAction func endOfEditing(sender: NSTextField) {
sender.makeFirstResponder(nil)
sender.resignFirstResponder()
}
Swift 4
For those still troubling with focus and unfocus: I don't know if it was the case back in 2016, but in Swift 4 my NSTextField loses focus except if I click in another "selectable" outlet.
For instance if I click on a NSButton right after NSTextField it won't lost focus. I suspect the NSButton catches the event, preventing the textfield to know it has to unfocus.
Here is a much radical solution which will definitely defocus an NSTextField
#IBAction func clickMyButton(_ sender: NSButton) { // or anywhere else
self.quantityTextField.isEnabled = false
self.quantityTextField.isEnabled = true
}
Not very sexy but it is reliable
Edit: Just saw it, it solves #Lee's issue
try add this function to your viewController
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
// I assume your textField call myText
myText. resignFirstResponder()
}