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.
Related
I try to use the new iOS 9.0 CNContactPickerViewController to select a contact in objective-C. I set the delegate and implement the CNContactPickerDelegate methods.
#import ContactsUI;
#import Contacts;
//-----------------------------------------------------------------------
- (void) presentContacts
{
CNContactPickerViewController *contactPicker = [[CNContactPickerViewController alloc] init];
contactPicker.delegate = self;
contactPicker.predicateForEnablingContact = [NSPredicate predicateWithFormat:#"familyName LIKE[cd] 'smith'"];
contactPicker.predicateForSelectionOfContact = [NSPredicate predicateWithFormat:#"TRUEPREDICATE"];
[_myViewController presentViewController:contactPicker animated:YES completion:nil];
}
//-----------------------------------------------------------------------
- (void) contactPickerDidCancel: (CNContactPickerViewController *) picker
{
NSLog(#"didCancel");
}
//-----------------------------------------------------------------------
- (void) contactPicker: (CNContactPickerViewController *) picker
didSelectContact: (CNContact *) contact
{
NSLog(#"didSelectContact"):
}
//-----------------------------------------------------------------------
- (void) contactPicker: (CNContactPickerViewController *) picker
didSelectContactProperty: (CNContactProperty *) contactProperty
{
NSLog(#"didSelectProperty");
}
//-----------------------------------------------------------------------
The contacts picker is presented with 'smith' selectable but I get the following message:
[CNUI ERROR] Selection predicates are set but the delegate does not implement contactPicker:didSelectContact: and contactPicker:didSelectContactProperty:. Those predicates will be ignored.
And I never get any log from the delegate methods. It behaves exactly as the line
contactPicker.delegate = self;
is ignored. Even I click on the "cancel" button in the picker, I don't get my "didCancel" message but I get another message:
plugin com.apple.MobileAddressBook.ContactsViewService invalidated
I found in https://forums.developer.apple.com/thread/12275 somebody with the similar problem in swift and he solved it telling us: "So I found that the ContactsPicker I was calling was in the wrong module" but I do not understand how we can get the wrong module and how to call the "right" module.
I have the same problem on the simulator and on a real device (iPad).
Thanks to Joel in my related question With CNContactPickerViewController in iOS 9.0, how to enable/disable single or multiple selection?, I found that I just forgot to store the CNContactPickerViewController in a property that exists the time the user make the selection.
So my code becomes:
- (void) presentContacts
{
_contactPicker = [[CNContactPickerViewController alloc] init];
contactPicker.delegate = self;
...
}
I have a subclass of webView that overrides - hitTest: The basic idea is that I want clicks on the webView to pass through to the nextResponder if the click was on the body DOM element. The method looks like this
- (NSView *)hitTest:(NSPoint)aPoint
{
NSDictionary *dict = [self elementAtPoint:aPoint];
if([[dict valueForKey:#"WebElementDOMNode"] isKindOfClass:[DOMHTMLBodyElement class]])
{
return (NSView *)[self nextResponder];
}
return [super hitTest:aPoint];
}
When run, it crashes on elementAtPoint with EXC_BAD_ACCESS code=2
Now, it gets weirder. If I breakpoint the app at that line, and do a po [self elementAtPoint:aPoint] in LLDB, LLDB just hangs until I do a ^C.
Weirder yet. If I comment out everything but the last return, break on the return statement, and run po [self elementAtPoint:aPoint] in LLDB—I get exactly what I expect, a nice dictionary telling me all about the DOM at that point.
What could be causing this behavior?
Note: This is on OS X, not iOS.
What could be causing this behavior?
Just look at [WebView _elementAtWindowPoint:] and [WebView _frameViewAtWindowPoint:] implementations in the WebKit source code. Unfortunately, they use hitTest: to determine elementAtPoint:.
In my case, this workaround seems to work:
- (NSView *)hitTest:(NSPoint)point
{
if (m_hitTestEnabled)
{
[NSTimer scheduledTimerWithTimeInterval:0.
target:self
selector:#selector(hitTestDelayed:)
userInfo:#[ #(point.x), #(point.y) ]
repeats:NO];
}
return [super hitTest:point];
}
- (void)hitTestDelayed:(NSTimer *)timer
{
NSPoint point = NSMakePoint([[timer userInfo][0] floatValue], [[timer userInfo][1] floatValue]);
m_hitTestEnabled = false;
NSDictionary *dict = [self elementAtPoint:point];
m_hitTestEnabled = true;
if ([[dict valueForKey:#"WebElementDOMNode"] isKindOfClass:[DOMHTMLDivElement class]])
{
NSLog(#"divAtPoint: %#", dict);
}
}
m_hitTestEnabled is set to YES in the initWith... method.
Why with a timer? Such operations on WebView are allowed only in main thread. So, we cannot launch another thread to get elementAtPoint: and wait for its completion in the "main" hitTest:. Maybe someone will come up with a better solution.
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.
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.
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.