NSScrollView: overriding scrollWheel method breaks trackpad panning - macos

I have a subclass of NSScrollView, like so:
class ImageScrollView : NSScrollView {
override func scrollWheel(with event: NSEvent) {
super.scrollWheel(with:event)
}
}
As you can see, the only thing this does is override the scrollWheel: method and call super.
The issue I'm having is that as soon as I do this, using 2 fingers on the trackpad to try to pan around the scroll view when zoomed in stops working. Removing the overridden method fixes it. Any tips?

Related

MKMapView moveRight/Left on keyDown:

I have mkmapview with move up, move down, zoom which works out of the box on key press. However move left/move right doesn't.
It works in default Maps.app and Maps doesn't use any subclassing.
Tried it with map that display compass without success.
I have subclassed MKMapView and I am not getting right/left keypress
- (void)keyDown:(NSEvent *)event
{
[super keyDown:event];
}
What do I miss?
Tested on macOS 10.12.
In the subclass of MKMapView you need to overwrite acceptsFirstResponder
override var acceptsFirstResponder: Bool {
return true
}
Then all will work. I just made a test.

How to close window (NSWindowController) by hitting the ESC key?

Issue
I would like the user being able to close a window by hitting the ESC key but I can't get it to work in this specific case, hitting ESC triggers an error sound (the "no you can't do that" macOS bloop) and nothing happens.
Context
I'm making a subclass of NSWindowController which itself creates an instance of a subclass of NSViewController and sets it in a view. Both controllers have their own xib file.
NSWindowController:
final class MyWindowController: NSWindowController, NSWindowDelegate {
#IBOutlet weak var targetView: MainView!
let myVC: MyViewController!
var params: SomeParams!
override func windowDidLoad() {
super.windowDidLoad()
myVC = MyViewController(someParams: params)
myVC.view.setFrameSize(targetView.frame.size)
myVC.view.setBoundsSize(targetView.bounds.size)
targetView.addSubview(myVC.view)
}
override var windowNibName: String! {
return "MyWindowController"
}
convenience init(someParams params: SomeType) {
self.init(window: nil)
self.params = params
}
}
NSViewController:
final class MyViewController: NSViewController {
convenience init(someParams params: SomeType) {
// do stuff with the params
}
override func viewDidLoad() {
super.viewDidLoad()
// configure stuff for the window
}
}
What I've tried
I suppose that my issue is that the MyWindowController NSWindow is the .initialFirstResponder when I would want the content of the targetView (an NSTableView) to be the first responder - this way I could use keyDown, I guess, and send the close command to the window from there. This doesn't seem optimal, though.
I've tried forcing the view controller views into being the first responder by using window?.makeFirstResponder(theView) in the windowDidLoad of MyWindowController but nothing ever changes.
I've also tried adding this to MyWindowController:
override func cancelOperation(_ sender: Any?) {
print("yeah, let's close!")
}
But this only works if the user clicks first on the background of the window then hits ESC, and it still emits the error sound anyway. Which is actually what made me think that the issue was about the first responder being on the window.
Question
How would you achieve that? Of course, I know that the user can already close the window with CMD+W, but I'd really like to sort out this issue nonetheless.
Note that the code example is in Swift but I can also accept explanations using Objective-C.
The documentation of cancelOperation explains how cancelOperation should work:
This method is bound to the Escape and Command-. (period) keys. The key window first searches the view hierarchy for a view whose key equivalent is Escape or Command-., whichever was entered. If none of these views handles the key equivalent, the window sends a default action message of cancelOperation: to the first responder and from there the message travels up the responder chain.
If no responder in the responder chain implements cancelOperation:, the key window searches the view hierarchy for a view whose key equivalent is Escape (note that this may be redundant if the original key equivalent was Escape). If no such responder is found, then a cancel: action message is sent to the first responder in the responder chain that implements it.
NSResponder declares but does not implement this method.
NSWindow implements cancelOperation: and the next responder, the window controller, isn't checked for an implementation of cancelOperation:. The cancel: message does arrive at the window controller. Implementing
- (void)cancel:(id)sender
{
NSLog(#"cancel");
}
will work. The cancel: message isn't inherited from a superclass so autocompletion doesn't suggest it.
This worked for me in Xcode 10 and Swift 4.2:
#objc func cancel(_ sender: Any?) {
close()
}
I tried it before but without the #objc part and it didn't work. So don't omit it.
When I needed such behavior I implemented it by overriding keyDown: of the NSWindow object.
I.e. something like the following:
- (void)keyDown:(NSEvent *)theEvent
{
int k = [theEvent keyCode];
if (k == kVK_Escape)
{
[self close];
return;
}
[super keyDown:theEvent];
}

Where do mouseDown events go when the control key is down?

I have subclassed NSComboBox for a number of reasons, including a strategy for displaying contextual menus without the OS adding arcane things to them. (“Add to iTunes as a spoken track”???) Here are my mouse event methods:
public override func mouseDown (event: NSEvent)
{ NSLog("mouseDown")
if NSEvent.modifierFlags().contains(.ControlKeyMask)
{ self.rightMouseDown(event) }
else
{ super.mouseDown(event) }
}
public override func rightMouseDown (event: NSEvent)
{ NSLog("rightMouseDown")
super.menu?.delegate = self
super.menu?.allowsContextMenuPlugIns = false
super.menu?.popUpMenuPositioningItem(nil, atLocation:
self.convertPoint(event.locationInWindow, fromView: nil), inView: self)
}
The rightMouseDown method does the last-second menu configuration I want. And I think the (left) mouseDown method would also work (it’s there only because ctrl-left-click is a traditional alternate to right-click), except that with the control key down it never sees the mouse event. The event seems to get to the superclass by going around rather than through my subclass, because NSComboBox does display a menu, just not the one I want (and the menu delegate isn’t right, etc).
I suspect there is some kind of legacy propagation path for ctrl-left-clicks, from the era when Apple mice had only one button. If I knew where these events were directed (I don’t think they’re going to my NSPanel), I might be able to intercept them. Does anyone know where they go? Is there something in NSEvent documentation I’m staring at and not seeing?

