How to make a NSTextView, that is added programmatically, active? - swift

I am making an app where a user can click anywhere on the window and a NSTextView is added at the mouse location. I have got it working with the below code but I am not able to make it active (in focus) after adding it to the view (parent view). I have to click on the NSTextView to make it active but this is not what I want. I want it to automatically become active when its added to the parent view.
Code in my ViewController to add the NSTextView to its view:
private func addText(at point: NSPoint) {
let textView = MyTextView(frame: NSRect(origin: point, size: CGSize(width: 150.0, height: 40.0)))
view.addSubview(textView)
}
MyTextView class looks like below:
class MyTextView: NSTextView {
override var shouldDrawInsertionPoint: Bool {
true
}
override var canBecomeKeyView: Bool {
true
}
override func viewWillDraw() {
isHorizontallyResizable = true
isVerticallyResizable = true
insertionPointColor = .red
drawsBackground = false
isRichText = false
allowsUndo = true
font = NSFont.systemFont(ofSize: 40.0)
}
}
Also, I want it to lose focus (become inactive) when some other elements (view) are clicked. Right now, once a NSTextView becomes active, it stays active no matter what other elements I click except when I click on an empty space to create yet another NSTextView.
I have gone through the Apple docs multiple times but I think I am missing something. Any help would be much appreciated.

Get the NSWindow instance of the NSViewController's view and call makeFirstResponder passing the text view as parameter.
To lose focus call makeFirstResponder passing nil.

Related

Cannot display UIActivityIndicatorView

I meet a question. I am using following code to display UIActivityIndicatorView. My requirement is to be able to create an UIActivityIndicatorView and display it when I click the button with tag 1, if I click other buttons the UIActivityIndicatorView will be removed from the super view.
class ViewController: UIViewController {
lazy var acLoad:(UIActivityIndicatorView) = {
let myActivityIndicator = UIActivityIndicatorView(style: UIActivityIndicatorView.Style.white)
myActivityIndicator.center = view.center
myActivityIndicator.hidesWhenStopped = true
myActivityIndicator.startAnimating()
return myActivityIndicator
}()
//more code
#objc func btnAction(sender: UIButton){
switch sender.tag {
case 1:
print("created")
view.addSubview(acLoad)
acLoad.startAnimating()
default:
print("removed")
acLoad.stopAnimating()
acLoad.removeFromSuperview()
}
}
}
The above doesn't work, I can get the print log after click, but UIActivityIndicatorView doesn't display, any ideas?
It seems like your UIActivityIndicatorView might be misplaced (wrong frame value) - so it is not visible to you.
print("created")
view.addSubview(acLoad)
acLoad.startAnimating()
// Add this line in your button tap code
// It will tell you where it is placed inside your view
print(acLoad.frame)
If you find that it's frame is not where you want it to be, just fix your layout code and it will be where you expect it to be.
Another note - myActivityIndicator.hidesWhenStopped = true makes it automatically hide on stopAnimating() call. So you can add it only once, not remove-add every time.
Also check your view.backgroundColor vs acLoad style.

How can I show/hide a button added to the title bar of an NSWindow?

