Detecting focus/unfocus of active NSDocument - objective-c

Is there anyway to detect the focus/unfocus of an NSDocument? I would like to dynamically update a menu item that pertains to the active document but I can't see any immediately obvious way of doing it.
The reason being, I'd like to activate and then populate the menu on document focus, and then unpopulate and deactivate on loss of focus.
Any ideas?
Thanks,

It appears the NSDocument is set as the delegate for all the document windows, and so the hooks required in my NSDocument subclass were:
- (void) windowDidBecomeMain: (NSNotification *) notification
{
NSLog(#"windowDidBecomeMain:");
}
- (void) windowDidResignMain: (NSNotification *) notification
{
NSLog(#"windowWillResign:");
}
- (void) windowWillClose: (NSNotification *) notification
{
NSLog(#"windowWillClose:");
}

The above solution didn't work, this did:
I have a window controller (subclassing NSWindowController) and I add a delegate on the window
-(void)windowDidLoad {
[super windowDidLoad];
self.window.delegate = self;
}
-(void)windowDidBecomeKey:(NSNotification *)notification {
// this gets called
}

Related

Disable Button Loop (NStimer or MouseMove) XCode Objective-C MacOS

I am just experimenting with disabling a button with various methods on XCode MacOS (not iOS) Cocoa Objective-C.
In this scenario I have a help button (m_btHelp) that is disabled when g_bEnableHelpButton = NO; but it is only being checked when the mouse moves.
-(void)mouseMoved:(NSEvent *)theEvent
{
if(g_bEnableHelpButton) {
[m_btHelp setEnabled:YES];
} else {
[m_btHelp setEnabled:NO];
}
I would rather have this continuously checked instead of only checked when the mouse moves. I have tried NSTimer with something like this, but it does not seem to work (m_btHelp does not get disabled when g_bEnableHelpButton = NO; like it does in the mouseMoved event:
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:#selector(Timerloop) userInfo:nil repeats:YES];
}
- (void)Timerloop
{
if(g_bEnableHelpButton) {
[m_btHelp setEnabled:YES];
} else {
[m_btHelp setEnabled:NO];
}
}
g_bEnableHelpButton is a global variable, right? Don't use global variables. It's way better to create a class which holds your state (can be view model, ...). I'm going to skip a state class in all examples below and will use BOOL helpButtonEnabled property on the same view controller (this is not mandatory, it just makes all these examples a bit shorter). You can move this property elsewhere, it can be a state class, it can be basically any object.
Another thing is this NSTimer, NSTrackingArea, ... With all these things one is wasting CPU cycles, battery life, ... Cocoa & Objective-C offers various ways how to monitor a property value and react to it. You can override property setter, you can use KVO or bindings. All three methods are covered in examples below.
There are other ways for sure (like ReactiveCocoa), but I'd like to demonstrate three ways how to achieve it without dependencies.
Initial state
Imagine you have this view:
With the following implementation:
#import "ViewController.h"
#interface ViewController ()
// Help button from the Main.storyboard
// Imagine it's your m_btHelp
#property (nonatomic, strong) IBOutlet NSButton *helpButton;
// Property driving helpButton enabled/disabled state
// Imagine it's your g_bEnableHelpButton
#property (nonatomic, getter=isHelpButtonEnabled) BOOL helpButtonEnabled;
#end
#implementation ViewController
// Just another button action coming from the Main.storyboard which toggles
// our helpButtonEnabled property value
- (IBAction)toggleHelpButtonEnabled:(id)sender {
self.helpButtonEnabled = !self.helpButtonEnabled;
}
#end
There's help button and there's toggle button which just toggles helpButtonEnabled value (YES -> NO, NO -> YES).
How to monitor it without timer, tracking area, ... to update the help button state?
Override setter
Encapsulating Data.
#implementation ViewController
// This is setter for the helpButtonEnabled property.
- (void)setHelpButtonEnabled:(BOOL)helpButtonEnabled {
// If the new value equals, do nothing
if (helpButtonEnabled == _helpButtonEnabled) {
return;
}
// Update instance variable
_helpButtonEnabled = helpButtonEnabled;
// Update button state
_helpButton.enabled = helpButtonEnabled;
}
- (void)viewDidLoad {
[super viewDidLoad];
// When the view loads update button state to the initial value
_helpButton.enabled = _helpButtonEnabled;
}
// Just another button action coming from the Main.storyboard which toggles
// our helpButtonEnabled property value
- (IBAction)toggleHelpButtonEnabled:(id)sender {
self.helpButtonEnabled = !self.helpButtonEnabled;
}
#end
KVO
Introduction to Key-Value Observing Programming Guide.
static void * const ViewControllerHelpButtonEnabledContext = (void*)&ViewControllerHelpButtonEnabledContext;
#implementation ViewController
- (void)dealloc {
// Remove previously registered observer when the view controller goes away
[self removeObserver:self forKeyPath:#"helpButtonEnabled" context:ViewControllerHelpButtonEnabledContext];
}
- (void)viewDidLoad {
[super viewDidLoad];
// Register observer for the helpButtonEnabled key path
// - it fires immeditately with the current value (NSKeyValueObservingOptionInitial)
// - it fires later every single time new value is assigned (NSKeyValueObservingOptionNew)
// - context is used to quickly distinguish why the observeValueForKeyPath:... was called
[self addObserver:self
forKeyPath:#"helpButtonEnabled"
options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew
context:ViewControllerHelpButtonEnabledContext];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if (context == ViewControllerHelpButtonEnabledContext) {
// It's our observer, let's update button state
_helpButton.enabled = _helpButtonEnabled;
} else {
// It's not our observer, just forward it to super implementation
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
// Just another button action coming from the Main.storyboard which toggles
// our helpButtonEnabled property value
- (IBAction)toggleHelpButtonEnabled:(id)sender {
self.helpButtonEnabled = !self.helpButtonEnabled;
}
#end
Binding
Introduction to Cocoa Bindings Programming Topics.
#implementation ViewController
- (void)dealloc {
// Remove binding when the view controller goes away
[self.helpButton unbind:NSEnabledBinding];
}
- (void)viewDidLoad {
[super viewDidLoad];
// self.helpButton.enabled is binded to self.helpButtonEnabled
[self.helpButton bind:NSEnabledBinding
toObject:self
withKeyPath:#"helpButtonEnabled"
options:nil];
}
// Just another button action coming from the Main.storyboard which toggles
// our helpButtonEnabled property value
- (IBAction)toggleHelpButtonEnabled:(id)sender {
self.helpButtonEnabled = !self.helpButtonEnabled;
}
#end

Presenting modal dialogs from XIB in Cocoa: best/shortest pattern?

Below is my typical WindowController module for presenting a modal dialog (could be settings, asking username/password, etc) loaded from a XIB. It seems a bit too complex for something like this. Any ideas how this can be done better/with less code?
Never mind that it's asking for a password, it could be anything. What frustrates me most is that I repeat the same pattern in each and every of my XIB-based modal window modules. Which of course means I could define a custom window controller class, but before doing that I need to make sure this is really the best way of doing things.
#import "MyPasswordWindowController.h"
static MyPasswordWindowController* windowController;
#interface MyPasswordWindowController ()
#property (weak) IBOutlet NSSecureTextField *passwordField;
#end
#implementation MyPasswordWindowController
{
NSInteger _dialogCode;
}
- (id)init
{
return [super initWithWindowNibName:#"MyPassword"];
}
- (void)awakeFromNib
{
[super awakeFromNib];
[self.window center];
}
- (void)windowWillClose:(NSNotification*)notification
{
[NSApp stopModalWithCode:_dialogCode];
_dialogCode = 0;
}
- (IBAction)okButtonAction:(NSButton *)sender
{
_dialogCode = 1;
[self.window close];
}
- (IBAction)cancelButtonAction:(NSButton *)sender
{
[self.window close];
}
+ (NSString*)run
{
if (!windowController)
windowController = [MyPasswordWindowController new];
[windowController loadWindow];
windowController.passwordField.stringValue = #"";
if ([NSApp runModalForWindow:windowController.window])
return windowController.passwordField.stringValue;
return nil;
}
The application calls [MyPasswordWindowController run], so from the point of view of the user of this module it looks simple, but not so much when you look inside.
Set tags on your buttons to distinguish them. Have them both target the same action method:
- (IBAction) buttonAction:(NSButton*)sender
{
[NSApp stopModalWithCode:[sender tag]];
[self.window close];
}
Get rid of your _dialogCode instance variable and -windowWillClose: method.
-[NSApplication runModalForWindow:] will already center the window, so you can get rid of your -awakeFromNib method.
Get rid of the invocation of -[NSWindowController loadWindow]. That's an override point. You're not supposed to call it. The documentation is clear on that point. It will be called automatically when you request the window controller's -window.
Get rid of the static instance of MyPasswordWindowController. Just allocate a new one each time. There's no point in keeping the old one around and it can be troublesome to reuse windows.

How to Display and Manage a Simple Application-Modal Dialog in Cocoa

I am not sure I am doing things the right way, or if I have it all hacked up.
I have a really simple test application (not document-based) I created for learning how to work with application-modal dialogs in Cocoa applications.
With the application project "TestModalDialog", I have a simple MainMenu.xib with the default view and a button, "Show Dialog", I added. I created a second XIB called TheDialog.xib that has "Cancel" and "OK" buttons. That xib has as its owner a class derived from NSWindowController called "TheDialogController"; the window outlet and delegate are connected to the controller.
Selection of "Show Dialog" on the main view will launch the dialog. Selection of "Cancel" or "OK" will dismiss the dialog. Here is the pretty simple code:
// TestModalDialogAppDelegate.h
// TestModalDialog
#import <Cocoa/Cocoa.h>
#class TheDialogController;
#interface TestModalDialogAppDelegate : NSObject <NSApplicationDelegate>
{
NSWindow *window;
TheDialogController* theDialogController;
}
#property (assign) IBOutlet NSWindow *window;
- (IBAction)showDialog:(id)sender;
#end
// TestModalDialogAppDelegate.m
// TestModalDialog
#import "TestModalDialogAppDelegate.h"
#import "TheDialogController.h"
#implementation TestModalDialogAppDelegate
#synthesize window;
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
theDialogController= [[TheDialogController alloc] init];
}
- (void)dealloc
{
if(nil != theDialogController)
[theDialogController release];
[super dealloc];
}
- (IBAction)showDialog:(id)sender
{
if(nil == theDialogController)
{
NSAlert* alert= [NSAlert alertWithMessageText:#"Dialog Error" defaultButton:nil alternateButton:nil otherButton:nil informativeTextWithFormat:#"The dialog controller was not allocated."];
[alert runModal];
return;
}
NSInteger result= [NSApp runModalForWindow:[theDialogController window]];
// Do something with result....
}
#end
// TheDialogController.h
// TestModalDialog
#import <Cocoa/Cocoa.h>
#interface TheDialogController : NSWindowController
{
// BOOL userClickedCloseOrOk; // Removed based on answer.
// Should declare a common define - just being lazy.
NSInteger userClickedOk; // Added based on answer.
UInt16 timesShown;
}
- (IBAction)showWindow:(id)sender;
- (IBAction)closeDialog:(id)sender;
- (IBAction)okDialog:(id)sender;
//- (BOOL)windowShouldClose:(id)sender; // Removed based on answer.
- (void)windowWillClose:(NSNotification*)notification; // Added based on answer.
- (void)windowDidBecomeKey:(NSNotification*)notification; // To set title when modal.
#end
// TheDialogController.m
// TestModalDialog
#import "TheDialogController.h"
#implementation TheDialogController
- (id)init
{
self = [super initWithWindowNibName:#"TheDialog"];
userClickedOk= 0; // Added based on answer.
// userClickedCloseOrOk= FALSE; // Removed based on answer.
return self;
}
-(void)dealloc
{
// Do member cleanup if needed.
[super dealloc];
}
- (void)windowDidLoad
{
[super windowDidLoad];
// Initialize as needed....
[[self window] center]; // Center the window.
}
// Does not show with runModalForWindow.
- (IBAction)showWindow:(id)sender
{
// Just playing with the window title....
++timesShown;
NSString* newTitle= [NSString stringWithFormat:#"Shown %d Times", timesShown];
[[self window] setTitle:newTitle];
return [super showWindow:sender];
}
// This method no longer used for this solution based on the answer.
//- (BOOL)windowShouldClose:(id)sender
//{
// if(!userClickedCloseOrOk) // The user did not click one of our buttons.
// [NSApp abortModal];
// else
// userClickedCloseOrOk= FALSE; // Clear for next time.
//
// return TRUE;
//}
// Added based on answer.
- (void)windowWillClose:(NSNotification*)notification
{
[NSApp stopModalWithCode:userClickedOk];
userClickedOk= 0; // Reset for next time.
}
// Note - the title will update every time the window becomes key. To do the
// update only once per modal session, a flag can be added. There might be a better
// notification to catch.
- (void)windowDidBecomeKey:(NSNotification*)notification
{
++timesShown;
NSString* newTitle= [NSString stringWithFormat:#"Shown %d Times", timesShown];
[[self window] setTitle:newTitle];
}
- (IBAction)closeDialog:(id)sender
{
//userClickedCloseOrOk= TRUE; // Removed based on answer.
//[NSApp abortModal]; // Removed based on answer.
//[[self window] performClose:self]; // Removed based on answer.
[[self window] close]; // Know we want to close - based on answer.
}
- (IBAction)okDialog:(id)sender
{
userClickedOk= 1; // Added based on answer.
//userClickedCloseOrOk= TRUE; // Removed based on answer.
//[NSApp stopModal]; // Removed based on answer.
//[[self window] performClose:self]; // Removed based on answer.
[[self window] close]; // Know we want to close - based on answer.
}
#end
I had trouble with the modality - before I put in userClickedCloseOrOk and tests, if the user hit the close button (upper-left red dot), the dialog would close but the modal session was still running.
I realize I could just leave the close-button off the dialog to start with, but with it there, is what I have demonstrated a good way to catch that scenario, or is there a better way? Or, am I doing something wrong to start with, which creates that problem for me?
Any advice would be appreciated.
NOTE - Code from the original example commented out and replaced with code based on the answer. Also added a new notification handler.
In the okDialog: and closeDialog: methods call close instead of performClose: to avoid the window calling the windowShouldClose: delegate method. That way you don't need userClickedCloseOrOk.
Also I think you want to be calling stopModalWithCode: instead of stopModal since in the app delegate you seem to be interested in the result. And you can call stopModal or stopModalWithCode: instead of abortModal because you are always in the runloop when you call it (abort is for when you are outside of the modal runloop, like in another thread or timer's runloop).
In windowShouldClose: you are doing an action (abortModal) when you should only be answering the question "should this window close". The windowWillClose: delegate method is where you should do actions if you need to.
Sheets are useful when there is one window and it tells the user they can't do anything with it until they complete whatever is in the sheet. Application-modal windows are useful when you have multiple windows that the user can't interact with until they complete whatever is in the modal window or where there is an error that involves the whole application but is not tied to the content of one window. In their HIG Apple suggests avoiding the use of Application-modal windows whenever possible.
I have actually just been struggling with the same problem and found this link :
Stopping modal when window is closed (Cocoa)

Hide UISearchBar Cancel Button

I have a UISearchDisplayController and UISearchBar hooked up to my ViewController via Outlets from my nib.
I'd like to hide the cancel button so that the user never sees it. The problem is that the following code hides the button, but only after displaying it to the user for a millisecond (e.g., it flashes on the simulator and device and then disappears out of view).
- (void)searchDisplayControllerDidBeginSearch:(UISearchDisplayController *)controller
{
controller.searchBar.showsCancelButton = NO;
}
Is there a better way to hide it?
I managed to hide the "Cancel" button by subclassing UISearchBar and override this method:
-(void)layoutSubviews{
[super layoutSubviews];
[self setShowsCancelButton:NO animated:NO];
}
I had the same issue, but fixed it a different way.
For those who can't or don't want to subclass UISearchDisplayController, I fixed the issue by adding a listener on UIKeyboardWillShowNotification, and setting [self setShowsCancelButton:NO animated:NO] there.
In viewWillAppear::
// Add keyboard observer:
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(keyboardWillAppear:)
name:UIKeyboardWillShowNotification
object:nil];
Then you create:
- (void)keyboardWillAppear:(NSNotification *)notification
{
[YOUR-SEARCHBAR-HERE setShowsCancelButton:NO animated:NO];
}
Don't forget to add,
[[NSNotificationCenter defaultCenter] removeObserver:self];
in viewWillDisappear:!
Hope this helps!
Similar to Nimrod's answer, you can also subclass UISearchDisplayController and implement the setActive:animated: method:
- (void)setActive:(BOOL)visible animated:(BOOL)animated {
[super setActive:visible animated:animated];
self.searchBar.showsCancelButton = NO;
}
This seems to be a bug within Xcode. I submitted this error to Apple's bug reporting site, and they've followed up asking for more sample code and use-cases.
Thanks everyone for your attempt at solving this problem.
class CustomSearchBar: UISearchBar {
override func setShowsCancelButton(showsCancelButton: Bool, animated: Bool) {
super.setShowsCancelButton(false, animated: false)
}
}
class CustomSearchController: UISearchController, UISearchBarDelegate {
lazy var _searchBar: CustomSearchBar = {
[unowned self] in
let customSearchBar = CustomSearchBar(frame: CGRectZero)
customSearchBar.delegate = self
return customSearchBar
}()
override var searchBar: UISearchBar {
get {
return _searchBar
}
}
}
Had this problem when using the UISearchBar with UISearchController. I'm using my own cancel button, as the cancel button wasn't showing on iPad with showsCancelButton = YES, now it won't hide on iPhone with showsCancelButton = NO!
The following worked for me.
Set the delegate, and initial value:
- (void)viewDidLoad
{
// ...
self.searchController.searchBar.showsCancelButton = NO;
self.searchController.searchBar.delegate = self;
}
Reset showsCancelButton to NO 0.1s after the text bar begins editing.
#pragma mark - UISearchBarDelegate
- (void)searchBarTextDidBeginEditing:(UISearchBar *)searchBar
{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
self.searchController.searchBar.showsCancelButton = NO;
});
}
If you want to avoid the subclassing, implement
searchController.searchBar.showsCancelButton = false;
in these two delegate methods (Do not forget to assign delegates):
- (void)updateSearchResultsForSearchController:(UISearchController *)searchController
- (void)didPresentSearchController:(UISearchController *)searchController
The first one is called everytime you update the searchBar (Cancel button is visible by default) and the second one is for the first searchBar activation.
Just based on issues I've had before have you tried setting it in:
- (void)searchDisplayControllerWillBeginSearch:(UISearchDisplayController *)controller
I don't know how to ask this question in your question sorry if this is out of place.
If the cancel button shows up when editing the search field of the search bar you could do the following; subclass the search bar and have it implement the UITextFieldDelegateprotocol:
#interface CustomAlignedSearchBar : UISearchBar<UITextFieldDelegate>
Then implement textFieldDidBeginEditing: and do something like:
- (void)textFieldDidBeginEditing:(UITextField *)textField{
[self setShowsCancelButton:self.cancelButtonShown animated:NO];
}
This will make sure that the cancel button will not show up.
After UISearchDisplayController deprecated in iOS8, Apple give handle search presentation to UISearchControllerDelegate.
so you can override searchBar to hide the Cancel button, like below :
- (void)didPresentSearchController:(UISearchController *)searchController {
[searchController.searchBar setShowsCancelButton:NO];
}
if you need hidden Cancel button from inactive state, you need set searchBar on init :
search = [[UISearchController alloc] initWithSearchResultsController:nil];
[search.searchBar setShowsCancelButton:NO];
On iOS 13.0 and later, UISearchController has this property you can use:
#property (nonatomic) BOOL automaticallyShowsCancelButton API_AVAILABLE(ios(13.0)); // Default YES

Display window again after closing it and setting Focus

I have two short questions:
How is it possible to reopen a window after closing it.
How do I set the focus on the window I open.
It would be nice if someone can help me!
Here is the source code I have so far but I don't know how to go on:
The method calling the WindowController:
- (IBAction)openPreferences:(id)sender
{
[NSApp activateIgnoringOtherApps:YES];
if (NULL == preferences)
{
preferences = [[PreferencesController alloc] initWithWindowNibName:#"Preferences"];
}
[preferences showPreferenceWindow];
}
This is the Header of PreferencesController:
#import <Foundation/Foundation.h>
#interface PreferencesController : NSWindowController <NSWindowDelegate>
- (void)showPreferenceWindow;
#end
This is the Main of PreferencesController:
#import "PreferencesController"
#interface PreferencesController()
#end
#implementation PreferencesController
- (void)windowWillClose:(NSNotification *)notification
{
}
// display the preference window
- (void)showPreferenceWindow
{
[self.window makeKeyAndOrderFront:NSApp];
// TODO: window should be focused and if the user press the close button it should be displayed again
}
- (void) dealloc
{
[super dealloc];
}
#end
NSWindow has this handy method - (void)setReleasedWhenClosed:(BOOL)releasedWhenClosed. Set that to NO and you window can open unlimited amounts of times. As to your focus issue: It should already be working to bringing your window to focus.
But since you are using a preference window: may I suggest DBPrefsWindowController it is pretty old but it still works today.