How to clear NSTextView selection without it becoming first responder? - cocoa

I have a basic Cocoa app with a number of NSTextViews. When a text view loses focus (i.e. resigns its first responder status), I'd like to clear its selection.
My strategy was to extend NSTextView and override resignFirstResponder():
override func resignFirstResponder() -> Bool {
// Both result in the text view becoming first responder again:
clearSelection(nil)
setSelectedRange(NSRange(location: 0, length: 0))
return super.resignFirstResponder()
}
The problem is that calling clearSelection() and setSelectedRange() both cause the text view to become first responder again.
Is there a way to clear the selection without it becoming the first responder?
I tried to also override acceptsFirstResponder and temporarily return false, but that didn't work either.

Met the same issue today and found the solution
You can do setSelectedRange in NSTextView's delegate method textDidEndEditing and it wouldn't cause NSTextView become first responder.
class TextView: NSTextView {
init() {
self.delegate = self
....
}
....
}
extension TextView: NSTextViewDelegate {
public func textDidEndEditing(_ notification: Notification) {
setSelectedRange(NSMakeRange(string.count, 0))
}
}

Related

Align NSToolbarItems with NSSplitView columns

Finder and Notes have a peculiar behaviour that I am seeking to reproduce. The ‘flexible space’ in the NSToolbar seems to take the dimensions of the split view into account. For instance, the first group of buttons aligns on the left side with the right side of the sidebar. The second group of icons aligns with the right side of the first column. When I widen the sidebar, the toolbar items move along with it.
Is it possible to reproduce this?
Solution
With the solution provided by #KenThomases, I have implemented this as follows:
final class MainWindowController: NSWindowController {
override func windowDidLoad() {
super.windowDidLoad()
window?.toolbar?.delegate = self
// Make sure that tracking is enabled when the toolbar is completed
DispatchQueue.main.async {
self.trackSplitViewForFirstFlexibleToolbarItem()
}
}
}
extension MainWindowController: NSToolbarDelegate {
func toolbarWillAddItem(_ notification: Notification) {
// Make sure that tracking is evaluated only after the item was added
DispatchQueue.main.async {
self.trackSplitViewForFirstFlexibleToolbarItem()
}
}
func toolbarDidRemoveItem(_ notification: Notification) {
trackSplitViewForFirstFlexibleToolbarItem()
}
/// - Warning: This is a private Apple method and may break in the future.
func toolbarDidReorderItem(_ notification: Notification) {
trackSplitViewForFirstFlexibleToolbarItem()
}
/// - Warning: This method uses private Apple methods that may break in the future.
fileprivate func trackSplitViewForFirstFlexibleToolbarItem() {
guard var toolbarItems = self.window?.toolbar?.items, let splitView = (contentViewController as? NSSplitViewController)?.splitView else {
return
}
// Add tracking to the first flexible space and remove it from the group
if let firstFlexibleToolbarItem = toolbarItems.first, firstFlexibleToolbarItem.itemIdentifier == NSToolbarFlexibleSpaceItemIdentifier {
_ = firstFlexibleToolbarItem.perform(Selector(("setTrackedSplitView:")), with: splitView)
toolbarItems.removeFirst()
}
// Remove tracking from other flexible spaces
for flexibleToolbarItem in toolbarItems.filter({ $0.itemIdentifier == NSToolbarFlexibleSpaceItemIdentifier }) {
_ = flexibleToolbarItem.perform(Selector(("setTrackedSplitView:")), with: nil)
}
}
}
When using macOS 11 or newer, you can insert NSTrackingSeparatorToolbarItem items to the toolbar, which will split up your toolbar in sections, aligned with the dividers of a NSSplitView object.
This example adds the new separator items to a toolbar that already contains the rest of the buttons, configured in Interface Builder or in code. The target splitview concerns a standard configuration of 3 splitviews, including a sidebar panel.
class WindowController: NSWindowController, NSToolbarDelegate {
let mainPanelSeparatorIdentifier = NSToolbarItem.Identifier(rawValue: "MainPanel")
override func windowDidLoad() {
super.windowDidLoad()
self.window?.toolbar?.delegate = self
// Calling the inserts async gives more time to bind with the split viewer, and prevents crashes
DispatchQueue.main.async {
// The .sidebarTrackingSeparator is a built-in tracking separator which always aligns with the sidebar splitview
self.window?.toolbar?.insertItem(withItemIdentifier: .sidebarTrackingSeparator, at: 0)
// Example of a custom mainPanelSeparatorIdentifier
// Index at '3' means that there are 3 toolbar items at the left side
// of this separator, including the first tracking separator
self.window?.toolbar?.insertItem(withItemIdentifier: mainPanelSeparatorIdentifier at: 3)
}
}
func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? {
if let splitView = (self.contentViewController as? NSSplitViewController)?.splitView {
// You must implement this for custom separator identifiers, to connect the separator with a split view divider
if itemIdentifier == mainPanelSeparatorIdentifier {
return NSTrackingSeparatorToolbarItem(identifier: itemIdentifier, splitView: splitView, dividerIndex: 1)
}
}
return nil
}
}
If you want to add an extra separator, for example for an Inspector panel, simply insert an additional toolbar item identifier to the toolbar, and assign an extra NSTrackingSeparatorToolbarItem to another divider in the itemForItemIdentifier delegate function.
You can do this with Apple-private methods, although that's not allowed in the App Store.
There's a private method, -setTrackedSplitView:, on NSToolbarItem. It takes an NSSplitView* as its parameter. You need to call it on the flexible-space toolbar item that you want to track a split view and pass it the split view it should track. To protect yourself against Apple removing the method, you should check if NSToolbarItem responds to the method before trying to use it.
Since the user can customize and re-order the toolbar, you generally need to enumerate the window's toolbar's items. For the first one whose identifier is NSToolbarFlexibleSpaceItemIdentifier, you set the split view it should track. For all other flexible-space items, you clear (set to nil) the split view to track. You need to do that when the window is first set up and again in the toolbar delegate's -toolbarWillAddItem: and -toolbarDidRemoveItem: methods. There's also another undocumented delegate method, -toolbarDidReorderItem:, where I've found it useful to update the toolbar.