I have created a method in an NSWindow extension that allows me to add a button next to the text in the title bar. This is similar to the "down chevron" button that appears in the title bar of Pages and Numbers. When the button is clicked, an arbitrary code, expressed as a closure, is run.
While I have that part working fine, I would also like the button to be invisible most of the time and only become visible when the mouse is scrolled into the title bar area. This would be mimicking the way that Pages and Numbers displays the button.
However, I'm having difficulties getting the show/hide to work properly. I believe I can do it if I make it completely custom in the application delegate, and possibly by subclassing NSWindow, but I would really like to keep it as a single method in an NSWindow extension. In this way the code would be easily reusable in multiple applications.
To accomplish this I believe I need to inject an additional handler/listener that will tell me when the mouse enters and leaves the appropriate area. I can define the necessary area using an NSTrackingArea, but I haven't figured out how to "inject" an event listener without the need of subclasses. Does anyone know how (or if) such a thing is possible?
The key to handling the show/hide based on the mouse position was to use an NSTrackingArea to signify the portion that we are interested in, and to handle the mouse enter and mouse exit events. But since this can't be done directly on the title bar view (since we have to subclass the view in order to add the event handlers) we need to create an additional NSView that is invisible but covers the area we want to track.
I'll post the full code below, but the key parts related to this question are the TrackingHelper class defined near the bottom of the file and the way it is added to the titleBarView with its constrains set to be equal to the size of the title bar. The class itself is designed to take three closures, one for the mouse enter event, one for the mouse exit, and one for the action to take when the button is pressed. (Technically the latter doesn't really need to be part of the TrackingHelper, but it is a convenient place to put it to ensure it does not go out of scope while the UI still exists. A more correct solution would be to subclass NSButton to keep the closure, but I have always found subclassing NSButton to be a royal pain.)
Here is the full text of the solution. Note that this has a couple of things that depend on another library of mine - but they are not necessary for the understanding of this problem and are used to deal with the button image. If you wish to use this code you will need to replace the getImage function with one that creates the image you want. (And if you want to see what KSSCocoa is adding, you can obtain it from https://github.com/klassen-software-solutions/KSSCore)
//
// NSWindowExtension.swift
//
// Created by Steven W. Klassen on 2020-02-24.
//
import os
import Cocoa
import KSSCocoa
public extension NSWindow {
/**
Add an action button to the title bar. This will add a "down chevron" icon, similar to the one used in
Numbers and Pages, just to the right of the title in the title bar. When clicked it will run the given
lambda.
*/
#available(OSX 10.14, *)
func addTitleActionButton(_ lambda: #escaping () -> Void) -> NSButton {
guard let titleBarView = getTitleBarView() else {
fatalError("You can only add a title action to an app that has a title bar")
}
guard let titleTextField = getTextFieldChild(of: titleBarView) else {
fatalError("You can only add a title action to an app that has a title field")
}
let trackingHelper = TrackingHelper()
let actionButton = NSButton(image: getImage(),
target: trackingHelper,
action: #selector(trackingHelper.action))
actionButton.setButtonType(.momentaryPushIn)
actionButton.translatesAutoresizingMaskIntoConstraints = false
actionButton.isBordered = false
actionButton.isEnabled = false
actionButton.alphaValue = 0
trackingHelper.translatesAutoresizingMaskIntoConstraints = false
trackingHelper.onButtonAction = lambda
trackingHelper.onMouseEntered = {
actionButton.isEnabled = true
actionButton.alphaValue = 1
}
trackingHelper.onMouseExited = {
actionButton.isEnabled = false
actionButton.alphaValue = 0
}
titleBarView.addSubview(trackingHelper)
titleBarView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-0-[trackingHelper]-0-|",
options: [], metrics: nil,
views: ["trackingHelper": trackingHelper]))
titleBarView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|-0-[trackingHelper]-0-|",
options: [], metrics: nil,
views: ["trackingHelper": trackingHelper]))
titleBarView.addSubview(actionButton)
titleBarView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:[titleTextField]-[actionButton(==7)]",
options: [], metrics: nil,
views: ["actionButton": actionButton,
"titleTextField": titleTextField]))
titleBarView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|-1-[actionButton]-3-|",
options: [], metrics: nil,
views: ["actionButton": actionButton]))
DistributedNotificationCenter.default().addObserver(
actionButton,
selector: #selector(actionButton.onThemeChanged(notification:)),
name: NSNotification.Name(rawValue: "AppleInterfaceThemeChangedNotification"),
object: nil
)
return actionButton
}
fileprivate func getTitleBarView() -> NSView? {
return standardWindowButton(.closeButton)?.superview
}
fileprivate func getTextFieldChild(of view: NSView) -> NSTextField? {
for subview in view.subviews {
if let textField = subview as? NSTextField {
return textField
}
}
return nil
}
}
fileprivate extension NSButton {
#available(OSX 10.14, *)
#objc func onThemeChanged(notification: NSNotification) {
image = image?.inverted()
}
}
#available(OSX 10.14, *)
fileprivate func getImage() -> NSImage {
var image = NSImage(sfSymbolName: "chevron.down")!
if NSApplication.shared.isDarkMode {
image = image.inverted()
}
return image
}
fileprivate final class TrackingHelper : NSView {
typealias Callback = ()->Void
var onMouseEntered: Callback? = nil
var onMouseExited: Callback? = nil
var onButtonAction: Callback? = nil
override func mouseEntered(with event: NSEvent) {
onMouseEntered?()
}
override func mouseExited(with event: NSEvent) {
onMouseExited?()
}
#objc func action() {
onButtonAction?()
}
override func updateTrackingAreas() {
super.updateTrackingAreas()
for trackingArea in self.trackingAreas {
self.removeTrackingArea(trackingArea)
}
let options: NSTrackingArea.Options = [.mouseEnteredAndExited, .activeAlways]
let trackingArea = NSTrackingArea(rect: self.bounds, options: options, owner: self, userInfo: nil)
self.addTrackingArea(trackingArea)
}
}

Cannot modify NSTabViewItem

