根据鼠标位置处理显示/隐藏的关键是使用NSTrackingArea 来表示我们感兴趣的部分,并处理鼠标进入和鼠标退出事件。但是由于这不能直接在标题栏视图上完成(因为我们必须对视图进行子类化才能添加事件处理程序)我们需要创建一个额外的 NSView 不可见但覆盖我们想要跟踪的区域。
我将在下面发布完整代码,但与此问题相关的关键部分是在文件底部附近定义的 TrackingHelper 类以及将其添加到 titleBarView 的方式,其约束设置为等于标题栏的大小。该类本身被设计为采用三个闭包,一个用于鼠标进入事件,一个用于鼠标退出,一个用于按下按钮时执行的操作。 (从技术上讲,后者实际上并不需要成为TrackingHelper 的一部分,但它是一个方便的放置位置,以确保在 UI 仍然存在时它不会超出范围。更正确的解决方案是子类化NSButton 保持关闭状态,但我一直发现子类化 NSButton 是一种皇家痛苦。)
这里是解决方案的全文。请注意,这有一些依赖于我的另一个库的东西 - 但它们对于理解这个问题并不是必需的,并且用于处理按钮图像。如果您希望使用此代码,则需要将 getImage 函数替换为创建所需图像的函数。 (如果你想看看KSSCocoa添加了什么,你可以从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)
}
}