Nested NSScrollViews that can be scrolled with different gestures? - macos

I've got an app with an NSScrollView nested inside another NSScrollView. I'd like the user to be able to scroll the inner scrollview using two-finger swipe, and to scroll the outer scrollview using three fingers.
I imagine I'll need to somehow configure each scrollview to reject touches with the wrong numbers of fingers, but I'm not sure how to do this.

I figured it out! The trick is to subclass the inner ScrollView and force it to reject gestures that have a certain number of touches, forwarding them to the parent scrollview:
- (void)scrollWheel:(NSEvent *)event {
if (_forwardScrollToParent) {
// [self.enclosingScrollView scrollWheel:event];
} else {
[super scrollWheel:event];
[self recordInteractionWithThisTab];
}
}
- (void)touchesBeganWithEvent:(NSEvent *)event {
[super touchesBeganWithEvent:event];
NSInteger nTouches = [event touchesMatchingPhase:NSTouchPhaseTouching inView:self].count;
if (nTouches == 3) {
_forwardScrollToParent = YES;
} else {
_forwardScrollToParent = NO;
}
}

Related

Translucent subviews of NSTableView flash when adding/inserting rows

I have a view based NSOutlineView in which the rows contains subviews which have an alpha value of less than 1.
When new rows are inserted or deleted (when the user expands or collapses a row) all of these sub views flash as they seem to temporarily be redrawn with an alpha value of 1.
Does anyone know how to stop this phenomenon ?
I've been looking at this problem four hours, and as so often happens as soon as I post the question here I work out the answer for myself.
Setting up the translucent layers like this solve the issue:
- (void)setup {
// This stops the strange flashing effect when collapsing/expanding rows
self.wantsLayer = YES;
[self.layer setShouldRasterize:YES];
}
- (instancetype)init
{
self = [super init];
if (self) {
[self setup];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (self) {
[self setup];
}
return self;
}

NSView subview right click not working over nsimageview

I have to two subviews associated with a view. One is a transparent view that handles a right click, the other a nsview with an nsimageview subview. For some reason the right click works over any part of the superview except the part within the nsimageview. The transparent view is on top of the other view yet the right mouse down event is not firing.
I finally solved it by subclassing the image view and overriding the hit test method to return nil. The full implementation is below :
#implementation TTBaseImageView
- (id)initWithFrame:(NSRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code here.
}
return self;
}
- (void)drawRect:(NSRect)dirtyRect
{
// Drawing code here.
[super drawRect:dirtyRect];
}
-(BOOL)isFlipped
{
return YES;
}
-(BOOL)acceptsFirstResponder
{
return NO;
}
-(BOOL)acceptsFirstMouse:(NSEvent *)theEvent
{
return NO;
}
-(NSView *)hitTest:(NSPoint)aPoint
{
return nil;
}
#end

user interaction enabled subviw in scrollview

I have a ScrollView that contains a ContainerView. The ContainerView contains another View that the user is supposed to be able to pan around in.
The scrollView scrolls vertical only, the "view inside the containerView" is panable in all directions.
Here is what I have
self.scrollView.contentSize = CGSizeMake(1024,1440);
self.modelController = [self.storyboard instantiateViewControllerWithIdentifier:#"LCProduct3DViewController"];
self.modelController.meshIdentifier = self.meshIdentifier;
[self addChildViewController:self.modelController];
self.modelController.view.frame = self.threeDView.bounds;
[self.threeDView addSubview:self.modelController.view];
What happens is that the touch events inside the modelController's view and the ones outside the modelControler's view but inside the scrollview bounds seems to be getting the the way of each other.
I played around with
self.scrollView.canCancelContentTouches = NO;
self.scrollView.delaysContentTouches = NO;
but havent found a working solution yet.
Any Ideas ?
Thanks in advance
as can be found in other places:
The trick is to subclass the scrollView and override the hitTest method like this:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView* result = [super hitTest:point withEvent:event];
if ([result isKindOfClass:[GLKView class]]) {
self.scrollEnabled = NO;
} else {
self.scrollEnabled = YES;
}
return result;
}
this way - if the hittest for the innver view is positive, scrolling for the scrollview is disabled and only the innver view will get the event.

Subclassing NSButton, need to make it look like a regular button