I may be getting lost in a glass of water as I am not an experienced developer but I cannot seem to be able to implement a simple override to modify the size of an NSTabView item.
I have a Tab View Controller (Style = toolbar)
I have a Tabless Tab View
I have 3 Tab Items. For testing I have only subclassed one of them to the subclass below
I have created a new subclass of NSTabViewItem: MyTabViewItem and subclassed one of the 3 tab Items. The code is:
import Cocoa
class MyTabViewItem: NSTabViewItem {
override func drawLabel(_ shouldTruncateLabel: Bool, in labelRect: NSRect) {
var size = self.sizeOfLabel(false)
size.width = 180
print("Draw!!")
}
override func sizeOfLabel(_ computeMin: Bool) -> NSSize {
var size = super.sizeOfLabel(false)
size.width = 180
print("Draw!!")
return size
}
}
Everything works, except the subclassing. The Tabs appear, they do operate by switching the views and the program runs as it should. Except that it does not resize the Tab Item. The code in the subclass MyTabViewItem is never reached (it never prints Draw!! as it should.
I cannot understand what I am missing here. I have not read of any IB connection to make (and I cannot seem to be able to connect the Tab Items anyways). Please apologise if it isa trivial question but I have searched everywhere and not found anything to help me.
Thank you
You said:
I have a Tabless Tab View
This is your problem. An NSTabView only asks an NSTabViewItem to drawLabel if the NSTabView itself is responsible for drawing the tab bar, but you have a “Tabless” tab view. (“Tabless” is the default style when you drag an NSTabViewController into a storyboard.)
You also said:
I have a Tab View Controller (Style = toolbar)
So you don't even want the tab view to draw a tab bar; you want items in the window toolbar to select tabs (like in Xcode's preference window).
Your ability to customize the toolbar items created for your tabs is limited. You can subclass NSTabViewController and override toolbar:itemForItemIdentifier:willBeInsertedIntoToolbar:, like this:
override func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? {
let toolbarItem = super.toolbar(toolbar, itemForItemIdentifier: itemIdentifier, willBeInsertedIntoToolbar: flag)
if
let toolbarItem = toolbarItem,
let tabViewItem = tabViewItems.first(where: { ($0.identifier as? String) == itemIdentifier.rawValue })
{
toolbarItem.label = "\(tabViewItem.label) 😀"
}
return toolbarItem
}
But I found that making other changes didn't work well:
Setting toolbarItem.image didn't work well for me.
Setting toolbarItem.view made the item stop receiving clicks.
Note that the minSize and maxSize properties are only used if toolbarItem.view is set.
Your best bet is probably to manage the toolbar yourself, without trying to use NSTabViewController's support.
I have also subclassed the NSTabViewController as follows:
import Cocoa
class MyTabViewController: NSTabViewController {
#IBOutlet weak var TradingTabItem: MyTabViewItem!
override func viewDidLoad() {
super.viewDidLoad()
print("Loaded Tab View")
TradingTabItem.label = "New"
// Do view setup here.
}
}
What happens now is that the tab item in my subclass (the only one of the 3 I subclassed) does change its label string to New. However, even if I have added the item as an IBOutlet here, it still does not change seize (and the overridden sizeOfLabel function is not reached).

Cursor isn’t changed properly when drag a file to a draggable view on Cocoa's WKWebView

I wrote the following code to put a draggable view on WKWebView.
With this code, I expected a "+" icon will be displayed nearby a cursor when I dragged a file to the view.
class ViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
let rect = NSRect(x: 0, y: 0, width: 200, height: 200)
let webView = WKWebView(
frame: rect,
configuration: WKWebViewConfiguration())
webView.load(URLRequest(
url: URL(string: "https://i.imgur.com/D5ru3Q7.jpg")!))
let draggableView = DraggableView(frame: rect)
draggableView.registerForDraggedTypes([.fileURL])
self.view.addSubview(webView)
self.view.addSubview(draggableView)
}
}
class DraggableView: NSView {
override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
return .copy
}
}
The result is here:
Sometimes the cursor changes to a magnifying glass (same as a mouse over behavior on WKWebView).
And sometimes "+" icon is shown.
I think the webView prevents the cursor changing.
So I tried the followings. But I couldn't fix.
Overriding hitTest of the webView worked only for non-dragging mouse over actions. But not for dragging.
webView.unregisterDraggedTypes() didn't work.
Is there anyway to fix this?
Found a solution:
for t in webView.trackingAreas {
webView.removeTrackingArea(t)
}
I think removing tracking areas on a web view is not a good idea.
But at least it satisfies my need.

OSX Application: how to make window maximized?

