How to disable drag-n-drop for NSTextField? - cocoa

I want to disallow dropping anything into my NSTextField. In my app, users can drag and drop iCal events into a different part of the GUI. Now I've had a test user who accidentally dropped the iCal event into the text field – but he didn't realize this because the text is inserted in the lines above the one that I see in my one-line text field.
(You can reveal the inserted text by clicking into the text field and using the keyboard to go one line up – but a normal user wouldn't do this because he/she wouldn't even have realized that something got inserted in the first place!)
I tried registerForDraggedTypes:[NSArray array]] (doesn't seem to have any effect) as well as implementing the draggingEntered: delegate method returning NSDragOperationNone (the delegate method isn't even invoked).
Any ideas?
EDIT: Of course dropping something onto an NSTextField only works when it has focus, as described by ssp in his blog and in the comments to a blog entry by Daniel Jalkut.

I am glad you discovered the comments in my blog post. I think they are the tip of the iceberg to discovering how to achieve what you're looking for.
You need to keep in mind that the reason dragging to an NSTextField works when it has focus, is that the NSTextField has itself been temporarily obscured by a richer, more powerful view (an NSTextView), which is called the "Field Editor."
Check out this section of Apple's documentation on the field editor:
https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/TextEditing/Tasks/FieldEditor.html
To achieve what you're striving for, I think you might need to intercept the standard provision of the field editor for your NSTextFields, by implementing the window delegate method:
windowWillReturnFieldEditor:toObject:
This gives you the opportunity to either tweak the configuration on the NSTextView, or provide a completely new field editor object.
In the worst case scenario, you could provide your own NSTextView subclass as the field editor, which was designed to reject all drags.

This might work: If you subclass NSTextView and implement -acceptableDragTypes to return nil, then the text view will be disabled as a drag destination. I also had to implement the NSDraggingDestination methods -draggingEntered: and -draggingUpdated: to return NSDragOperationNone.
#implementation NoDragTextView
- (NSArray *)acceptableDragTypes
{
return nil;
}
- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender
{
return NSDragOperationNone;
}
- (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)sender
{
return NSDragOperationNone;
}
#end

I was able to solve this problem by creating a custom NSTextView and implementing the enter and exit NSDraggingDestination protocol methods to set the NSTextView to hidden. Once the text field is hidden the superview will be able to catch the drag/drop events, or if the superview doesn't implement or want the drag/drop they are discarded
For example:
- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender {
//hide so that the drop event falls through into superview's drag/drop view
[self setHidden:YES];
return NSDragOperationNone;
}
- (void)draggingExited:(id<NSDraggingInfo>)sender {
//show our field editor again since dragging is all over with
[self setHidden:NO];
}

Have you tried - (void)unregisterDraggedTypes from NSView?

Related

Receive trigger when active NSView focus changes

