I am storing keyboard shortcuts represented as NSDictionary instances into user defaults and have some trouble representing “none” values. When my application starts I register some default shortcuts. Then the user may change keyboard shortcuts in application preferences. The keyboard setting widgets are bound directly to user defaults.
The user may click a special “clear” button that clears a keyboard shortcut, meaning that user does not wish to set a shorcut for given feature. When the keyboard shortcut is cleared, it’s set to nil. This is a problem, because setting the shortcut to nil in user defaults reverts the value to the one registered with registerDefaults:. (The objectForKey: method does not find given key in the app domain and therefore goes into the NSRegistrationDomain domain.)
How should I represent the “cleared” keyboard shortcuts so that objectForKey: returns nil? Do I have to play games with empty NSDictionary instances and other sentinel values, or is there a better way?
I ended up using this transformer:
#interface NullTransformer : NSValueTransformer {}
#end
#implementation NullTransformer
+ (BOOL) allowsReverseTransformation {
return YES;
}
- (id) transformedValue: (id) value {
return [value count] ? value : nil;
}
- (id) reverseTransformedValue: (id) value {
return value ? value : [NSDictionary dictionary];
}
#end
In other words, I use an empty dictionary to represent unset values and convert between this special value and nil using a custom transformer so that my classes don’t have to care. It’s not perfect, but seems like a tolerable solution.
Related
I have an NSPopupButton whose content is bound to an NSArray, let’s say the array is
#[
#"Option 1",
#"Option 2"
];
Its selected object is bound to User Defaults Controller, and is written to a preference file by the user defaults system.
In my code I check whether the preference is set to #"Option 1" or not, and perform actions accordingly.
This all worked well (though I did feel a little uneasy checking for what is essentially a UI value, but whatever...) until I needed to localize.
Because the value is the label, I’m having an issue.
If my user is in France, his preferences file will say #"L’option 1", which is not equal to #"Option 1". I need to abstract the presentation from the meaning and it's proving pretty difficult.
I split up the binding into two arrays, let's call them values and labels.
Let’s say they look like this:
values = #[
#"option_1",
#"option_2"
];
labels = #[
NSLocalizedString(#"Option 1", nil),
NSLocalizedString(#"Option 2", nil)
];
I’ve bound the NSPopUpButton’s Content binding to values and its Content Values binding to labels. However, the popup list is showing option_1 and option_2, it does not seem to want to use the labels array to label the items in the popup button.
How do I get the NSPopUpButton to use values internally and store that in the preferences file, but display labels to the user?
It doesn’t have to be architected this way, if you can think of a better solution. The point is I want to store and check one value, and have that value associated with a label that gets localized appropriately.
Cocoa bindings work very well with value transformers, because you can apply them directly in the bindings window, for example
#implementation LocalizeTransformer
+ (Class)transformedValueClass
{
return [NSArray class];
}
+ (BOOL)allowsReverseTransformation
{
return NO;
}
- (id)transformedValue:(id)value {
if (![value isKindOfClass:[NSArray class]]) return nil;
NSMutableArray *output = [NSMutableArray arrayWithCapacity:[value count]];
for (NSString *string in value) {
[output addObject:NSLocalizedString(string, nil)];
}
return [output copy];
}
#end
you have to register the transformer in awakeFromNib or better in +initialize
NSValueTransformer *localizeTransformer = [[LocalizeTransformer alloc] init];
[NSValueTransformer setValueTransformer:localizeTransformer
forName:#"LocalizeTransformer"];
then it appears in the popup menu of value transformers
Bind Selected Tag to your User Defaults Controller instead of Selected Object.
If the NSPopupButton choices are fixed add the NSMenuItems in Interface Builder and set their Tags. Otherwise bind an array of NSMenuItem, again with proper Tags.
Selected Index would also work but only until you change the order.
I'm working on a screensaver, so I'm supposed to use ScreenSaverDefaults instead of NSUserDefaults. I'd like to have my configure panel use bindings for its UI, but they need to be wired to ScreenSaverDefaults, and I can't see a way to do that; the only defaults controller available in IB (xCode) is the standard user defaults controller. Is there a workaround, or is it just not possible to use bindings in the nib in the context of a screensaver?
I have successfully used bindings in a screen saver.
(Possibly important note: I'm still developing on 10.6, with XC 3.2.6; this code has only been lightly tested so far on 10.10 (but does seem to work). Also, I mix C++ and Objective-C, so you may have minor code cleanup to do, in what follows.)
My code creates, and the UI binds to, a custom user defaults controller (controls bind to File's Owner, which is set to my screen saver view, the controller key is empty, and the model key path is self.defaultsController.values.key_val, where key_val is whatever key is used in the defaults plist to access the value bound to a control).
You must also create your own screen saver defaults, and direct your custom user defaults controller to use them, early in screen saver initialization (such as in the initWithFrame: method of your screen saver view), like so:
// Find our bundle:
NSBundle * screenSaverBundle = [NSBundle bundleForClass: [self class]];
// Load per-user screen saver defaults:
ScreenSaverDefaults * screenSaverDefaults = [ScreenSaverDefaults defaultsForModuleWithName: [screenSaverBundle bundleIdentifier]];
// Create default defaults (values to use when no other values have been established):
NSDictionary * defaultDefaultValues = [self defaultDefaults];
// Register default defaults as fallback values (in registration domain):
[screenSaverDefaults registerDefaults: defaultDefaultValues];
// Configure custom user defaults controller to use the correct defaults:
// NOTE: defaultsController of NSUserDefaultsController * type *MUST* be declared using #property in your header, and accessors must be provided (typically using #synthesize in the implementation section).
defaultsController = [[NSUserDefaultsController alloc] initWithDefaults: screenSaverDefaults initialValues: nil]; // <- change nil to factory defaults, if desired
// Make sure changes are only saved when committed by user:
[defaultsController setAppliesImmediately: false];
(The above code was slightly rearranged from various methods in my own code; might have a typo or two, but the gist is correct.)
An implementation of defaultDefaults looks something like this (pardon my unconventional style):
- (NSDictionary *) defaultDefaults
{
NSString * ResourcesPath = [[self screenSaverBundle] resourcePath];
NSString * DefaultDefaultsPath = [ResourcesPath stringByAppendingPathComponent: #"DefaultDefaults.plist"];
NSDictionary * DefaultDefaults = [NSDictionary dictionaryWithContentsOfFile: DefaultDefaultsPath];
return DefaultDefaults;
}
If you want to provide a "factory reset" capability in your screen saver, you'll need to set the initialValues: argument to an "original factory values" dictionary when creating the custom user defaults controller. In response to a "Reset to Factory Defaults" button, simply send revertToInitialValues: to the controller.
Finally, please note that depending on the intended lifetime of some of the objects created in the above code, you may need to retain some of them (and release them properly later). I assume you understand this bhaller; I'm just pointing this out for the general audience.
Swift Version:
First, register default datas.
let defaults = ScreenSaverDefaults.init(forModuleWithName: "com.your.xxxx" ?? "")!
defaults.register(defaults: ["isShowSecond":true,
"userThemeIndex":0,
"userClockIndex":0
])
You can set value:
defaults.set(false, forKey: "isShowSecond")
You can get value:
let defaults = ScreenSaverDefaults.init(forModuleWithName: "com.your.xxxx" ?? "")!
let userClockIndex = defaults.bool(forKey: "isShowSecond")
How do I prevent a window title from displaying "Edited" for an NSDocument which is dirty?
I'm managing saving and autosaving myself, using a web service, and just don't want the distraction in the title bar.
I've tried overriding:
NSDocument's -isDocumentEdited and -hasUnautosavedChanges always to return NO.
-[NSWindowController setDocumentEdited] to do nothing, or always to use NO regardless of the parameter's actual value.
-[NSWindowController synchronizeWindowTitleWithDocumentName] to do nothing.
-[NSWindow setDocumentEdited] to do nothing, or always to use NO regardless of the parameter's actual value.
In all cases, the title bar still changes to Edited when I make changes to a saved document.
If I override -[NSDocument updateChangeCount:] and -[NSDocument updateChangeCountWithToken:forSaveOperation:] to do nothing, I can prevent this from happening, but it affects saving, autosaving, and other document behaviors, too.
I also tried this:
[[self.window standardWindowButton: NSWindowDocumentVersionsButton] setTitle:nil];
That displayed a blank string instead of Edited, but the dash still appeared – the one which normally separates the document name and Edited.
Any idea how to pry apart this part of the window from the document?
Several options:
To get a pointer to the "dash", look for a TextField in [window.contentView.superview.subviews] with a stringValue equals to "-". You can set its text to an empty string as well.
#implementation NSWindow (DashRetrivalMethod)
- (NSTextField*)versionsDashTextField
{
NSTextField* res = nil;
NSView* themeFrame = [self.contentView superview];
for (NSView* tmp in [themeFrame subviews])
{
if ([tmp isKindOfClass:[NSTextField class]])
{
if ([[(NSTextField*)tmp stringValue] isEqualToString:#"—"])
{
res = (NSTextField*)tmp;
break;
}
}
}
return res;
}
#end
You can override NSWindow's -setRepresentedURL:. This would also affect the NSWindowDocumentIconButton and the popup menu, but you can manually create it if you want by: [NSWindow standardWindowButton: NSWindowDocumentIconButton].
Override one of these three NSDocument's undocumented methods:
// Always return here NO if you don't want the version button to appear.
// This seems to be the cleanest options, besides the fact that you are
/// overriding a private method.
- (BOOL)_shouldShowAutosaveButtonForWindow:(NSWindow*)window;
// Call super with NO
- (void)_setShowAutosaveButton:(BOOL)flag;
// Here the button and the dash are actually created
- (void)_endVersionsButtonUpdates;
// Here Cocoa hide or unhide the edited button
- (void)_updateDocumentEditedAndAnimate:(BOOL)flag
Have you tried overriding NSDocuments - (BOOL)hasUnautosavedChanges in addition to overriding - (BOOL) isDocumentEdited?
Although this is a late answer, you can easily determine what is going to be the title of your NSDocument window by overriding
- (NSString *)windowTitleForDocumentDisplayName:(NSString *)displayName
in your NSWindowController and return the appropriate title.
You can do that also by overriding the property of your NSDocument:
- (NSString *)displayName
but this is not recommended by Apple, because that is normally used by the OS error handlers.
I added this answer, because none of the other answers really set me on the right path.
I tried a very simple implementation, like this:
#implementation ScrollingTextField
- (void)scrollWheel:(NSEvent *)event {
self.doubleValue -= event.scrollingDeltaY;
}
#end
I bound the value of the scrolling text field to some other object. Scrolling now updates the visible text on the text field just fine. However, the bound value does not change.
Why does the bound value not change?
Or: How can I make the bound value recognize the change?
The bound value doesn't change by Apple's design. To propagate the value to the model yourself after a change, adapt this code:
NSDictionary *bindingInfo = [self infoForBinding:NSValueBinding];
[[bindingInfo valueForKey:NSObservedObjectKey] setValue:self.doubleValue
forKeyPath:[bindingInfo valueForKey:NSObservedKeyPathKey]];
(Thanks #DrummerB for that Apple link!)
In my mac app, [Model m] is a custom object with a synthesized BOOL property and ivar rollAnimations. And animationsItem is an NSMenuItem object. I want to bind the state of my animationsItem to the rollAnimations property and ivar of [Model m]. A two-way binding would be ideal (so that changing either property changes the other), but if that's messy (retain cycles and such), I'll settle for a one-way binding, such that changing the menu item changes the rollAnimations property.
Here is a code snippet. It's not working. What am I missing?
NSMutableDictionary *bindingOptions = [NSMutableDictionary dictionary];
NSNumber *yesNumber = [NSNumber numberWithBool:YES];
[bindingOptions setObject:yesNumber forKey:NSValidatesImmediatelyBindingOption];
[animationsItem bind:#"state" toObject:[Model m] withKeyPath:#"rollAnimations" options:bindingOptions];
The Cocoa Bindings Reference lists all the bindings a menu item supports. The one you want is #"value", not #"state". (This goes for buttons, too, by the way.)