I am working on a mac application, and I like to make initial window be in maximized state, like when you are pressing green button with plus sign.
I don't want it to be full screen.
An app in its zoomed state is not the same thing as "maximized." The green plus icon indicates zoom, which means "the appropriate size for this content." In some applications that's the visible frame (as Eric D. discusses), but it can be almost anything. Try zooming a Safari window for instance.
Assuming you really want "maximized" and not "zoom", then Eric is on the right track, but it can be done better. First, you should use the window's screen if it has one. Also, you should not animate the window resize during launch (since that can look awkward on launch).
func applicationDidFinishLaunching(aNotification: NSNotification) {
if let screen = window.screen ?? NSScreen.mainScreen() {
window.setFrame(screen.visibleFrame, display: true)
}
}
You may want to consider using a NSWindowController to manage this rather than putting it in the application delegate. In that case, you can put this in windowDidLoad. Window controllers are a pretty common tool in AppKit (as opposed to view controllers, which are not historically as common).
If you actually want zoom behavior, familiarize yourself with the the NSWindowDelegate method windowWillUseStandardFrame(_:defaultFrame:). You shouldn't generally call zoom(_:) directly on launch because that will animate, but whatever logic you do in the delegate should be used to compute your frame. Again, make sure to adjust your frame to live on the window's screen if it has one, rather than the main screen.
Ideally, you really should be honoring the last frame that the user used rather than forcing it to the visible frame. That's called frameAutosave in Cocoa if you want to research that more. A window controller will help you manage that somewhat automatically if you just set a autosave name in Interface Builder. (Though it's slightly complicated by needing to compute the frame on first launch to get the visible frame, so it won't be completely automatic.)
Do give some careful thought before making your default frame be the visible frame in any case. That can be really enormous on large monitors (there are still a lot of 30" Cinema displays out there, but even on a 27" it can be pretty overwhelming). Sometimes that's fine depending on your app, but I often find that it's worth defining a maximum initial size (while allowing the user to make it larger).
You can "zoom" a window to the max available space by using NSScreen's visibleFrame as the target frame. Let's say window is your NSWindow IBOutlet:
if let screen = NSScreen.mainScreen() {
window.setFrame(screen.visibleFrame, display: true, animate: true)
}
For example, in the AppDelegate.swift:
import Cocoa
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
#IBOutlet weak var window: NSWindow!
func applicationDidFinishLaunching(aNotification: NSNotification) {
if let screen = NSScreen.mainScreen() {
window.setFrame(screen.visibleFrame, display: true, animate: true)
}
}
in Swift 4.2:
class ViewController: NSViewController {
override func viewDidAppear() {
super.viewDidAppear()
view.window?.zoom(self) //bespread the screen
//view.window?.toggleFullScreen(self) //fullscreen
}
2020 | SWIFT 5.1:
use extension:
extension NSWindowController {
func maximize() { self.window?.zoom(self) }
}
just call maximize() of NSWindowController instance :)
Swift 5
If anyone's still having issues, trying calling the zoom function the main thread. Worked for me.
DispatchQueue.main.async {
self.view.window?.zoom(self)
}
Hi Guys I really appreciate your help.
I am working on a document based mac application. I put the code you provided in the makeWindowControllers() of Document class and it works like a charm.
Thank you very much. Here is the code I use.
override func makeWindowControllers() {
// Returns the Storyboard that contains your Document window.
let storyboard = NSStoryboard(name: "Main", bundle: nil)
let windowController = storyboard.instantiateControllerWithIdentifier("Document Window Controller") as! NSWindowController
self.addWindowController(windowController)
if let screen = NSScreen.mainScreen() {
windowController.window?.setFrame(screen.visibleFrame, display: true, animate: true)
}
}
this code works well only on single-windowed application, but it's really easy to edit to work with multy-windowed application
usage to maximize and unmaximize window:
TheApp.maximized.toggle()
Source code
public class TheApp {
static var maximized: Bool {
get {
guard let visibleFrame = NSScreen.main?.visibleFrame,
let window = NSApp.mainWindow
else { return false }
return window.frame == visibleFrame
}
set { NSApp.mainWindow?.zoom(newValue) }
}
static var fullscreen: Bool {
get {
guard let screenFrame = NSScreen.main?.frame,
let window = NSApp.mainWindow
else { return false }
return window.frame == screenFrame
} set {
NSApp.mainWindow?.toggleFullScreen(newValue)
}
}
static var mimimized: Bool {
get { NSApp.mainWindow?.isMiniaturized ?? false }
set { NSApp?.mainWindow?.miniaturize(newValue) }
}
}