I'm subclassing NSButton because I need to repeat a selector while the mouse is being held down.
I'm doing that like this:
- (id)initWithFrame:(NSRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code here.
[self setBezelStyle:NSBezelBorder];
PotRightIsDown = NO;
}
return self;
}
- (void)drawRect:(NSRect)dirtyRect
{
// Drawing code here.
}
- (void)mouseDown:(NSEvent *)theEvent;
{
NSLog(#"pot right mouse down");
PotRightIsDown = YES;
holdDownTimer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:#selector(sendCommand) userInfo:nil repeats:YES];
}
- (void)mouseUp:(NSEvent *)theEvent;
{
NSLog(#"pot right mouse up");
PotRightIsDown = NO;
}
-(void)sendCommand
{
if (PotRightIsDown)
{
NSLog(#"run the stuff here");
}
else
{
[holdDownTimer invalidate];
}
}
Works like a champ, sends the command every 100ms.
In the window in IB, I've dragged a Bevel Button onto the window and set it's class to this subclass. When I ran the application, the button is invisible however it works. I'm guessing this is because I have an empty drawRect function in the subclass.
How can I make this subclassed button look like a Bevel button?
Thank you,
Stateful
If you aren't adding any functionality to a particular subclass method then you can simply avoid implementing it altogether, which will allow the superclass to provide the default behaviour.
Alternatively (as pointed out my #Carl Norum) you can explicitly do that using:
- (void)drawRect:(NSRect)dirtyRect
{
[super drawRect:dirtyRect];
}
But it's a bit pointless.

NSScrollView inside another NSScrollView

I have a NSScrollview nested inside another NSScrollview. How do i make the inner view handle horizontal scrolling only? Vertical scrolling should move the outer view.
Currently i pass the scrollWheel: event to the outer view from the inner view, but it is very slow.
I also had the problem of nested scroll views. The inner scroll view should scroll horizontally, and the outer should scroll vertically.
When handling scroll events from magic mouse / trackpad, it is important to pick only one of the scroll views for each gesture, otherwise you will see odd jerking when your fingers don't move perfectly straight. You should also ensure that tapping the trackpad with two fingers shows both scrollers.
When handling legacy scroll events from mighty mouse or mice with old fashioned scroll wheels, you must pick the right scroll view for each event, because there is no gesture phase information in the events.
This is my subclass for the inner scroll view, tested only in Mountain Lion:
#interface PGEHorizontalScrollView : NSScrollView {
BOOL currentScrollIsHorizontal;
}
#end
#implementation PGEHorizontalScrollView
-(void)scrollWheel:(NSEvent *)theEvent {
/* Ensure that both scrollbars are flashed when the user taps trackpad with two fingers */
if (theEvent.phase==NSEventPhaseMayBegin) {
[super scrollWheel:theEvent];
[[self nextResponder] scrollWheel:theEvent];
return;
}
/* Check the scroll direction only at the beginning of a gesture for modern scrolling devices */
/* Check every event for legacy scrolling devices */
if (theEvent.phase == NSEventPhaseBegan || (theEvent.phase==NSEventPhaseNone && theEvent.momentumPhase==NSEventPhaseNone)) {
currentScrollIsHorizontal = fabs(theEvent.scrollingDeltaX) > fabs(theEvent.scrollingDeltaY);
}
if ( currentScrollIsHorizontal ) {
[super scrollWheel:theEvent];
} else {
[[self nextResponder] scrollWheel:theEvent];
}
}
#end
My implementation does not always forward Gesture cancel events correctly, but at least in 10.8 this does not cause problems.
This is my subclass of NSScrollView that does what you are asking. Since it is merely passing the events it doesn't care about up the responder chain it should be as performant as if it weren't a subclass (or at least close)
h file
#import <Cocoa/Cocoa.h>
#interface HorizontalScrollView : NSScrollView
#end
and m
#implementation HorizontalScrollView
- (void)scrollWheel:(NSEvent *)theEvent {
NSLog(#"%#", theEvent);
if(theEvent.deltaX !=0)
[super scrollWheel:theEvent];
else
[[self nextResponder] scrollWheel:theEvent];
}
#end
Swift 4.
Answer is a translation of Jakob's excellent answer above, with the addition of a direction flag.
As mentioned in the comments on his answer, there can be subtle complications if this isn't done right. When simply forwarding all scrollWheel() calls to the next responder, I was seeing issues with NSTrackingAreas not being updated correctly and tooltips and cursor styles over NSTextView views being misaligned.
class ECGuidedScrollView: NSScrollView
{
enum ScrollDirection {
case vertical
case horizontal
}
private var currentScrollMatches: Bool = false
private var scrollDirection: ScrollDirection
init(withDirection direction: ScrollDirection) {
scrollDirection = direction
super.init(frame: NSRect.zero)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func scrollWheel(with event: NSEvent)
{
// Ensure that both scrollbars are flashed when the user taps trackpad with two fingers
if event.phase == .mayBegin {
super.scrollWheel(with: event)
nextResponder?.scrollWheel(with: event)
return
}
/*
Check the scroll direction only at the beginning of a gesture for modern scrolling devices
Check every event for legacy scrolling devices
*/
if event.phase == .began || (event.phase == [] && event.momentumPhase == [])
{
switch scrollDirection {
case .vertical:
currentScrollMatches = fabs(event.scrollingDeltaY) > fabs(event.scrollingDeltaX)
case .horizontal:
currentScrollMatches = fabs(event.scrollingDeltaX) > fabs(event.scrollingDeltaY)
}
}
if currentScrollMatches {
super.scrollWheel(with: event)
} else {
self.nextResponder?.scrollWheel(with: event)
}
}
}

Resources