NSResponder accepts events when acceptsFirstResponder is false?

Why are the methods moveLeft() and moveRight() being called, I have turned off the first responder ability for the window controller? I haven't added any code in elsewhere, so I'm obviously missing something somewhere...
In the end I do want to accept events, but if I 'enable' them here and deal with overriding keyEvent(), it causes it to be handled twice and a choice being made twice.
import Cocoa
enum UserChoice {
case Left, Right
}
class MainWindowController: NSWindowController {
override func windowDidLoad() {
super.windowDidLoad()
}
override var windowNibName: String? {
return "MainWindowController"
}
override var acceptsFirstResponder: Bool {
return false
}
override func moveLeft(sender: AnyObject?) {
chooseImage(UserChoice.Left)
}
override func moveRight(sender: AnyObject?) {
chooseImage(UserChoice.Right)
}
func chooseImage(choice: UserChoice) {
print("choice made")
}
}
The only other file I have is AppDelegate.swift:
import Cocoa
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
var mainWindowController: MainWindowController!
func applicationDidFinishLaunching(notification: NSNotification) {
mainWindowController = MainWindowController()
mainWindowController.showWindow(self)
}
}
Any comments on my code are welcome too, I'm new to Swift/Cocoa so...
When your controller refuses to be first responder, it's still a responder, just not the first one. Another responder such as the window or a view within it can choose to pass the buck back up the responder chain.
I'm not clear on why you implemented moveLeft and moveRight if you don't want to handle them.

Execute callback when text changes inside a NSTextField in Swift

