NSTextField autocomplete - objective-c

Does anyone know of any class or lib that can implement autocompletion to an NSTextField?
I'am trying to get the standard autocmpletion to work but it is made as a synchronous api. I get my autocompletion words via an api call over the internet.
What have i done so far is:
- (void)controlTextDidChange:(NSNotification *)obj
{
if([obj object] == self.searchField)
{
[self.spinner startAnimation:nil];
[self.wordcompletionStore completeString:self.searchField.stringValue];
if(self.doingAutocomplete)
return;
else
{
self.doingAutocomplete = YES;
[[[obj userInfo] objectForKey:#"NSFieldEditor"] complete:nil];
}
}
}
When my store is done, i have a delegate that gets called:
- (void) completionStore:(WordcompletionStore *)store didFinishWithWords:(NSArray *)arrayOfWords
{
[self.spinner stopAnimation:nil];
self.completions = arrayOfWords;
self.doingAutocomplete = NO;
}
The code that returns the completion list to the nstextfield is:
- (NSArray *)control:(NSControl *)control textView:(NSTextView *)textView completions:(NSArray *)words forPartialWordRange:(NSRange)charRange indexOfSelectedItem:(NSInteger *)index
{
*index = -1;
return self.completions;
}
My problem is that this will always be 1 request behind and the completion list only shows on every 2nd char the user inputs.
I have tried searching google and SO like a mad man but i cant seem to find any solutions..
Any help is much appreciated.

Instead of having the boolean property doingAutocomplete, make the property your control that made the request. Let's call it autoCompleteRequestor:
#property (strong) NSControl* autoCompleteRequestor;
So where you set your current property doingAutocomplete to YES, instead store a reference to your control.
- (void)controlTextDidChange:(NSNotification *)obj
{
if([obj object] == self.searchField)
{
[self.spinner startAnimation:nil];
[self.wordcompletionStore completeString:self.searchField.stringValue];
if(self.autoCompleteRequestor)
return;
else
{
self.autoCompleteRequestor = [[obj userInfo] objectForKey:#"NSFieldEditor"];
}
}
}
Now when your web request is done, you can call complete: on your stored object.
- (void) completionStore:(WordcompletionStore *)store didFinishWithWords:(NSArray *)arrayOfWords
{
[self.spinner stopAnimation:nil];
self.completions = arrayOfWords;
if (self.autoCompleteRequestor)
{
[self.autoCompleteRequestor complete:nil];
self.autoCompleteRequestor = nil;
}
}

NSTextView has the functionality of completing words of partial words.
Take a look at the documentation for this component.
Maybe you can switch to this component in your application.

Related

Click on map pin for detail view gives EXC_BAD_ACCESS

I have implemented calloutAccessoryControlTapped like this:
- (void)mapView:(MKMapView *)mapView annotationView:(MKAnnotationView *)view
calloutAccessoryControlTapped:(UIControl *)control
{
NSLog(#"calloutAccessoryControlTapped: annotation = %#", view.annotation.title);
if([_delegate respondsToSelector:#selector(mapView:didClickPinButton:)])
{
NSString *titleStr = [(DisplayMap *)view.annotation title];
[_delegate performSelector:#selector(mapView:didClickPinButton:)withObject:titleStr];
}
}
When I click on pin's button, it goes to method mapView:didClickPinButton on main view controller and shows an error:
Thread 1:EXC_BAD_ACCESS (Code=1,address=0x......)
This is the mapView:didClickPinButton: method:
- (void) mapView: (MapView *) mapView didClickPinButton:(NSString *) title
{
NSLog(#"bsn name-->%#",title);
for(int i=0;i<[self.categoryListArray count];i++)
{
NSMutableDictionary *couponDetail = [self.categoryListArray objectAtIndex:i];
if([[couponDetail objectForKey:#"category_name"] isEqualToString:title])
{
NSNumber *coupon_type=[couponDetail objectForKey:#"coupon_type"];
int c_type = [coupon_type intValue];
if(c_type == 1)
{
[self callServiceForCouponTypeOneBlock:[couponDetail objectForKey:#"id"] ];
break;
}
else if(c_type == 2)
{
BusinessViewController *businessVC = [[BusinessViewController alloc]initWithNibName:#"BusinessViewController" bundle:nil];
[businessVC setCouponListDetail:couponDetail];
[self.navigationController pushViewController:businessVC animated:YES];
break;
}
}
}
The problem is this line:
[_delegate performSelector:#selector(mapView:didClickPinButton:)withObject:titleStr];
The performSelector:withObject: method can only be used to call methods that have one object argument but your mapView:didClickPinButton: method has two object arguments.
You could instead use performSelector:withObject:withObject: which lets you call a method with two object arguments like this:
[_delegate performSelector:#selector(mapView:didClickPinButton:)
withObject:mapView withObject:titleStr];
However, using performSelector here doesn't seem to be really necessary.
It's much easier and simpler to just call the method directly on the delegate so it won't matter how many arguments the method has:
[_delegate mapView:mapView didClickPinButton:titleStr];

Changing cocoa bindings value programmatically

I have an NSButton in my preferences to interact with adding the application to the LoginItems. If the adding of the login item fails, I want to uncheck the box so the user doesn't get a false sense that it was added to the login items. However, after doing this, when I click the checkbox again, the bindings is not triggered.
- (void)addLoginItem:(BOOL)status
{
NSURL *url = [[[NSBundle mainBundle] bundleURL] URLByAppendingPathComponent:
#"Contents/Library/LoginItems/HelperApp.app"];
// Registering helper app
if (LSRegisterURL((__bridge CFURLRef)url, true) != noErr) {
NSLog(#"LSRegisterURL failed!");
}
if (!SMLoginItemSetEnabled((__bridge CFStringRef)[[NSBundle mainBundle] bundleIdentifier], (status) ? true : false)) {
NSLog(#"SMLoginItemSetEnabled failed!");
[self willChangeValueForKey:#"startAtLogin"];
[self.startAtLogin setValue:[NSNumber numberWithBool:[self automaticStartup]] forKey:#"state"];
[self didChangeValueForKey:#"startAtLogin"];
}
}
- (void)setAutomaticStartup:(BOOL)state
{
NSLog(#"Set automatic startup: %d", state);
if ([self respondsToSelector:#selector(addLoginItem:)]) {
[self addLoginItem:state];
}
}
- (BOOL)automaticStartup
{
BOOL isEnabled = NO;
// the easy and sane method (SMJobCopyDictionary) can pose problems when sandboxed. -_-
CFArrayRef cfJobDicts = SMCopyAllJobDictionaries(kSMDomainUserLaunchd);
NSArray* jobDicts = CFBridgingRelease(cfJobDicts);
if (jobDicts && [jobDicts count] > 0) {
for (NSDictionary* job in jobDicts) {
if ([[[NSBundle mainBundle] bundleIdentifier] isEqualToString:[job objectForKey:#"Label"]]) {
isEnabled = [[job objectForKey:#"OnDemand"] boolValue];
break;
}
}
}
NSLog(#"Is Enabled: %d", isEnabled);
// if (isEnabled != _enabled) {
[self willChangeValueForKey:#"startupEnabled"];
startupEnabled = isEnabled;
[self didChangeValueForKey:#"startupEnabled"];
// }
return isEnabled;
}
I have my databinding for the checkbox bound to self.automaticStartup. If I remove the line [self.startAtLogin setValue:[NSNumber numberWithBool:[self automaticStartup]] forKey:#"state"]; then the bindings work fine, but it doesn't uncheck, if the adding of the item fails.
How can I change this binding value programmatically so that every other binding event is not ignored?
From your explanation, your bound value is automaticStartup, but you are sending willChangeValueForKey: for startAtLogin. In order for bindings to work correctly, you need to alert on the change to the bound variable at some point. However, since you are in the midst of setAutomaticStartup: at the time, it's not really safe to do that here.
In this case, I would not use bindings to perform the change itself, I would consider the old-style IBAction mechanism and then set the checkbox value manually through an IBOutlet when you can confirm the status.

How to subclass NSDocumentController to only allow one doc at a time

I'm trying to create a Core Data, document based app but with the limitation that only one document can be viewed at a time (it's an audio app and wouldn't make sense for a lot of docs to be making noise at once).
My plan was to subclass NSDocumentController in a way that doesn't require linking it up to any of the menu's actions. This has been going reasonably but I've run into a problem that's making me question my approach a little.
The below code works for the most part except if a user does the following:
- Tries to open a doc with an existing 'dirty' doc open
- Clicks cancel on the save/dont save/cancel alert (this works ok)
- Then tries to open a doc again. For some reason now the openDocumentWithContentsOfURL method never gets called again, even though the open dialog appears.
Can anyone help me work out why? Or perhaps point me to an example of how to do this right? It feels like something that must have been implemented by a few people but I've not been able to find a 10.7+ example.
- (BOOL)presentError:(NSError *)error
{
if([error.domain isEqualToString:DOCS_ERROR_DOMAIN] && error.code == MULTIPLE_DOCS_ERROR_CODE)
return NO;
else
return [super presentError:error];
}
- (id)openUntitledDocumentAndDisplay:(BOOL)displayDocument error:(NSError **)outError
{
if(self.currentDocument) {
[self closeAllDocumentsWithDelegate:self
didCloseAllSelector:#selector(openUntitledDocumentAndDisplayIfClosedAll: didCloseAll: contextInfo:)
contextInfo:nil];
NSMutableDictionary* details = [NSMutableDictionary dictionary];
[details setValue:#"Suppressed multiple documents" forKey:NSLocalizedDescriptionKey];
*outError = [NSError errorWithDomain:DOCS_ERROR_DOMAIN code:MULTIPLE_DOCS_ERROR_CODE userInfo:details];
return nil;
}
return [super openUntitledDocumentAndDisplay:displayDocument error:outError];
}
- (void)openUntitledDocumentAndDisplayIfClosedAll:(NSDocumentController *)docController
didCloseAll: (BOOL)didCloseAll
contextInfo:(void *)contextInfo
{
if(self.currentDocument == nil)
[super openUntitledDocumentAndDisplay:YES error:nil];
}
- (void)openDocumentWithContentsOfURL:(NSURL *)url
display:(BOOL)displayDocument
completionHandler:(void (^)(NSDocument *document, BOOL documentWasAlreadyOpen, NSError *error))completionHandler NS_AVAILABLE_MAC(10_7)
{
NSLog(#"%s", __func__);
if(self.currentDocument) {
NSDictionary *info = [NSDictionary dictionaryWithObjectsAndKeys:[url copy], #"url",
[completionHandler copy], #"completionHandler",
nil];
[self closeAllDocumentsWithDelegate:self
didCloseAllSelector:#selector(openDocumentWithContentsOfURLIfClosedAll:didCloseAll:contextInfo:)
contextInfo:(__bridge_retained void *)(info)];
} else {
[super openDocumentWithContentsOfURL:url display:displayDocument completionHandler:completionHandler];
}
}
- (void)openDocumentWithContentsOfURLIfClosedAll:(NSDocumentController *)docController
didCloseAll: (BOOL)didCloseAll
contextInfo:(void *)contextInfo
{
NSDictionary *info = (__bridge NSDictionary *)contextInfo;
if(self.currentDocument == nil)
[super openDocumentWithContentsOfURL:[info objectForKey:#"url"] display:YES completionHandler:[info objectForKey:#"completionHandler"]];
}
There's a very informative exchange on Apple's cocoa-dev mailing list that describes what you have to do in order to subclass NSDocumentController for your purposes. The result is that an existing document is closed when a new one is opened.
Something else you might consider is to mute or stop playing a document when its window resigns main (i.e., sends NSWindowDidResignMainNotification to the window's delegate), if only to avoid forcing what might seem to be an artificial restriction on the user.
I know it's been a while, but in case it helps others....
I had what I think is a similar problem, and the solution was to call the completion handler when my custom DocumentController did not open the document, e.g.:
- (void)openDocumentWithContentsOfURL:(NSURL *)url display:(BOOL)displayDocument completionHandler:(void (^)(NSDocument * _Nullable, BOOL, NSError * _Nullable))completionHandler {
if (doOpenDocument) {
[super openDocumentWithContentsOfURL:url display:displayDocument completionHandler:completionHandler];
} else {
completionHandler(NULL, NO, NULL);
}
}
When I added the completionHandler(NULL, NO, NULL); it started working for more than a single shot.

ShareKit: Customizing text for different sharers (SHKActionSheet)

According to the official FAQ from ver.2 to customize your text/content depending on what sharer was selected by the user, you need:
subclass from SHKActionSheet and override
dismissWithClickedButtonIndex
set your new subclass name in
configurator (return it in (Class)SHKActionSheetSubclass;).
It doesn't work for me. But even more: I put
NSLog(#"%#", NSStringFromSelector(_cmd));
in (Class)SHKActionSheetSubclass to see if it's even got called. And it's NOT ;(( So ShareKit doesn't care about this config option...
Has anybody worked with this before?
thank you!
UPD1: I put some code here.
Here's how my subclass ITPShareKitActionSheet looks like. According to the docs I need to override dismissWithClickedButtonIndex:animated:, but to track if my class gets called I also override the actionSheetForItem::
+ (ITPShareKitActionSheet *)actionSheetForItem:(SHKItem *)item
{
NSLog(#"%#", NSStringFromSelector(_cmd));
ITPShareKitActionSheet *as = (ITPShareKitActionSheet *)[super actionSheetForItem:item];
return as;
}
- (void)dismissWithClickedButtonIndex:(NSInteger)buttonIndex animated:(BOOL)animate
{
NSLog(#"%#", NSStringFromSelector(_cmd));
NSString *sharersName = [self buttonTitleAtIndex:buttonIndex];
[self changeItemForService:sharersName];
[super dismissWithClickedButtonIndex:buttonIndex animated:animate];
}
And here's what I do in code to create an action sheet when user presses 'Share' button:
- (IBAction)shareButtonPressed:(id)sender
{
// Create the item to share
SHKItem *item = [SHKItem text:#"test share text"];
// Get the ShareKit action sheet
ITPShareKitActionSheet *actionSheet = [ITPShareKitActionSheet actionSheetForItem:item];
// Display the action sheet
[actionSheet showInView:self.view]; // showFromToolbar:self.navigationController.toolbar];
}
When I run this code, press 'Share' button and select any sharer I expect to get two lines in log:
actionSheetForItem: - custom action sheet got created
dismissWithClickedButtonIndex:animated: - custom mechanics to
process action sheet's pressed button got called.
But for some reason I get only the first line logged.
I was having the same issues but I've suddenly got it to call my Subclass successfully.
Firstly My Configurator is setup as so:
-(Class) SHKActionSheetSubclass{
return NSClassFromString(#"TBRSHKActionSheet");
}
Now My Subclass:
.h File
#import "SHKActionSheet.h"
#interface TBRSHKActionSheet : SHKActionSheet
#end
.m implementation override:
#import "TBRSHKActionSheet.h"
#import "SHKActionSheet.h"
#import "SHKShareMenu.h"
#import "SHK.h"
#import "SHKConfiguration.h"
#import "SHKSharer.h"
#import "SHKShareItemDelegate.h"
#implementation TBRSHKActionSheet
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code
}
return self;
}
+ (SHKActionSheet *)actionSheetForItem:(SHKItem *)i
{
NSLog(#"%#", NSStringFromSelector(_cmd));
SHKActionSheet *as = [self actionSheetForType:i.shareType];
as.item = i;
return as;
}
- (void)dismissWithClickedButtonIndex:(NSInteger)buttonIndex animated:(BOOL)animated
{
NSInteger numberOfSharers = (NSInteger) [sharers count];
// Sharers
if (buttonIndex >= 0 && buttonIndex < numberOfSharers)
{
bool doShare = YES;
SHKSharer* sharer = [[NSClassFromString([sharers objectAtIndex:buttonIndex]) alloc] init];
[sharer loadItem:item];
if (shareDelegate != nil && [shareDelegate respondsToSelector:#selector(aboutToShareItem:withSharer:)])
{
doShare = [shareDelegate aboutToShareItem:item withSharer:sharer];
}
if(doShare)
[sharer share];
}
// More
else if ([SHKCONFIG(showActionSheetMoreButton) boolValue] && buttonIndex == numberOfSharers)
{
SHKShareMenu *shareMenu = [[SHKCONFIG(SHKShareMenuSubclass) alloc] initWithStyle:UITableViewStyleGrouped];
shareMenu.shareDelegate = shareDelegate;
shareMenu.item = item;
[[SHK currentHelper] showViewController:shareMenu];
}
[super dismissWithClickedButtonIndex:buttonIndex animated:animated];
}
Finally on my implementation file I've not modified the call to SHKActionSheet as Vilem has suggested because of some dependancies that seemed to cause conflicts for me.
So this is my caller (straight from tutorial):
NSURL *url = [NSURL URLWithString:#"http://getsharekit.com"];
SHKItem *item = [SHKItem URL:url title:#"ShareKit is Awesome!" contentType:SHKURLContentTypeWebpage];
// Get the ShareKit action sheet
SHKActionSheet *actionSheet = [SHKActionSheet actionSheetForItem:item];
// ShareKit detects top view controller (the one intended to present ShareKit UI) automatically,
// but sometimes it may not find one. To be safe, set it explicitly
[SHK setRootViewController:self];
// Display the action sheet
[actionSheet showFromToolbar:self.navigationController.toolbar];
This Calls no problems for me.
edit: by far the best way to achieve this is to use SHKShareItemDelegate. More info is in ShareKit's FAQ.

Suppressing the text completion dropdown for an NSTextField

I'm trying to create the effect of an NSComboBox with completes == YES, no button, and numberOfVisibleItems == 0 (for an example, try filling in an Album or Artist in iTunes's Get Info window).
To accomplish this, I'm using an NSTextField control, which autocompletes on -controlTextDidChange: to call -[NSTextField complete:], which triggers the delegate method:
- (NSArray *)control:(NSControl *)control
textView:(NSTextView *)textView
completions:(NSArray *)words
forPartialWordRange:(NSRange)charRange
indexOfSelectedItem:(NSInteger *)index;
I've gotten this working correctly, the only problem being the side effect of a dropdown showing. I would like to suppress it, but I haven't seen a way to do this. I've scoured the documentation, Internet, and Stack Overflow, with no success.
I'd prefer a delegate method, but I'm open to subclassing, if that's the only way. I'm targeting Lion, in case it helps, so solutions don't need to be backward compatible.
To solve this, I had to think outside the box a little. Instead of using the built-in autocomplete mechanism, I built my own. This wasn't as tough as I had originally assumed it would be. My -controlTextDidChange: looks like so:
- (void)controlTextDidChange:(NSNotification *)note {
// Without using the isAutoCompleting flag, a loop would result, and the
// behavior gets unpredictable
if (!isAutoCompleting) {
isAutoCompleting = YES;
// Don't complete on a delete
if (userDeleted) {
userDeleted = NO;
} else {
NSTextField *control = [note object];
NSString *fieldName = [self fieldNameForTag:[control tag]];
NSTextView *textView = [[note userInfo] objectForKey:#"NSFieldEditor"];
NSString *typedText = [[textView.string copy] autorelease];
NSArray *completions = [self comboBoxValuesForField:fieldName
andPrefix:typedText];
if (completions.count >= 1) {
NSString *completion = [completions objectAtIndex:0];
NSRange difference = NSMakeRange(
typedText.length,
completion.length - typedText.length);
textView.string = completion;
[textView setSelectedRange:difference
affinity:NSSelectionAffinityUpstream
stillSelecting:NO];
}
}
isAutoCompleting = NO;
}
}
And then I implemented another delegate method I wasn't previously aware of (the missing piece of the puzzle, so to speak).
- (BOOL)control:(NSControl *)control
textView:(NSTextView *)textView doCommandBySelector:(SEL)commandSelector {
// Detect if the user deleted text
if (commandSelector == #selector(deleteBackward:)
|| commandSelector == #selector(deleteForward:)) {
userDeleted = YES;
}
return NO;
}
Update: Simplified and corrected solution
It now doesn't track the last string the user entered, instead detecting when the user deleted. This solves the problem in a direct, rather than roundabout, manner.