How to detach NSTextView's undo manager from NSDocument? - macos

I have an NSDocument that is in a non-directly editable format, it's a description of something in XML. The NSWindow has a settings view controller associated with it which manipulates the data in the document just fine, undo works as expected, save, etc. Now also in the NSWindow is an NSTextView, which the user can enter some text into, but is not part of the content of the document, it's used only as temporary text. Of course I want to support undo for this text too, so I have the "Undo" checkmark enabled in Interface Builder for this NSTextView, and undo works just fine.
Now comes the rub: the NSDocument is getting marked as dirty when the NSTextView is modified. Because this is temporary text, I don't want the user to be nagged to save changes to the document, that really are not part of the document.
How do I detach the NSTextView from the responder chain leading up to the NSDocument's undo manager instance? Simply providing a new instance of NSUndoManager doesn't solve it, because it just goes up the responder chain to NSDocument as well.
extension InputViewController: NSTextViewDelegate {
func undoManager(for view: NSTextView) -> UndoManager? {
return myUndoManager
}
}

I'm pretty sure you'd need to override the undoManager property of the window's view controller, not the text field's delegate.
However, to simply make your document/window permanently non-editable all you need to do is override either the documentEdited property so it always returns false, or override updateChangeCount so it ignores requests to record changes.

Thanks to James Bucanek's comment about overriding updateChangeCount, I was able to do something like this in my NSDocument subclass:
override func updateChangeCount(_ change: NSDocument.ChangeType) {
if !suppressChangeCount {
super.updateChangeCount(change)
} else {
suppressChangeCount = false
}
}
Thus allowing undo/redo to work in my InputViewController keycode handler without dirtying the document.

Related

How to intercept keystrokes from within the field editor of an NSTextField?

