Override behaviour of pressing "space" in PDF Kit view for OSX - objective-c

It seems like the standard behaviour of a pdfkit view when pressing space is to switch pages. We want to use the space button for other events (playing audio in our case).
Setting the space as shortcut in the menu does only work before the pdfkit view is interacted with. After that the behaviour is always set to switch pages.
We have also tried to intercept the space key-down-event using this code:
if (!keyDownEventMonitor) {
__weak IBBookViewController *weakSelf = self;
NSEvent * (^monitorHandler)(NSEvent *);
monitorHandler = ^NSEvent * (NSEvent * theEvent){
BOOL handleEvent = weakSelf.view.window != nil
&& theEvent.type == NSKeyDown
&& !theEvent.isARepeat;
if (handleEvent) {
switch ([theEvent keyCode]) {
case 49:
[self audioPlayPauseBtnHit:nil];
break;
default:
break;
}
}
return theEvent;
};
keyDownEventMonitor = [NSEvent addLocalMonitorForEventsMatchingMask:NSKeyDownMask handler:monitorHandler];
}
but it does only extend the space functionlity so both the audio is played and the page is switched.
Any ideas?

You have other directions to go here.
Subclassing to override behavior.
( keeping in mind that the behavior may be inherited from a superclass or part of a class that is a private ivar in the header for the pdfview class. )
Looking at the responder chain and which object really gets the keyDown message first. Intercept and forward, possibly changing key view temporarily.
Creating a cover view that handles the keyDown events you care about first and forwards other events to the responder chain.

Related

How to send app to background

