如何显示/隐藏添加到NSWindow标题栏的按钮



我在NSWindow扩展中创建了一个方法,允许我在标题栏的文本旁边添加一个按钮。这类似于";向下V形";按钮,该按钮显示在页面和数字的标题栏中。单击按钮时,将运行一个表示为闭包的任意代码。

虽然这部分工作正常,但我也希望按钮在大多数情况下都是不可见的,只有当鼠标滚动到标题栏区域时才可见。这将模仿Pages and Numbers显示按钮的方式。

然而,我很难让表演/隐藏正常工作。我相信,如果我在应用程序委托中完全自定义它,并且可能通过子类化NSWindow,我可以做到这一点,但我真的希望将它作为NSWindow扩展中的一个单独方法。通过这种方式,代码将很容易在多个应用程序中重复使用。

为了实现这一点,我认为我需要注入一个额外的处理程序/监听器,当鼠标进入和离开适当的区域时,它会告诉我。我可以使用NSTrackingArea定义必要的区域,但我还没有弄清楚如何";注入";不需要子类的事件侦听器。有人知道这样的事情是怎么发生的吗?

根据鼠标位置处理显示/隐藏的关键是使用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)
}
}

最新更新