首页 > 解决方案 > 如何显示/隐藏添加到 NSWindow 标题栏的按钮?

问题描述

我在NSWindow扩展中创建了一个方法,允许我在标题栏中的文本旁边添加一个按钮。这类似于出现在 Pages and Numbers 标题栏中的“向下 V 形”按钮。单击按钮时,将运行表示为闭包的任意代码。

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

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

为了做到这一点,我相信我需要注入一个额外的处理程序/侦听器,它会告诉我鼠标何时进入和离开适当的区域。我可以使用 定义必要的区域NSTrackingArea,但我还没有弄清楚如何在不需要子类的情况下“注入”事件侦听器。有谁知道(或是否)这样的事情是可能的?

标签: swiftmacoscocoaeventsnotifications

解决方案


根据鼠标位置处理显示/隐藏的关键是使用 anNSTrackingArea来表示我们感兴趣的部分,并处理鼠标进入和鼠标退出事件。但是由于这不能直接在标题栏视图上完成(因为我们必须对视图进行子类化才能添加事件处理程序),我们需要创建一个额外的 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)
    }
}

推荐阅读