Intro
When my custom NSTextField is in "text editing mode" and the field editor has been placed in front of it as firstResponder I no longer get the keystrokes through NSTextField.keyDown(...). I understand that the keystrokes are now being routed through the field editor. Most online recommendations are to override the following method within the delegate of the custom NSTextField:
control(_:textView:doCommandBySelector:)
I have indeed overridden this method but it doesn't seem to get called? See code below:
class FocusDelegate: NSObject, NSTextFieldDelegate{
func control(control: NSControl, textView: NSTextView, doCommandBySelector commandSelector: Selector) -> Bool {
println("FocusDelegate")
if commandSelector == Selector("cancelOperation:"){
println("cancelOperation")
control.abortEditing()
return true
}
return false
}
}
The only other thing I do is set the delegate of my custom NSTextField to an instance of the class above. I have also tried placing the method above directly into the custom NSTextField class - it doesn't cause any errors but it also doesn't get called.
Questions
What am I missing above in getting the delegate call to work?
Other than creating my own custom field editor is this the only way? Can the textDidChange(notification: NSNotification) not be interrogated to yield the keys pressed?
In using the control(_:textView:doCommandBySelector:) delegate method, how do I trap the pressing of keys that do NOT have standard key bindings. In particular, I want to intercept the key combination "shift+enter" which does not map to any standard selector. (Implied Question: can you only map to standard key-action methods in NSResponder?)
I believe I have achieved greater clarity with regard to the workings of NSTextField its delegates and its fieldEditor and its delegates. Yes, each one has its own delegate and the NSTextField is automatically set up as the delegate of the fieldEditor.
I was adding a delegate to the host NSTextField. This will NOT get called when the fieldEditor is firstResponder, which was what I was concerned with. This is also not the important delegate for the scenario above.
What appears to be useful is making the NSTextField conform to the NSTextViewDelegate protocol and specifically overriding the method:
func textView(textView: NSTextView, shouldChangeTextInRange affectedCharRange: NSRange, replacementString: String) -> Bool
to trap various keypresses.
The notification in textDidChange(notification: NSNotification) is not of use in this scenario because it is reported AFTER the keypress.
Intercepting pressed keys in textView:shouldChangeTextInRange:replacementString: is not ideal because that is called rather late, and is also called when the user pastes from the clipboard, for instance.
A probably cleaner way is to intercept textView:doCommandBySelector:, which gets called for all kinds of special keys such as tab, cursor, return and enter, for which you'll then get specific selectors that let you determine which special key was invoked.
For some keys, however, such as Return (CR) and Enter (ETX), you can't tell them apart, because both use the same "insertNewline:" selector.
In this case, you can check whether the currently handled event is a keydown event, and if so, check which key it was.
Here's a quick test code that lets you monitor the selector names and the involved keycode:
-(BOOL)textView:(NSTextView *)textView doCommandBySelector:(SEL)commandSelector
{
NSEvent *ev = self.view.window.currentEvent;
NSLog (#"selector: %s, key: %d", commandSelector, (int)ev.keyCode);
return NO;
}

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?

Cocoa: Avoiding 'Updates Continuously' in control binds

I have several panels that contain NSTextField controls bound to properties within the File's Owner object. If the user edits a field and then presses Tab, to move to the next field, it works as expected. However if the user doesn't press Tab and just presses the OK button, the new value is not set in the File's Owner object.
In order to workaround this I have set Updates Continuously in the binding, but this must be expensive (EDIT: or at least it's inelegant).
Is there a way to force the bind update when the OK button is pressed rather than using Updates Continuously?
You're right that you don't need to use the continuously updates value option.
If you're using bindings (which you are), then what you should be doing is calling the -commitEditing method of the NSController subclass that's managing the binding. You'd normally do this in your method that closes the sheet that you're displaying.
-commitEditing tells the controller to finish editing in the active control and commit the current edits to the bound object.
It's a good idea to call this whenever you are performing a persistence operation such as a save.
The solution to this is to 'end editing' in the action method that gets called by the OK button. As the pane is a subclass of NSWindowController, the NSWindow is easily accessible, however in your code you might have to get the NSWindow via a control you have bound to the controller; for example NSWindow *window = [_someControl window].
Below is the implementation of my okPressed action method.
In summary I believe this is a better solution to setting Updated Continuously in the bound controls.
- (IBAction)okPressed:(id)sender
{
NSWindow *window = [self window];
BOOL editingEnded = [window makeFirstResponder:window];
if (!editingEnded)
{
logwrn(#"Unable to end editing");
return;
}
if (_delegateRespondsToEditComplete)
{
[_delegate detailsEditComplete:&_mydetails];
}
}
Although this is really old, I absolutely disagree with the assumption that this question is based on.
Countinously updating the binding is absolutely not expensive. I guess you might think this updates the value continuously, understanding as "regularly based on some interval".
But this is not true. This just means it updates whenever you change the bound value. This means, when you type something in a textView, it would update as you write; this is what you'd want in this situation.

How to handle NSTextView preferences (spelling and grammar, substitutions,...)

The NSTextView class allows the user to dis-/enable features like "spelling while typing" with the context menu (right click). But when I use a NSTextView in my own app, those preferences are not saved automatically by the text view itself, which means that I have to save them separately - right?
Now I also want to allow the user to change those settings in my app preferences (like in TextEdit). What I do is to save the text view preferences in the user defaults, which means that every time the user changes the setting in the app preferences, I apply those settings and save them. It's pretty easy to accomplish that except the one case where the user changes the text view setting with the context menu and not through the app preferences.
My question now: How can I get notified when the settings of a NSTextView is changed, so I can save them?
I've done a project where I have subclassed NSTextView and I can easily catch any setting changes that have been made by the user.
So, for you to do this, simply create a new .h & .m file and declare it like this:
(in the .h file)
#interface BrutellaTextView : NSTextView
#end
(in the .m file)
#interface BrutellaTextView
- (void)setContinuousSpellCheckingEnabled:(BOOL)flag
{
// here I am just setting user defaults... you may choose
// to have some other place to save the settings
NSUserDefaults * userDefaults = [NSUserDefaults standardUserDefaults];
if(userDefaults)
{
[userDefaults setBool: flag forKey: #"continuousSpellCheckingEnabled"];
}
// and to get the functionality to actually happen,
// call the superclass
[super setContinuousSpellCheckingEnabled: flag];
}
#end
(and you can override other NSTextView methods to capture when other settings change, such as setRulerVisible:).
Now, when you are in your XIB file, make sure to set the CustomClass of your text view to be BrutellaTextView and you'll be all set!
There are no notifications that you can register to get NSTextView settings changes, so as far as I'm concerned, this is the best way to do what you're trying to do.
I hope this answer helps you out!
You could also use NSTextViews method
toggleAutomaticSpellingCorrection:
This method is only called, when the user changes the setting via menu/contextMenu and not if the programmer changes the setting programatically (e.g. temporarily disabling when inserting text).

How to disable drag-n-drop for NSTextField?

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?

Resources