Scrolling in NSScrollView stops when overwriting scrollWheel function

I experience a weird bug when I overwrite the scrollWheel(theEvent: NSEvent) function in a NSScrollView.
Here's my code:
import Cocoa
class GraphScrollView: NSScrollView {
var axisScrollViewInstance: AxisScrollView?
override func scrollWheel(theEvent: NSEvent) {
if theEvent.deltaY != 0 {
axisScrollViewInstance?.scrollEventFromOtherScrollView(theEvent)
super.scrollWheel(theEvent)
} else if theEvent.deltaX != 0 {
super.scrollWheel(theEvent)
}
}
}
class AxisScrollView: NSScrollView {
var graphScrollViewInstance: GraphScrollView?
override func scrollWheel(theEvent: NSEvent) {
if theEvent.deltaY != 0 {
super.scrollWheel(theEvent)
graphScrollViewInstance?.scrollWheel(theEvent)
}
}
func scrollEventFromOtherScrollView(theEvent: NSEvent) {
if theEvent.deltaY != 0 {
super.scrollWheel(theEvent)
}
}
}
The code basically checks in which direction the user is scrolling and forwards the NSEvent to another NSScrollView if the direction is vertically. I need this so that I can have two NSScrollViews next to each other, which both scroll vertically and one of them scrolls horizontally as well. As an example you can look at the Mac Calendar app, which has an hour column on the left that only scrolls vertically and a week overview on the right that scrolls horizontally and vertically.
However, if I overwrite the scrollWheel-method and call the super function from inside, it leads to a weird problem with the Mighty Mouse. I can only scroll upwards and to the right, whereas it does nothing if I scroll in any other direction, although a NSEvent is always occurring, disregarding in which direction I scroll. After a while it stops scrolling altogether.
I have already tried to simply overwrite the scrollWheel-method and only call the super function with the event like this:
override func scrollWheel(theEvent: NSEvent) {
super.scrollWheel(theEvent)
}
but it leads to the same problems. I have a second (non-Apple) mouse connected to my system, which has a 'normal' scroll wheel and with which my code works perfectly fine.
Do you have an idea what the problem could be? I heard that the NSEvents are of different subtype (1: Apple Device, 0: Any other Device). Maybe that has something to do with it?
Otherwise, do you know how I can have two NSScrollViews next to each other that scroll vertically simultaneously?
Okay, so it's a bug with the Interface Builder in Xcode 6.4 and maybe in earlier versions as well. You have to make sure that the box "Show Vertical Scroller" and "Show Horizontal Scroller" in the Attribute Inspector of the NSScrollView are checked.
If you check both boxes you won't have problems with overwriting the scrollWheel(theEvent: NSEvent) function. You can then remove the scroll bars programmatically again without problems by setting the hasHorizontalScroller and hasVerticalScroller to false in e.g. your init()
Update at 12.10.2015
I've reported the 'bug' to Apple, however it seems that by overwriting the -scrollWheel function, the view loses its responsive scrolling ability.
This is the answer of the Apple Dev Team:
If you override -scrollWheel, then that scrollView cannot perform responsiveScrolling.
If responsive scrolling is not on, then the scroller must be shown in order for scrollView to scroll.
See Documentation for +isCompatibleWithResponsiveScrolling
In the documentation is says:
The default implementation of this method returns true unless the class overrides the lockFocus or scrollWheel: method
I had the same issue and needed horizontal scrolling override, what seems to work is this:
override func scrollWheel(with event: NSEvent) {
self.hasHorizontalScroller = true
self.hasVerticalScroller = true
// do your magic here
self.hasHorizontalScroller = false
self.hasVerticalScroller = false
}

How can I disable the animations in an NSCollectionView

I would like to turn off the 'shuffle' animations that happen when you resize an NSCollectionView. Is this possible?
This works, but it's setting a private instance variable so it may no be ok in the Mac App Store.
[collectionView setValue:#(0) forKey:#"_animationDuration"];
kainjow is correct. adding this:
- (id) animationForKey:(NSString *) key
{
return nil;
}
to the prototype view subclass (not the collection view!) disables animations
For 10.6, I was able to disable the animation by subclassing NSView, overriding animationForKey: and returning nil. Then make sure you use that view for the prototype's view.
To disable all the collection view's animations in Swift, do this just before the something animatable happens:
NSAnimationContext.current.duration = 0
I was only able to get this to work if I did the following:
1) Subclass the view that the NSCollectionViewItem used as its view. That subclassed view required a CALayer and I set the view subclass as the delegate of the CALayer.
2) Implement the CALayer delegate method so no animation actions should occur:
override func actionForLayer(layer: CALayer, forKey event: String) -> CAAction? {
return NSNull()
}
3) Finally, in the NSCollectionView data source method:
func collectionView(collectionView: NSCollectionView, itemForRepresentedObjectAtIndexPath indexPath: NSIndexPath) -> NSCollectionViewItem {
// get a new collection view item
....
// disable animations
CATransaction.begin()
CATransaction.setDisableActions(true)
// populate your cell
....
CATransaction.commit()
}

Resources