I'm surprised that I haven't been able to find an answer for this from searching. So if there is a page describing how to do this, let me know, but I've been unable to find it.
I have 3 sibling NSOutlineViews all inside an NSSplitView I have added observers for NSOutlineViewSelectionDidChange which, when triggered update an NSTextView editor. I even test for negative row values to indicate that the user has unselected the row and clear the text.
The NSOutlineViews are connected to custom datasource objects and the NSSplitView is created by my custom NSWindowController in turn created by my custom NSDocument and custom NSDocumentController (I'm not using .nibs)
However I cannot seem to receive triggers for the NSOutlineViews changing their active status, it works if the user selects a different row in a different NSOutlineView, as the selection has changed but if they click the selected row of a different view, I don't receive any event or notification that anything has changed. Visually I can see the change as the row selection highlight colour changes from coloured to grey, in the outline view that has lost focus and the selection row colour changes from grey to coloured in the newly activated view.
I've tried to catch mouseDown events, tried becoming first responder, tried observing changes in the NSSplitView I've been right through the NSObject hierarchy from NSOutlineView to NSResponder looking for the appropriate notification or method. I found deprecated documentation regarding a focus change notification. I've tried combinations of nsview, nsnotification, nsoutlineview and various actions in google but can't find the 'this is how you do it'
EDIT:
this is the code I've added to my NSOutlineView subclass (along with prototypes in the headers) to become first responder, but it is never triggered.
- (BOOL)acceptsFirstResponder { return YES; }
- (BOOL)becomeFirstResponder {
NSLog(#"becomefirstResponder %#",self);
return YES;
}

NSTextField - notifications when individual keys are pressed

I am making an app that will add sound to keypresses as the user types in an NSTextField. I need to capture keystrokes and know what each individual keypress is (like "d" or "space" or "6"). The app depends on this. There is no other way around it.
Each window is an NSDocument File Owner, and it has a single NSTextField in it, which is where the document data is parsed, and the user will type.
After hours of parsing the Internet for answers and hacking away at code, the four most commonly repeated answers are:
"that is not how things work, here is (irrelevant answer)"
"you are new to Cocoa, that is a bad idea, use control:textView:doCommandSelector:" that doesn't give me individual keys, and some keys need their own unique sound trigger.
"use controlTextDidChange: or textView:shouldChangeTextInRange:replaceString:" controlTextDidChange doesn't give me individual keys, and the second one only works for textViews or UIKit.
People get confused and answer with recommendations for UIKit instead of AppKit, which is iOS-only.
The weird thing is that if I subclass NSTextField, it receives -keyUp. I don't know where -keyDown is going.
So my ultimate question is: can you tell me some kind of step-by-step way to actually capture the keyDown that is sent to NSTextField? Even if it's a hack. Even if it's a terrible idea.
I would love to solve this problem! I am very grateful for your reading.
controlTextDidChange is quite a good solution, but don't forget this 2 important things:
Set the delegate binding of the textField to the object where you define the controlTextDidChange method. Commonly, in document based apps it is the window controller, otherwise your app delegate.
Set the textField's control to "continous" in the attribute inspector section
If you miss those points, you will have no result.
This is a pretty old question, but as I was trying to implement a NSTextField that could react to keyDown so that I could create a hotkey preferences control I found I wanted the answer to this question.
Unfortunately this is a pretty non-standard use and I didn't find any places that had a direct answer, but I've come up with something that works after digging through the documentation (albeit in Swift 4) and I wanted to post it here in case it helps someone else with a non-standard use case.
This is largely based off of the information gleaned from the Cocoa Text Architecture Guide
There are three components to my solution:
Creating your NSWindowController and setting a NSWindowDelegate on your NSWindow:
guard let windowController = storyboard.instanciateController(withIdentifier:NSStoryboard.SceneIdentifier("SomeSceneIdentifier")) as? NSWindowController else {
fatalError("Error creating window controller");
}
if let viewController = windowController.contentViewController as? MyViewController {
windowController.window?.delegate=viewController;
}
Your NSWindowDelegate
class MyViewController: NSViewController, NSWindowDelegate {
// The TextField you want to capture keyDown on
var hotKeyTextField:NSTextField!;
// Your custom TextView which will handle keyDown
var hotKeySelectionFieldEditor:HotKeySelectionTextView = HotKeySelectionTextView();
func windowWillReturnFieldEditor(_ sender: NSWindow, to client: Any?) -> Any? {
// If the client (NSTextField) requesting the field editor is the one you want to capture key events on, return the custom field editor. Otherwise, return nil and get the default field editor.
if let textField = client as? NSTextField, textField.identifier == hotKeyTextField.identifier {
return hotKeySelectionFieldEditor;
}
return nil;
}
}
Your custom TextView where you handle keyDown
class HotKeySelectionTextView: NSTextView {
public override func keyDown(with event: NSEvent) {
// Here you can capture the key presses and perhaps save state or communicate back to the ViewController with a delegate pattern if you prefer.
}
}
I fully admit that this feels like a workaround somewhat, but as I am experimenting with Swift at the moment and not quite up to speed with all of the best practices yet I can't make an authoritative claim as to the "Swift-i-ness" of this solution, only that it does allow a NSTextField to capture keyDown events indirectly while maintaining the rest of the NSTextField functionality.
Try like this if you print nslog you will get individual character record for example you pressd "A" you will get the same in console:-
-(void)controlTextDidChange:(NSNotification*)obj
{
NSLog(#"%#",[yourTextfield stringValue]);
}
Also, not sure this is only your requirement.
Text editing for an NSTextField is handled by an NSTextView provided by the window, called the field editor. See the NSWindow method fieldEditor:forObject: and the NSWindowDelegate method windowWillReturnFieldEditor:toObject:. I suppose you could use one of these to provide your own subclassed NSTextView as the field editor. Or, could you simply use NSTextView instead of NSTextField?

NSTextField subclass doesn't respect -acceptsFirstResponder?

I'm working with NSTextFields and enabling/disabling selection and editing, and I ran across some strange behavior in a sample app. I have a subclass of NSTextField called MyTextField; the only thing this subclass does is to deny first responder status whenever asked, as in:
#interface MyTextField : NSTextField
#end
#implementation MyTextField
- (BOOL)acceptsFirstResponder {
return NO;
}
#end
However, when I place an instance of this text field in a .xib, then launch the app, I can still click into the text field and start editing it. Is the text field ignoring the return value of -acceptsFirstResponder?
I've tried a couple things to work around/diagnose this:
The text field's class is properly set to MyTextField instead of NSTextField in the .xib
If I place an NSLog statement before the return, it gets printed to console as expected
If I return NO for -becomeFirstResponder, it exhibits the same behavior: I can still edit the field
If I call [myTextField setSelectable:NO] in the view controller, it works as I expect: I'm no longer able to click into the field
However, if I return NO from -isSelectable and -isEditable (without calling -setSelectable: explicitly), I can still select text in (and edit) the field
What's going on here?
I'm not sure about this, but I think this has to do with the fact that the first responder is not actually the text field, but the field editor, which is a special (non-visible) text view object. Have a look at the "Working with the field editor" section of the "Text Editing Programming Guide" in the Apple docs to see an explanation.

Not being able to edit NSTextField on NSPopover even though Editable behavior is set

I have an application, which open popover with NSTextField. The text field is not editable. Behavior for text field is set to Editable. I still can paste and copy text to this field but i can't edit it.
Anyone knows, what can be wrong?
Not sure if you still need the answer, but there may be some others still looking. I found a solution on apple developer forums. Quoting the original author:
The main problem is the way keyboard events works. Although the NSTextField (and all its superviews) receives keyboard events, it doesn't make any action. That happens because the view where the popover is atached, is in a window which can't become a key window. You can't access that window in any way, at least I couldn't. So the solution is override the method canBecomeKeyWindow for every NSWindow in our application using a category.
NSWindow+canBecomeKeyWindow.h
#interface NSWindow (canBecomeKeyWindow)
#end
NSWindow+canBecomeKeyWindow.m
#implementation NSWindow (canBecomeKeyWindow)
//This is to fix a bug with 10.7 where an NSPopover with a text field cannot be edited if its parent window won't become key
//The pragma statements disable the corresponding warning for overriding an already-implemented method
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-protocol-method-implementation"
- (BOOL)canBecomeKeyWindow
{
return YES;
}
#pragma clang diagnostic pop
#end
That makes the popover fully resposive. If you need another window which must respond NO to canBecomeKeyWindow, you can always make a subclass.
I struggled with this for a while as well, until I realized it was a bug.
However, instead of relying on an isActive state of a NSStatusItem view, I find it much more reliable to use the isShown property of the NSPopover you have implemented.
In my code, I have a NSPopover in a NSViewController:
- (BOOL)canBecomeKeyWindow
{
if([self class]==NSClassFromString(#"NSStatusBarWindow"))
{
NSPopover *mainPopover = [[((AppDelegate*)[NSApp delegate]) mainViewController] mainPopover];
if(![mainPopover isShown])
return NO;
}
return YES;
}
Balazs Toth's answer works, but if you're attaching the popover to NSStatusItem.view the status item becomes unresponsive - requiring two clicks to focus.
What i found when working with this solution is that when NSStatusItem becomes unresponsive, you can easily override this behavior like this
- (BOOL)canBecomeKeyWindow {
if([self class]==NSClassFromString(#"NSStatusBarWindow")) {
CBStatusBarView* view = [((CBAppDelegate*)[NSApp delegate]) statusItemView];
if(![view isActive]) return NO;
}
return YES;
}
You will check for the class of the window, if it matches the NSStatusBarWindow we can then check somehow if the NSStatusItem is active. If it is, that means we have to return YES, because this way the NSPopover from NSStatusItem will have all keyboard events.
What I'm using for checking if the NSStatusItem was clicked (or is active) is that in my own custom view i have a bool value which changes when user clicks on the NSStatusItem, system automatically checks for "canBecomeKeyWindow" and when it does it will return NO and after user clicks on it (while it is returning the NO) it will change the bool value and return YES when system asks again (when NSPopover is being clicked for NSTextField editing).
Sidenotes:
CBStatusBarView is my custom view for NSStatusItem
CBAppDelegate is my App Delegate class
If anyone is still looking for an answer to this, I am working in Swift.
At the time where you wish the field to allow text entry, I have used myTextField.becomeFirstReponder()
To opt out; just use myTextField.resignFirstResponder()
Definitely a bug. That bug report is exactly what I was trying to do. Even down to creating the status item and overriding mousdown.
I can confirm that Balazs Toth's answer works. I just wonder if it might get in the way down the road.
If someone gets it and the solution above didn't do the trick for him.
The problem in my app was in the info tab in the targets my application was set to
Application is background only = true
and shulde of been
Application is agent = true
Spent an entire day on this thing.
Bug. http://openradar.appspot.com/9722231

NSOutlineView with transparent field editor

I'm working with a NSOutlineView located on a HUD panel. I configured it so that it doesn't draw its background. Everything looks fine until I double click to edit a cell.
The field editor draws its background and focus ring which completely ruin the whole user experience.
This is what I'm doing in the subclass of NSTextFieldCell:
- (NSText *)setUpFieldEditorAttributes:(NSText *)textObj
{
NSText *text = [super setUpFieldEditorAttributes:textObj];
[text setDrawsBackground:YES];
[text setBackgroundColor:[NSColor darkGrayColor]];
return text;
}
If I use setDrawsBackground:NO it's completely ignored and I get a white background. My solution is far from being good because I can't touch the alpha component of the color (if I do that, again the field editor will use another color as a background), but at least I don't get a white background.
I'm wondering if there's an actual solution to this problem. Do I have to provide my own field editor? Is it worth it?
What I want is simply a field editor with no background and no focus ring, just the cursor blinking.
Thanks!
The problem is that the white background is drawn by NSTableView when it's sent -editColumn:row:withEvent:select:. It fills the cell's rect with +[NSColor textBackgroundColor].
If there's a public API for overriding the current setting for named colors from the developer colorspace, we could set it inside an override of -editColumn:row:withEvent:select: or the like. I do not recall such an API (pointers are appreciated). ALSO: I've only tested this code on Snow Leopard (even the Leopard SDK addendum below). Verify the code against the actual SDKs and runtime environments you intend to support.
NSTableView has a private accessor it uses for the fill color, but it's a read-only property. No setter, so we can't just change the value on a standard NSTableView. We must subclass it. (Since you want the same behavior in an outlineView and NSOutlineView is already a subclass of NSTableView, we're going to subclass NSOutlineView. But, aside from the superclass, the code is identical.)
#interface ASCOutlineView : NSOutlineView {
}
#end
#implementation ASCOutlineView
- _textBackgroundColor
{
return ([NSColor clearColor]);
}
#end
seems to be all one needs to prevent that glaring white block from ruining your HUD when editing table cells in Snow Leopard.
Apps compiled against the Leopard SDK need a little more support though. Leopard's tableViews may have hard-coded some rendering properties so we need to override a choice method.
NSTextFieldCells are actually wrappers for NSTextViews so they can be used inside controls. They normally share the same textView instance, which is managed by the window (or its subclass, panel, in this case). NSTableView alters the settings of the NSTextFieldCell to conform to system UI settings for editing data. Mostly. The NSTextFieldCell then propagates those settings to the NSTextView. At any point along this pipeline we can override a method or two to alter the values of those properties to match our own UI.
I use -[NSTextFieldCell setDrawsBackground:] because it requires little effort to get correct. It's also important to keep the internal state as consistent with the effect we're hoping to achieve in the event some other object might depend on that state.
#interface ASCTextFieldCell : NSTextFieldCell {
}
#end
#implementation ASCTextFieldCell
- (void)setDrawsBackground: (BOOL)flag
{
[super setDrawsBackground: NO];
}
#end
And preventing the focus ring from appearing while the cell's being edited is a simple matter of changing the setting of its focus ring type. Frustratingly, IB doesn't provide access to this property, so it must be done programmatically:
for(eachColumn in [hudOutlineView tableColumns])
{
columnCell = [[ASCTextFieldCell alloc] initTextCell: #""];
[eachColumn setDataCell: columnCell];
if([columnCell respondsToSelector: #selector(setFocusRingType:)] != NO)
[(NSTextFieldCell *)columnCell setFocusRingType: NSFocusRingTypeNone];
}
It looks like there is other background behind field editor, which is drawn as white.
Probably, NSCell, or background of row, whatever else.

Resources