Change NSMenuItem state in MenuController from AppController? - objective-c

My code is split into two main implementations: MenuController.m and AppController.m, each with header files.
I have a couple user preferences, which are stored using NSUserDefaults, and changed via NSMenuItems so that they show check marks when enabled (using setState: NSOffState). There's only one missing bit of my setup- I need the app to setState for those menu items on startup if the options are on in the prefs. However, the only way I know to set something on app launch is to have it in the awakeFromNib method, and that's in the AppController and can't access the NSMenuItem instantiated in the MenuController.
I am rather new to Objective-C, and have managed to get this far thanks to many helpful tutorials and answers on this site, but right now I'm just stumped.
I've tried using class and object methods to change the settings, but have failed- I need to use the existing instance of the NSMenuItems. validateMenuItem looked promising, but it only enables and disables menus and doesn't setState.
Relevant code (I think):
from MenuController.h:
#interface MenuController : NSMenu {
IBOutlet NSMenu *optionsMenu;
IBOutlet NSMenuItem *onTopItem;
IBOutlet NSMenuItem *liveIconItem;
}
- (IBAction)menuLiveIconToggle:(id)pid;
from MenuController.m: (method to change prefs and setState- works great)
- (IBAction)menuLiveIconToggle:(id)pid; {
//NSLog(#"Live Icon Toggle");
NSUserDefaults *standardUserDefaults = [NSUserDefaults standardUserDefaults];
if ([standardUserDefaults boolForKey:#"LiveIcon"] == TRUE){
[standardUserDefaults setBool:FALSE forKey:#"LiveIcon"];
[liveIconItem setState: NSOffState];
} else {
[standardUserDefaults setBool:TRUE forKey:#"LiveIcon"];
[liveIconItem setState: NSOnState];
}
[standardUserDefaults synchronize];
}
from AppController.m: (does NOT work, but this is the gist of it)
- (void) awakeFromNib{
// Update menu items
if ([standardUserDefaults boolForKey:#"LiveIcon"] == TRUE) {
[liveIconItem setState: NSOnState];
} else {
[liveIconItem setState: NSOffState];
}
}
Thanks for any help!

There are several ways you could achieve this. First, you could simply move your awakeFromNib implementation into your MenuController class, where you have access to the outlets. awakeFromNib is not specific to the App Delegate, but available for all objects that are loaded from Nibs (as you have outlets in your MenuController, I assume that it is loaded from a Nib).
You could also implement validateMenuItem:, always return YES, but also set the state of the menu item that is given to you as the parameter.
Or, get rid of all the code and just use bindings in Interface Builder. You can bind the "value" (== state) of your menu item to the "Shared User Defaults Controller" and enter "LiveIcon" as the model key path. You can then delete all of the code you posted and it'll just work.

Related

menuWillOpen: and menuDidClose: not invoked for NSMenuDelegate

[Edit] as Willeke helpfully points out it's menuDidClose: NOT menuWillClose:. My code actually had that part right. Correcting the post in case someone else finds this researching a similar problem.
I'm sure this is just a Cocoa newbie problem but I've wracked my brain on it for hours. I've read the NSMenu and NSMenuDelegate docs a few times trying to figure out what I'm missing but it looks straight forward.
I have a window controller for a preferences window with a toolbar and three views. The window controller is declared as NSMenuDelegate.
#interface PrefsController : NSWindowController <NSMenuDelegate, NSWindowDelegate, NSOpenSavePanelDelegate>
This issue is a NSPopUpButton on the first view. The menu associated with popupbutton works fine. I can modify, etc. the menu via the associated IBOutlet variable. It's bound to Shared User Defaults Controller for selected value and that works fine.
But the menuWillOpen: and menuDidClose: methods are not invoked when the menu is accessed.
- (void)menuWillOpen:(NSMenu *)menu {
if (menu == myPopupButton.menu) {
[self updateMenuImages:NSMakeSize(32, 32)];
}
}
- (void)menuDidClose:(NSMenu *)menu {
if (menu == myPopupButton.menu) {
[self updateMenuImages:NSMakeSize(16, 16)];
}
}
My apologies for what is almost certainly a dumb mistake on my part, but I'm stumped.
Menu delegates are not used that often, so Apple hasn't made them too easy to set up in Interface Builder. Instead, do this in awakeFromNib:
myPopupButton.menu.delegate = self;

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.

Listen to a value change of my text field

I'm trying to understand how to catch a "text changed" event from a text field in my window. I'm used to Java's "action listeners", and can't find anything similar in Objective-C/Cocoa.
I searched for quite a while and found the "key value observing" protocol, but the observeValueForKeyPath: method (function?) only triggers when the value of my text field was changed in code (using [textfield setStringValue:...], e.g.), not by typing in it.
How can I "listen" to the value change when a user types in the text field?
You can set a delegate for your NSTextField instance and have the delegate implement the following method:
- (void)controlTextDidChange:(NSNotification *)notification {
// there was a text change in some control
}
Your delegate object can be the application delegate, a window controller, a view controller, or some other object in your application. The delegate can be programatically set via
[myTextField setDelegate:delegateObject];
or, in Interface Builder, via the delegate outlet available in the NSTextField control.
Note that if there are multiple controls hooked to the same delegate then -controlTextDidChange: will be sent for each control, i.e., the same method is called for different controls. If you want different behaviour according to the control where the text has changed, you can use -[NSNotification object] to identify the control that has sent the notification.
For instance, if you have two text fields with corresponding outlets nameField and addressField, and you’ve set the same delegate for both fields, then:
- (void)controlTextDidChange:(NSNotification *)notification {
// there was a text change in some control
// [notification object] points to the control that has sent
// the notification
if ([notification object] == nameField) {
// nameField has changed
}
else if ([notification object] == addressField) {
// addressField has changed
}
}
Alternatively, you could have one delegate for each text field. In this case, there’d be no need to test [notification object].
You can also just hook up to the "Editing Changed" from IB and create the Action to handle it
- (IBAction)txtField_Changed:(id)sender
{
// my textfield has been changed
}
This works for me
func textView(textView: NSTextView, shouldChangeTextInRange affectedCharRange: NSRange, replacementString: String?) -> Bool {
print("Changed!")
return true
}
You can use textFieldShouldBeginEditing: method of UITextFieldDelegate. In iOS listeners are called NSNotifications
EDIT
In objective-c a lot of UIObjects have a corresponding protocol class that's called "delegate" The delegate is responsible for reacting to events. So to be able to respond or to be notified about actions you need to implement the delegate and its methods.

NSTableView + Delete Key

I'm looking for an easy solution to delete NSTableView rows by pushing the delete key.
All I have seen when searching in Google were answers like this:
http://likethought.com/lockfocus/2008/04/a-slightly-improved-nstableview/
This seems to me an Engineering solution, but I would like to know if this is the best way. Does any one know a better answer?
What I usually do is create a new menu item in your application's menu bar. Something like:
File -> Delete ${Name of Item}
Then you can link that NSMenuItem in Interface Builder to point to an IBAction method defined somewhere on either your app delegate or some other controller. The implementation for this method should delete the item from your model, and refresh the NSTableView.
The advantage to making an NSMenuItem out of the action is that:
You can give the item a keyboard shortcut in Interface Builder. (Like the delete key.)
Users who are not familiar with your application, afraid to press the delete key, or do not have access to a keyboard for whatever reason, can still make use of this functionality.
I've implemented something similar to LTKeyPressTableView. However, I use array controllers, so in my subclass I added IBOutlet NSArrayController * relatedArrayController. Instead of handling delete request in a delegate, I handle it directly in the subclass since my subclass specifically deals with adding handling of Delete key. When I detect keypress for delete key, I'm just calling [relatedArrayController delete:nil];.
IRTableView.h:
#import <Cocoa/Cocoa.h>
#interface IRTableView : NSTableView {
IBOutlet NSArrayController * relatedArrayController;
}
#end
and IRTableView.m:
#import "IRTableView.h"
#implementation IRTableView
- (void)keyDown:(NSEvent *)event
{
// Based on LTKeyPressTableView.
//https://github.com/jacobx/thoughtkit/blob/master/LTKeyPressTableView
id delegate = [self delegate];
// (removed unused LTKeyPressTableView code)
unichar key = [[event charactersIgnoringModifiers] characterAtIndex:0];
if(key == NSDeleteCharacter)
{
if([self selectedRow] == -1)
{
NSBeep();
}
BOOL isEditing = ([[self.window firstResponder] isKindOfClass:[NSText class]] &&
[[[self.window firstResponder] delegate] isKindOfClass:[IRTableView class]]);
if (!isEditing)
{
[relatedArrayController remove:nil];
return;
}
}
// still here?
[super keyDown:event];
}
#end
End result is quite IB-friendly for me, and a quite simple solution for use in a Cocoa Bindings+Core Data application.
There is no need to subclass and catch keyDown in NSViewController.
The Delete menu item in the menu Edit is connected to the selector delete: of First Responder. If there is no Delete menu item, create one and connect it to delete: of First Responder (red cube).
Assign a key equivalent to the Delete menu item (⌫ or ⌘⌫)
In the view controller implement the IBAction method
Swift: #IBAction func delete(_ sender: AnyObject)
Objective-C: -(IBAction)delete:(id)sender
and put in the logic to delete the table view row(s).
After 10.10, NSViewController is part of the responder chain. So the easiest way is to implement keyDown in your subclassed NSViewController

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
}