On tvOS, I've only been able to get the begin states of button presses from the Siri remote by overriding the pressesBegan method on the view. If I use gesture recognizers, it only returns the end state. The catch is, when I override pressesBegan, even if I only use it for the select button, it still overrides the default function of the menu button (to push the app to the background). So I was looking into how to send the app to the background and call that method for the menu button (as is default behavior), but it appears that it is not kosher per Apple's standards to do that.
Here is my code for reference:
-(void)pressesBegan:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)event {
for (UIPress* press in presses) {
switch (press.type) {
case UIPressTypeSelect:
NSLog(#"press began");
break;
case UIPressTypeMenu:
// this is where I would call the send to background call if Apple would allow that
// removing this case also has no effect on what happens
break;
default:
break;
}
}
As an alternative, this ONLY sends button release signals, but nothing presses begin.
UITapGestureRecognizer* tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(gestureTap:)];
tapGesture.allowedPressTypes = #[[NSNumber numberWithInteger:UIPressTypeSelect]];
[view addGestureRecognizer:tapGesture];
When there's some behavior that happens if you don't override a method, and the behavior goes away in an empty override implementation, it stands to reason that behavior is provided by the superclass. (Cocoa is dynamic and complicated, so such inferences aren't true 100% of the time, but often enough.)
So, just call super for the cases where you don't want your override to change the default behavior:
case UIPressTypeMenu:
[super pressesBegan: presses withEvent: event];
break;

NSWindow, press key ENTER: how to limit the key listening to the focused NSControl?

I have an NSWindow with a main "OK" button. This button has as "key equivalent" property in interface builder, the key ENTER i.e ↵.
It works good, but now I have a new NSComboBox, which is supposed to invoke a method when the user selects a list item, or he preses Enter / ↵.
However, when I press Enter, the main Button receive the notification and the window close. How to prevent this?
This is the normal behavior what you are getting, but you can hack a bit, by removing and adding the key-equivalent.
Add following delegates of NSComboBox:
- (void)comboBoxWillPopUp:(NSNotification *)notification;{
[self.closeButton setKeyEquivalent:#""];
}
- (void)comboBoxWillDismiss:(NSNotification *)notification;{
[self.closeButton setKeyEquivalent:#"\r"];
}
One way you can workaround for prevent enter notification is like that below:-
//Connect this action method to your combobbox and inside that set one BOOL flag to yes
- (IBAction)comBoxItm:(id)sender
{
self.isEnterCalled=YES;
}
//Now check this flag to your some method where close window is called
-(void)someMethod
{
//Check the flag value if it is yes then just ignore it
if (!self.isEnterCalled)
{
//Close window logic
}
self.isEnterCalled=NO;
}
Ran into the same problem. Had "hot key" which I'd like to switch off while editing some text fields. I found solution for myself. There's no need in override lots of NSTextField base methods.
Firstly, I removed all the "key equivalents". I used to detect Enter key down with the + (void)addLocalMonitorForEventsMatchingMask:(NSEventMask)mask handler:(NSEvent *(^)(NSEvent *))block class method of NSEvent. You pass block as a parameter, where you can check for some conditions. The first parameter is the event mask. For your task it would be NSKeyDownMask, look for other masks at the NSEvent Reference Page
The parameter block will perform each time the user pushes the button. You should check if it is right button pushed, and - generally - if the current window first responder isn't some editable control. For that purposes we need NSWindow category class just not to implement this code each time we deal with NSKeyDownMasked local monitors.
NSWindow+Responders class listing:
#interface NSWindow (Responders)
- (BOOL)isEditableFirstResponder;
#end
#implementation NSWindow (Responders)
- (BOOL)isEditableFirstResponder
{
if (!self.firstResponder)
return NO; // no first responder at all
if ([self.firstResponder isKindOfClass:[NSTextField class]]) // NSComboBox is NSTextField subclass
{
NSTextField *field=(NSTextField *)self.firstResponder;
return field.isEditable;
}
if ([self.firstResponder isKindOfClass:[NSButton class]]) // yep, buttons may be responders
return YES;
return NO; // the first responder is not NSTextField or NSButton subclass - not editable
}
#end
Don't know if there's another way to check if we are now editing some text field or combo box. So, there's at least the part you add the local monitor somewhere in your class (NSWindow, NSView, some controller etc.).
- (void)someMethod
{
id monitor=[NSEvent addLocalMonitorForEventsMatchingMask:NSKeyDownMask handler:(NSEvent *)^(NSEvent *theEvent){
if (theEvent.keyCode==/*Enter key code*/ && ![self.window.isEditableFirstResponder]) // you should check the key modifiers too
{
// your code here
}
return theEvent; // you may return the event to pass the key to the receiver
}];
}
Local monitors is safe remedy about the Apple rules. It works only inside your application. For global key down events you may use addGlobalMonitor but Apple may reject your app from the AppStore.
And don't forget to remove the monitor when there's no need in it.
- (void)viewControllerShutdownMethod
{
[NSEvent removeMonitor:monitor];
}
Good luck.

Allowing escape to exit full screen as it normally does in Cocoa

So I'm enabling full screen mode in my Cocoa/Mac app, and the default behavior obviously is that you can hit the escape key to exit out of full screen mode. I've added some NSTextViews (inside NSScrollViews), and now they are intercepting the escape key and I'm unable to exit out of full screen.
What's the best way for me to still allow the escape key to exit out of full screen and not be intercepted by my NSTextViews?
Thanks!
The default key binding for Escape is to -cancelOperation:. In response to that, NSTextView then turns around and sends itself -complete: to do text completion.
Is that what you're seeing when you press Escape? Does the text view offer to complete any text that you've typed?
The first thing to try is to disable completion by implementing -textView:completions:forPartialWordRange:indexOfSelectedItem: in the text view's delegate and returning nil.
If that doesn't work, implement -textView:doCommandBySelector: and compare the selector to #selector(cancelOperation:). If it's not equal, return NO to allow normal processing. If it is, try passing it up the responder chain using [[theTextView nextResponder] doCommandBySelector:theSelector]. Then return YES from your delegate method to prevent the text view from trying to handle it.
If that still doesn't work, instead of passing it up the responder chain, check if the app is in full-screen mode by testing if [NSApp presentationOptions] contains NSApplicationPresentationFullScreen. If it is, get it out of full-screen mode by setting a normal set of presentation options. Alternatively, you can test the main window to see if its style mask includes NSFullScreenWindowMask and, if it does, call -toggleFullScreen: on it. Again, you'd return YES from your delegate method to prevent further processing.
Based on Ken's suggestions, here's what I ended up doing to get it to work:
- (BOOL)textView:(NSTextView *)textView doCommandBySelector:(SEL)commandSelector {
if (commandSelector == #selector(cancelOperation:)) {
if([self inFullScreenMode]) {
[self.window toggleFullScreen:nil];
}
}
else {
return NO;
}
return YES;
}
- (BOOL) inFullScreenMode {
NSApplicationPresentationOptions opts = [[NSApplication sharedApplication ] presentationOptions];
if ( opts & NSApplicationPresentationFullScreen) {
return YES;
}
return NO;
}

How can I close a NSWindow loaded modally as popup, by clicking outside of it?

How can I close a NSWindow loaded modally as popup, by clicking outside of it?
I would like to handle the mouse event, when the cursor is outside the modal window with the focus (but still inside the app).
You may implement the following delegate method of NSWindow to get the notification of window losing focus.
- (void)windowDidResignKey:(NSNotification *)notification
And inside check, if your application is the front most App. If yes then close accordingly.
While the application is in modal run loop, it does not respond to any
other events (including mouse, keyboard, or window-close events)
unless they are associated with the window. It also does not perform
any tasks (such as firing timers) that are not associated with the
modal run loop.
You can use nextEventMatchingMask:untilDate:inMode:dequeue: method.This will work in modal loop.
Both NSWindow and NSApplication define the method
nextEventMatchingMask:untilDate:inMode:dequeue:, which allows an
object to retrieve events of specific types from the event queue.
As mentioned above it is necessary to override [NSApplication nextEventMatchingMask:untilDate:inMode:dequeue:] method. In my case (plugin) I have to use the existing instance of a 3rd party unknown class derived from NSApplication. I can't just derive a new class from it. Thus I use method_exchangeImplementations to exchange the named method above with my own implementation
+ (void)hijack
{
Class appClass = [NSApplication class];
Method originalMethod = class_getInstanceMethod(appClass, #selector(nextEventMatchingMask:untilDate:inMode:dequeue:));
Method categoryMethod = class_getInstanceMethod(appClass, #selector(my_nextEventMatchingMask:untilDate:inMode:dequeue:));
method_exchangeImplementations(originalMethod, categoryMethod);
}
which looks as follows:
- (NSEvent *)my_nextEventMatchingMask:(NSUInteger)mask untilDate:(NSDate *)expiration inMode:(NSString *)mode dequeue:(BOOL)deqFlag
{
NSEvent *event = [self my_nextEventMatchingMask:mask untilDate:expiration inMode:mode dequeue:deqFlag];
NSEventType type = [event type]; // 0 if event is nil
if (type == NSLeftMouseDown || type == NSRightMouseDown)
{
if ([self modalWindow] != nil && [event window] != [self modalWindow])
{
[self stopModalWithCode:NSModalResponseCancel];
event = nil;
}
}
return event;
}
And finally the modal window is invoked as follows:
[NSApplication hijack];
[NSApp runModalForWindow:window];
[NSApplication hijack];
Obviously if you can just override NSApplication then you don't need to define and call hijack method.

QuickLook consumer as a delegate from an NSViewController

I am having some problems implementing QuickLook functionality from a table in an NSView. The limited documentation on QuickLook really doesn't help at all.
After reading through the Apple Docs (which are geared heavily towards custom generators and plugins), I ended up looking at the QuickLookDownloader sample code. This code is based upon a document-based application, but appears to be the right method for me (after all it is Apple's code and it does work in their project).
In my implementation I can get the QuickLook panel to show up just fine, and I can dismiss it just as easy. However, the panel itself never calls the delegate methods from within my NSViewController. As a result I never even get to displaying objects, just the wording "No items selected". And I am stumped.
I tried calling a setDelegate, but get warned about impending doom if I continue down that route...
[QL] QLError(): -[QLPreviewPanel setDelegate:] called while the panel has no controller - Fix this or this will raise soon.
See comments in QLPreviewPanel.h for -acceptsPreviewPanelControl:/-beginPreviewPanelControl:/-endPreviewPanelControl:.
And then doom happens anyway with a dealloc when trying to respond to one of the delegate methods.
And yes I did read the header which confirms that I should be setting the delegate after I won the panel (see code below).
So here's my code, which pretty much matches the sample code with the exception of a) where I get my data from (I get it from an NSArrayController) and the b) where I get my preview item from (mine comes directly from my model object - or should anyway)
#interface MyViewController : NSViewController
<QLPreviewPanelDataSource, QLPreviewPanelDelegate> {
QLPreviewPanel * previewPanel;
NSArrayController * myArrayController;
NSTableView * myTable;
// [...] Other instance vars
}
#implementation MyViewController
// [...] all the other methods, init, dealloc etc...
-(IBAction)togglePreviewPanel:(id)previewPanel {
if ([QLPreviewPanel sharedPreviewPanelExists] &&
[[QLPreviewPanel sharedPreviewPanel] isVisible])
{
[[QLPreviewPanel sharedPreviewPanel] orderOut:nil];
}
else
{
[[QLPreviewPanel sharedPreviewPanel] makeKeyAndOrderFront:nil];
}
}
-(BOOL)acceptsPreviewPanelControl:(QLPreviewPanel *)panel
{
return YES;
}
// This document is now responsible of the preview panel.
// It is allowed to set the delegate, data source and refresh panel.
-(void)beginPreviewPanelControl:(QLPreviewPanel *)panel
{
if (DEBUG) NSLog(#"QuickLook panel control did BEGIN");
previewPanel = [panel retain];
panel.delegate = self;
panel.dataSource = self;
}
// This document loses its responsisibility on the preview panel.
// Until the next call to -beginPreviewPanelControl: it must not change
// the panel's delegate, data source or refresh it.
-(void)endPreviewPanelControl:(QLPreviewPanel *)panel
{
[previewPanel release];
previewPanel = nil;
if (DEBUG) NSLog(#"QuickLook panel control did END");
}
// Quick Look panel data source
-(NSInteger)numberOfPreviewItemsInPreviewPanel:(QLPreviewPanel *)panel
{
if (DEBUG) NSLog(#"QuickLook preview count called");
return [[myArrayController selectedObjects] count];
}
-(id <QLPreviewItem>)previewPanel:(QLPreviewPanel *)panel
previewItemAtIndex:(NSInteger)index
{
if (DEBUG) NSLog(#"QuickLook preview selection of item called");
return [[displayAC selectedObjects] objectAtIndex:index];
}
-(BOOL)previewPanel:(QLPreviewPanel *)panel handleEvent:(NSEvent *)event {
if (DEBUG) NSLog(#"QuickLook panel error handler called");
// redirect all key down events to the table view
if ([event type] == NSKeyDown) {
[myTable keyDown:event];
return YES;
}
return NO;
}
The issue seems to be that the acceptsPreviewPanelControl never gets called, so the delegates never get used (they definitely never get called).
I'm sure this is a simple step that I'm missing, but after dissecting the sample code and scouring over the docs I don't see the answer.
Is it because this is all from within an NSViewController (although I see no reason why that should even come into the equation)?
Any and all help much appreciated.
SOLUTION UPDATE
Thanks to Peter's observation, the fix was a quick one. Don't you hate it when the error message in the debugger means what it says? :-)
In my class that loaded MyViewController I simply needed to add three lines of code to fix the problem.
// mainWindow is an IBOutlet to my window because the calling class
// is a simple object and not an NSWindowController otherwise I could
// have used `self` instead of `mainWindow`
NSResponder * aNextResponder = [mainWindow nextResponder];
[mainWindow setNextResponder:myViewControllerInstance];
[myViewControllerInstance setNextResponder:aNextResponder];
Job done :-) Thanks Peter.
Why would you expect it to send you delegate messages if you aren't (yet) its delegate? If you want it to send you delegate messages, then you need to set yourself as its delegate.
I tried calling a setDelegate, but get warned about impending doom if I continue down that route...
[QL] QLError(): -[QLPreviewPanel setDelegate:] called while the panel has no controller - Fix this or this will raise soon. See comments in QLPreviewPanel.h for -acceptsPreviewPanelControl:/-beginPreviewPanelControl:/-endPreviewPanelControl:.
“No controller”, it says. So, you need it to have a controller.
The comments on that header, particularly on acceptsPreviewPanelControl: and the QLPreviewPanel instance method updateController, suggest that the panel's controller, when it has one, is an object that is in the responder chain. Therefore, if your controller is not becoming the panel's controller, it's because your controller isn't in the responder chain.
So, fix that, and then it'll work.
I would imagine that your view controller should be in the responder chain whenever its view or any subview thereof is in the responder chain, but maybe this isn't the case. The documentation doesn't say. If all else fails, set yourself as some view's next responder explicitly (and its previous next responder as your next responder), then send the preview panel an updateController message.
After so many years, in the swift world, I found this line of code works as well.
Without rearrange the default response chain, just "push" your view controller to be the first responder in the window. I'm not sure if it works for every scenario:
view.window?.makeFirstResponder(self)
And the object setups are the same:
override func acceptsPreviewPanelControl(_ panel: QLPreviewPanel!) -> Bool {
return true
}
override func beginPreviewPanelControl(_ panel: QLPreviewPanel!) {
panel.dataSource = self
panel.delegate = self
panel.currentPreviewItemIndex = //your initial index
}
override func endPreviewPanelControl(_ panel: QLPreviewPanel!) {
panel.dataSource = nil
panel.delegate = nil
}