How to get NSScrollView to scroll horizontally with the scroll wheel without the Shift key? - cocoa

I'm using an NSScrollView to implement a tab bar which scrolls horizontally when there are too many tabs to fit in the window. I can move the scrollview left and right using the trackpad just fine. I'd like to enable mouse users to scroll left and right using the scroll wheel. Right now, this already works if the user holds the Shift key while turning the scroll wheel - the tabs will scroll horizontally. However, I'd like for it to work without the Shift key. This is how Safari's tabs work, for example.
How do I implement horizontal scrolling using the scroll wheel without the Shift key?
Please note that my tab bar does not display a horizontal scroller. Since macOS 10.9's introduction of responsive scrolling, I don't believe we can override -scrollWheel: anymore (see the Update at the bottom of this post: https://stackoverflow.com/a/31201614/111418)

I used an NSEvent monitor to accomplish this.
_scrollWheelEventMonitor = [NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskScrollWheel handler:^NSEvent * _Nullable(NSEvent * _Nonnull event) {
NSPoint location = [_scrollView convertPoint:event.locationInWindow fromView:nil];
// We want events:
// where the mouse is over the _scrollView
// and where the user is not modifying it with the SHIFT key
// and initiated by the scroll wheel and not the trackpad
if ([_scrollView mouse:location inRect:_scrollView.bounds]
&& !event.modifierFlags
&& !event.hasPreciseScrollingDeltas)
{
// Create a new scroll wheel event based on the original,
// but set the new deltaX to the original's deltaY.
// stackoverflow.com/a/38991946/111418
CGEventRef cgEvent = CGEventCreateCopy(event.CGEvent);
CGEventSetIntegerValueField(cgEvent, kCGScrollWheelEventDeltaAxis2, event.scrollingDeltaY);
NSEvent *newEvent = [NSEvent eventWithCGEvent:cgEvent];
CFRelease(cgEvent);
return newEvent;
}
return event;
}];

Related

Replicate UIScrollView's scrollToTop does not expand the UINavigationBar on UITabBar tap in iOS 11

(Similar to this question, which is unanswered: Tableview scroll to top not going all the way, as well as this one, also unanswered: Show navigation bar's large title and search bar on scroll to top collection view iOS 11 Swift 4)
I am trying to replicate scrolling to the top of a UITableView on status bar tap, but when I've tapped the UITabBar item and I'm already in that view. I have the tapping part working, but the scrolling to top is not working as I want it to.
The tableview is embedded in a navigationbar with large titles, and the searchbar is in the header, so it expands and collapses with scroll (the default behavior).
I am using the following to scroll, which, as expected, scrolls to the first table view cell, not expanding the navigation bar and/or the search bar:
[tv scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]
atScrollPosition:UITableViewScrollPositionTop animated:YES];
I can't seem to figure out how to scroll to the top and expand the navigation bar and the search bar. Manually calculating the content offset of the tableview doesn't work, as the offsets of the tableview are obviously different when the tableview is scrolled. Furthermore, I can't store the offset, as different screen sizes have different content offsets for an expanded navigationbar and searchbar.
Has anyone been able to figure this out?
Well, I'm a bit late to the action here but for future reference:
For iOS 10+ (sufficient, since growing navigationBars were introduced in 11) you can use
scrollView.perform(NSSelectorFromString("_scrollToTopIfPossible:"), with: true)
This is private API. I've searched for a couple of hours and tried a lot of stuff but in the end this is the only thing that worked. Note that WhatsApp seems to be using the same method. Maybe they have a special deal with Apple or hide the api call (cough base64 cough).
#implementation UIWindow (SCROLL)
- (void)performScrollToTop {
SEL selector = NSSelectorFromString(#"_scrollToTopViewsUnderScreenPointIfNecessary:resultHandler:");
if ([self respondsToSelector:selector] == false) {
return;
}
NSMethodSignature *signature = [UIWindow instanceMethodSignatureForSelector:selector];
if (signature == nil) {
return;
}
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[UIWindow instanceMethodSignatureForSelector:selector]];
[invocation setTarget:self];
[invocation setSelector:selector];
CGRect statusBarFrame = UIApplication.sharedApplication.statusBarFrame;
CGPoint point = CGPointMake(statusBarFrame.size.width / 2.0, statusBarFrame.size.height + 1.0);
[invocation setArgument:&point atIndex:2];
[invocation invoke];
}
#end

InAppStoreWindow, how to reposition the NSView in NSWindow's title bar when resize?

I'm using InAppStorewindow (https://github.com/indragiek/INAppStoreWindow) to cuztomize my NSWindows's title bar. What i'm trying to do is to add a logo (image) to my title bar in center position:
NSSize logoSize = self.logo.frame.size;
NSRect logoFrame = NSMakeRect(NSMidX(self.window.titleBarView.bounds) - (logoSize.width / 2.f),
NSMidY(self.window.titleBarView.bounds) - (logoSize.height / 2.f),
logoSize.width, logoSize.height);
self.logo.frame = logoFrame;
I put the above code in applicationDidFinishedLaunching method.
it works fine, however if I click on the green resize button, the position won't change. So how am I going to call the above code to reposition my logo, when the resize button is clicked and performZoom: is called?
You can use the NSWindowDelegate Method
- (void)windowDidResize:(NSNotification *)notification
By that you will know when the window is resized so you can recalculate the position of your logo.

Change NSTextField's behavior for multiple clicks in a row

I have a NSTextField which is nested by a custom view and I want to change the default behavior of multiple clicks in a row (double click, tripple click etc.), similarly to the behavior of text nodes MindNode (see the image below).
I want the first click to "activate" the text field and then go on from the beginning (like reseting the click count of the event).
I have following ideas, but I don't know how to implement them and if they actually make sense:
Somehow change the time using +[NSEvent doubleClickInterval] and slow down the second click.
Reduce the click count programmatically?
Make the NSTextField non-selectable using -hitTest:, forward the click to the superview, change some parameter of the text field and accept the next clicks. In this case, the click count of the second click is still 2.
Override -mouseDown: and not call super. This breaks the NSTextField's selection functionality.
I hope there is an easier way to achieve this, which I have overlooked.
Thanks for your answers!
Here is a graphical representation of the problem:
I would do this by embedding the text field and a custom view in an NSBox, which would be set to the custom type, initially with no background color or border (so it would be invisible). Initially, the custom view would be on top and it would have a mouseDown: method that would receive the first click. In that method you could rearrange the box's subviews so that the text field would then be on top and receive the next clicks. If you wanted, the box could be somewhat bigger than the text field so you could give it a background color or other drawing that would look like a custom activation ring around the text field. In the text field's controlTextDidEndEditing: method, you could reset the system back to the beginning state, so it would be ready for the next time you click on it.
After Edit: Here is the code I'm using in my overlay class:
#implementation Overlay
static NSComparisonResult rdComparator( NSView *view1, NSView *view2, void *context ) {
if ([view1 isKindOfClass:[NSTextField class]])
return NSOrderedDescending;
else if ([view2 isKindOfClass:[NSTextField class]])
return NSOrderedAscending;
return NSOrderedSame;
}
-(void)mouseDown:(NSEvent *)theEvent {
self.box.fillColor = [NSColor redColor];
NSView *contentView = self.box.subviews.lastObject;
[contentView sortSubviewsUsingFunction:rdComparator context:nil];
}
I've solved it by subclassing NSTextField and decrementing click count of mouse down events programmatically. Using a boolean property of the subclass I am able to turn this special behavior on and off.
- (void)mouseDown:(NSEvent *)theEvent
{
if (self.specialBehavior) {
theEvent = [NSEvent mouseEventWithType:theEvent.type
location:theEvent.locationInWindow
modifierFlags:theEvent.modifierFlags
timestamp:theEvent.timestamp
windowNumber:theEvent.windowNumber
context:theEvent.context
eventNumber:theEvent.eventNumber
clickCount:theEvent.clickCount - 1
pressure:theEvent.pressure];
}
[super mouseDown:theEvent];
}
To simplify this long method call, I wrote a category method for NSEvent which decrements the click count of an event.

NSScrollView scroll bars are of the wrong length

I have a Cocoa window, whose content view contains an NSScrollView that, in turns, contains a fixed-size NSView.
Upon launching the program, the scroll bars displayed initially are too small, as if the content size was much larger than it actually is:
When I start playing with, e.g., the vertical scroll bar, and bring it back to the original position at the top, it gets resized to its expected size (which corresponds to the ratio of scroll view and content view sizes):
(Notice the horizontal bar, which still has incorrect size. If I then play with it, and bring it back to its leftmost position, it gets resized to the correct size.)
I also encountered the same problem, I have searched everywhere but it seems no one else experiences this problem. Fortunately I found a hack which solves the problem.
What I did notice was that when the window is resized or maximized the scrollbars resize to the expected size (autoresizing has to be enabled). This is because when the window resizes so does the scrollview and the length of the scroll bars gets recalculated and is calculated correctly. Possibly due to some bug the scroll bar lengths are not calculated correctly on initialization. Anyway to fix the problem, in your application delegate create an outlet to your window. Override the "applicationDidFinishLaunching" method and inside it call the method "frame" on the window outlet, which returns the current NSRect of the window. Using the returned value add one to the size.width and size.height. The call the method setFrame with display set to YES. This will resize the window and force the size of the scrollbars to be recalculated.
Here is the code for applicationDidFinishLaunching Below
(void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
// Get the current rect
NSRect windowRect = [_window frame];`
// add one to the width and height to resize window
windowRect.size.width += 1;
windowRect.size.height += 1;
// resize window with display:YES to redraw window subviews
[_window setFrame:windowSize display:YES];
}
I encountered this issue when modifying an NSTextView textContainer size to toggle line wrapping. Resizing the enclosing view does cause the correct scroll view height to be used, however its a brutal solution.
NSScrollView supports -reflectScrolledClipView. Calling this directly in my case had no effect except when delayed on the runloop:
[textScrollView performSelector:#selector(reflectScrolledClipView:) withObject:textScrollView.contentView afterDelay:0];
The scroller position is correct but there is a scroller redraw. So it looks as if part of the view geometry is calculated when drawing. A better solution is therefore:
NSDisableScreenUpdates();
[textScrollView display];
[textScrollView reflectScrolledClipView:textScrollView.contentView];
[textScrollView display];
NSEnableScreenUpdates();
Building on the answer from jstuxx above, if you don't want the window to visibly resize, try:
NSRect windowRect = [[[self view] window] frame];
windowRect.size.width += 1;
windowRect.size.height += 1;
[[[self view] window] setFrame:windowRect display:YES];
windowRect.size.width -= 1;
windowRect.size.height -= 1;
[[[self view] window] setFrame:windowRect display:YES];
I had to put this code after where I was programmatically adding the scroll view to my interface.

OSX assign left mouse click to a keyboard key

I would like to be able to assign a key on my keyboard to be equivalent to a left mouse click.
Ideally it needs to act such that holding the key down is also equivalent to holding the left mouse button down.
I'd like this capability as a user, additionally a programmatic solution (cocoa/applescript etc) would be great too.
Not exactly what you want, but in the System preferences -> Universal access you can turn on mouse keys - and with them you can move (and click) mouse by keyboard. docs here:
http://docs.info.apple.com/article.html?path=Mac/10.6/en/cdb_moskys.html
http://docs.info.apple.com/article.html?path=Mac/10.6/en/8565.html
http://docs.info.apple.com/article.html?artnum=61472
Or,
With the "ControllerMate.app" is possible to do this, but it is commercial app.
This can be done by writing some code:
Write a global handler to receive the type of event you want to watch
[NSEvent addGlobalMonitorForEventsMatchingMask:NSKeyDownMask
handler:^(NSEvent *event){
NSLog(#"%i", [event keyCode]);
//todo invoke mouse clicking code;
}];
Then write the mouse click code:
// get current mouse pos
CGEventRef ourEvent = CGEventCreate(NULL);
CGPoint point = CGEventGetLocation(ourEvent);
NSLog(#"Location? x= %f, y = %f", (float)point.x, (float)point.y);
// perform a click
CGEventSourceRef source = CGEventSourceCreate(kCGEventSourceStateCombinedSessionState);
CGEventRef theEvent = CGEventCreateMouseEvent(source, kCGEventLeftMouseDown, point, kCGMouseButtonLeft);
CGEventSetType(theEvent, kCGEventLeftMouseDown);
CGEventPost(kCGHIDEventTap, theEvent);
CFRelease(theEvent);

Resources