【问题标题】:How can I show/hide a button added to the title bar of an NSWindow?如何显示/隐藏添加到 NSWindow 标题栏的按钮?
【发布时间】:2020-07-27 21:37:48
【问题描述】:

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

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

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

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

【问题讨论】:

  • 您指的是 Pages 中的哪个按钮? NSTrackingArea 是你要找的吗?
  • 该按钮是“向下 V 形”按钮,当鼠标悬停在该区域时,该按钮会出现在标题栏中的文档名称旁边。按下它可以将文档移动到另一个位置。我正在尝试模仿这种行为,尽管在我的情况下,下来的工作表并没有让您移动文档,而是更改了我的应用程序正在监视的端口。我试过NSTrackingArea,但到目前为止还没有设法解决这个问题。现在我在想我必须继承 NSWindow 这不会是世界末日,但也不理想。
  • 隐藏图片而不是按钮怎么样?
  • 我现在有了解决问题的办法。它使用@Willeke 建议的NSTrackingArea,但我必须做的是向菜单栏添加一个附加对象。具体来说,我从设置跟踪区域和mouseEnteredmouseExited 方法的自定义类中添加了一个不可见视图。该跟踪区域(覆盖整个标题栏)然后启用/禁用感兴趣的按钮。
  • 我现在将清理我的代码,如果版主重新打开此问题,我将发布完整的答案。基本上我现在应该有一个 NSWindow 扩展,它添加了一个方法 addTitleActionButton 来放置所有东西。

标签: swift macos cocoa events notifications


【解决方案1】:

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

【讨论】:

    猜你喜欢
    • 2015-04-06
    • 1970-01-01
    • 2015-01-27
    • 1970-01-01
    • 1970-01-01
    • 2010-11-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多