I have a NSTextField and I would like to execute a callback whenever the text inside it changes. The callback would be to enable a disabled "save" button at the bottom of the form.
What I managed to do so far is sub-class NSTextView in order to override textDidChange(notification)
import Cocoa
class MyTextField: NSTextField {
override func textDidChange(notification: NSNotification) {
super.textDidChange(notification)
}
}
After that, I didn't manage to execute a function inside my ViewController. I tried using NSNotificationCenter to trigger some kind of global event that I could catch inside the ViewController like so :
//MyTextField.swift
import Cocoa
class MyTextField: NSTextField {
override func textDidChange(notification: NSNotification) {
NSNotificationCenter.defaultCenter().postNotification(notification)
super.textDidChange(notification)
}
}
//ViewController.swift
import Cocoa
class ViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: "fieldTextDidChange:", name: "NSTextDidChangeNotification", object: nil)
}
override func viewDidAppear() {
super.viewDidAppear()
}
func fieldTextDidChange(notification: NSNotification) {
print(notification, appendNewline: true)
}
}
But I get a runtime error when typing inside the field : Thread 1: EXC_BAD_ACCESS (code=2, address=0x7fff5f3fff70) on the line that calls postNotification()
How can I manage to trigger a callback on text change of a NSTextField ?
EDIT
Sub-classing and sending a notification is silly as pointed out by matt. There is no need to sub-class the text field. Simply observing the NSTextDidChangeNotification is enough to react to the event I was looking for.
I had tested this but I was missing a colon at the end of the selector on top of this, so I thought it was not the correct method. It is indeed the correct method.
The reason you are crashing is that your selector is wrong. It should be selector: "fieldTextDidChange:" (notice the final colon).

Move a NSWindow by dragging a NSView

I have a NSWindow, on which i apply this:
window.styleMask = window.styleMask | NSFullSizeContentViewWindowMask
window.titleVisibility = NSWindowTitleVisibility.Hidden;
window.titlebarAppearsTransparent = true;
I then add a NSView behind the titlebar to simulate a bigger one.
Now it looks like this:
I want to be able to move the window, by dragging the light-blue view. I have already tried to subclass NSView and always returning true for mouseDownCanMoveWindow using this code:
class LSViewD: NSView {
override var mouseDownCanMoveWindow:Bool {
get {
return true
}
}
}
This didn't work.
After some googling i found this INAppStoreWindow on GitHub. However it doesn't support OS X versions over 10.9, so it's completely useless for me.
Edit1
This is how it looks in the Interface Builder.
How can i move the window, by dragging on this NSView?
None of the answers here worked for me. They all either don't work at all, or make the whole window draggable (note that OP is not asking for this).
Here's how to actually achieve this:
To make a NSView control the window with it's drag events, simply subclass it and override the mouseDown as such:
class WindowDragView: NSView {
override public func mouseDown(with event: NSEvent) {
window?.performDrag(with: event)
}
}
That's it. The mouseDown function will transfer further event tracking to it's parent window.
No need for window masks, isMovableByWindowBackground or mouseDownCanMoveWindow.
Try setting the window's movableByWindowBackground property to true.
There are two ways to do this. The first one would be to set the NSTexturedBackgroundWindowMask as well as the windows background color to the one of your view. This should work.
Otherwise you can take a look at this Sample Code
I somehow managed to solve my problem, i don't really know how, but here are some screenshots.
In the AppDelegate file where i edit the properties of my window, i added an IBOutlet of my contentView. This IBOutlet is a subclass of NSView, in which i've overriden the variable mouseDownCanMoveWindow so it always returns false.
I tried this before in only one file, but it didn't work. This however solved the problem.
Thanks to Ken Thomases and Max for leading me into the right direction.
Swift3.0 Version
override func viewDidAppear() {
//for hide the TitleBar
self.view.window?.styleMask = .borderless
self.view.window?.titlebarAppearsTransparent = true
self.view.window?.titleVisibility = .hidden
//for Window movable with NSView
self.view.window?.isMovableByWindowBackground = true
}
Swift 3:
I needed this but dynamically. It's a little long but well worth it (IMHO).
So I decided to enable this only while the command key is down. This is achieved by registering a local key handler in the delegate:
// MARK:- Local key monitor
var localKeyDownMonitor : Any? = nil
var commandKeyDown : Bool = false {
didSet {
let notif = Notification(name: Notification.Name(rawValue: "commandKeyDown"),
object: NSNumber(booleanLiteral: commandKeyDown))
NotificationCenter.default.post(notif)
}
}
func keyDownMonitor(event: NSEvent) -> Bool {
switch event.modifierFlags.intersection(.deviceIndependentFlagsMask) {
case [.command]:
self.commandKeyDown = true
return true
default:
self.commandKeyDown = false
return false
}
}
which is enabled within the delegate startup:
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Watch local keys for window movenment, etc.
localKeyDownMonitor = NSEvent.addLocalMonitorForEvents(matching: NSEventMask.flagsChanged) { (event) -> NSEvent? in
return self.keyDownMonitor(event: event) ? nil : event
}
}
and its removal
func applicationWillTerminate(_ aNotification: Notification) {
// Forget key down monitoring
NSEvent.removeMonitor(localKeyDownMonitor!)
}
Note that when the commandKeyDown value is changed by the key down handler. This value change is caught by the didset{} to post a notification. This notification is registered by any view you wish to have its window so moved - i.e., in the view delegate
override func viewDidLoad() {
super.viewDidLoad()
// Watch command key changes
NotificationCenter.default.addObserver(
self,
selector: #selector(ViewController.commandKeyDown(_:)),
name: NSNotification.Name(rawValue: "commandKeyDown"),
object: nil)
}
and discarded when the viewWillDisappear() (delegate) or the window controller windowShouldClose(); add this
<your-view>.removeObserver(self, forKeyPath: "commandKeyDown")
So sequence goes like this:
key pressed/release
handler called
notification posted
The view's window isMovableByWindowBackground property is changed by notification - placed within view controller / delegate or where you registered the observer.
internal func commandKeyDown(_ notification : Notification) {
let commandKeyDown : NSNumber = notification.object as! NSNumber
if let window = self.view.window {
window.isMovableByWindowBackground = commandKeyDown.boolValue
Swift.print(String(format: "command %#", commandKeyDown.boolValue ? "v" : "^"))
}
}
Remove the tracer output when happy. See it in action in SimpleViewer on github.

Detecting key press event in Swift

I'm trying to find a way to detect if a key (on a keyboard) has been pressed on Swift. Any ideas and suggestions will be greatly appreciated.
Since you updated your question and you wanted to know how to do this for a window, here's an answer. Subclass NSWindow and use this subclass instead.
Your custom class should look like this:
import Cocoa
class EditorWindow: NSWindow {
override func keyDown(event: NSEvent) {
super.keyDown(event)
Swift.print("Caught a key down: \(event.keyCode)!")
}
}
If you've made your window in Interface Builder/XCode, click the window object and go to the Attribute Inspector (⌥+⌘+3). The Attribute Inspector will be in the sidebar on the right. Making sure your window is selected in Interface Builder, at the top of the Attribute Inspector in the Custom Class area put your new class in the class input.
In order to communicate the event from the this window class to my app I add a function to the window that accepts a callback function that I then store in an array of callback functions. I get access to this window through the AppDelegate which can get a weak reference to the current main window. Then in the above function I iterate overall the callbacks and call it with the NSEvent as the argument. I also first check to see if any command keys like the option keys are being pressed first through modifierFlags property on the event. It ends up looking like this:
import Cocoa
typealias Callback = (NSEvent) -> ()
class KeyCaptureWindow: NSWindow {
var keyEventListeners = Array<Callback>()
override func keyDown(event: NSEvent) {
if event.modifierFlags.contains(NSEventModifierFlags.CommandKeyMask) {
super.keyDown(event)
return
}
for callback in keyEventListeners {
callback(event)
}
}
func addKeyEventCallback(callback: Callback) {
keyEventListeners.append(callback)
}
}
And then elsewhere in my code I have a line like so:
let appDelegate = NSApplication.sharedApplication().delegate as! AppDelegate
let mainWindow = appDelegate.getWindow()
mainWindow.addKeyEventCallback(handleKeyEvent)
I added the getWindow method to my app delegate class. This method returns the NSWindow cast to KeyCaptureWindow. There may be a better way to do all this but this works for me. Another way to possibly do this is to use first responders and NSView, but that's not how I've been doing it.
You have to override the keyDown-method.
var direction:String = ""
override func keyDown(theEvent: NSEvent!) // A key is pressed
{
if theEvent.keyCode == 123
{
direction = "left" //get the pressed key
}
else if theEvent.keyCode == 124
{
direction = "right" //get the pressed key
}
println("Key with number: \(theEvent.keyCode) was pressed")
}

Resources