// // MenuElement.swift // PrivadoVPN // // Created by Murad Shabanov on 05.08.2021. // Copyright © 2021 Privado LLC. All rights reserved. // import Cocoa protocol MenuElementDelegate: AnyObject { func didClickMenuElement(_ menuElement: MenuElement) } class MenuElement: NSView { let handler: () -> Void weak var delegate: MenuElementDelegate? private let configuration: Configuration private var checkmark: CheckmarkView! init(with menuItem: WindowMenuItem, isSelected: Bool = false, configuration: Configuration) { self.configuration = configuration handler = menuItem.action ?? {} super.init(frame: .zero) alphaValue = menuItem.isEnabled ? 1.0 : 0.5 if let customView = menuItem.customView { makeCustomViewElement(with: customView) } else { makeStandardElement(with: menuItem, isSelected: isSelected) } } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @objc private func menuElementClicked(_ sender: Control) { handler() delegate?.didClickMenuElement(self) } private func makeCustomViewElement(with customView: NSView) { customView.translatesAutoresizingMaskIntoConstraints = false addSubview(customView) let leadingConstraint: NSLayoutConstraint let trailingConstraint: NSLayoutConstraint switch configuration.textAlignment { case .left: leadingConstraint = customView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: configuration.contentEdgeInsets.left) trailingConstraint = customView.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor, constant: -configuration.contentEdgeInsets.right) case .right: leadingConstraint = customView.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor, constant: configuration.contentEdgeInsets.left) trailingConstraint = customView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -configuration.contentEdgeInsets.right) } NSLayoutConstraint.activate([ leadingConstraint, trailingConstraint, customView.topAnchor.constraint(equalTo: topAnchor), customView.bottomAnchor.constraint(equalTo: bottomAnchor) ]) } private func makeStandardElement(with menuItem: WindowMenuItem, isSelected: Bool) { let stackView = makeHorizontalStackView() var lImageView: NSImageView? = nil var rImageView: NSImageView? = nil let image = menuItem.image switch configuration.iconAlignment { case .left: lImageView = makeLeftImageView(with: image) case .right: rImageView = makeRightImageView(with: image) } let label = makeLabel(with: menuItem.title) if let lImageView = lImageView { stackView.addArrangedSubview(lImageView) NSLayoutConstraint.activate([ lImageView.heightAnchor.constraint(equalTo: lImageView.widthAnchor, multiplier: 1.0), lImageView.centerYAnchor.constraint(equalTo: stackView.centerYAnchor), ]) lImageView.heightAnchor.constraint(equalToConstant: configuration.menuItemImageHeight ?? 0.0).isActive = configuration.menuItemImageHeight != nil } stackView.addArrangedSubview(label) if let rImageView = rImageView { stackView.addArrangedSubview(rImageView) NSLayoutConstraint.activate([ rImageView.heightAnchor.constraint(equalTo: rImageView.widthAnchor, multiplier: 1.0), rImageView.centerYAnchor.constraint(equalTo: stackView.centerYAnchor), ]) rImageView.heightAnchor.constraint(equalToConstant: configuration.menuItemImageHeight ?? 0.0).isActive = configuration.menuItemImageHeight != nil } if configuration.rememberSelection { let checkmarkView = CheckmarkView(with: configuration) checkmarkView.translatesAutoresizingMaskIntoConstraints = false switch configuration.iconAlignment { case .left: stackView.addArrangedSubview(checkmarkView) case .right: stackView.insertArrangedSubview(checkmarkView, at: 0) } if isSelected { checkmarkView.animate(duration: 0) if #available(OSX 10.14, *) { lImageView?.contentTintColor = isSelected ? configuration.menuItemHoverImageTintColor : configuration.menuItemImageTintColor } label.textColor = isSelected ? configuration.menuItemHoverTextColor : configuration.menuItemTextColor } checkmark = checkmarkView } let control = makeHoverControl(update: label, leftImageView: lImageView, rightImageView: rImageView, isEnabled: menuItem.isEnabled, isSelected: isSelected) addSubview(control) addSubview(stackView) NSLayoutConstraint.activate([ heightAnchor.constraint(equalToConstant: configuration.menuItemHeight), widthAnchor.constraint(equalToConstant: configuration.menuItemWidth), control.leadingAnchor.constraint(equalTo: leadingAnchor), control.trailingAnchor.constraint(equalTo: trailingAnchor), control.topAnchor.constraint(equalTo: topAnchor), control.bottomAnchor.constraint(equalTo: bottomAnchor), stackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: configuration.contentEdgeInsets.left), stackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -configuration.contentEdgeInsets.right), stackView.topAnchor.constraint(equalTo: topAnchor), stackView.bottomAnchor.constraint(equalTo: bottomAnchor), ]) if configuration.rememberSelection, let checkmarkView = checkmark { NSLayoutConstraint.activate([ checkmarkView.heightAnchor.constraint(equalTo: checkmarkView.widthAnchor, multiplier: 1.0), checkmarkView.centerYAnchor.constraint(equalTo: stackView.centerYAnchor), checkmarkView.widthAnchor.constraint(equalToConstant: configuration.menuItemCheckmarkHeight) ]) } } private func makeHorizontalStackView() -> NSStackView { let stackView = NSStackView() stackView.translatesAutoresizingMaskIntoConstraints = false stackView.orientation = .horizontal stackView.distribution = .fill stackView.spacing = configuration.menuItemHorizontalSpacing return stackView } private func makeLabel(with text: String) -> NSTextField { let label = NSTextField() label.translatesAutoresizingMaskIntoConstraints = false label.cell = VerticallyCenteredTextFieldCell() label.cell?.lineBreakMode = .byTruncatingTail label.maximumNumberOfLines = 1 label.backgroundColor = .clear label.stringValue = text label.isEditable = false label.isBordered = false label.font = configuration.menuItemFont label.textColor = configuration.menuItemTextColor switch configuration.textAlignment { case .left: label.alignment = .left case .right: label.alignment = .right } return label } private func makeRightImageView(with image: NSImage?) -> NSImageView { let rImageView = NSImageView() rImageView.translatesAutoresizingMaskIntoConstraints = false rImageView.image = image if #available(OSX 10.14, *) { if let menuItemImageTintColor = configuration.menuItemImageTintColor, let image = image, image.isTemplate { rImageView.contentTintColor = menuItemImageTintColor } } rImageView.isHidden = image == nil return rImageView } private func makeLeftImageView(with image: NSImage?) -> NSImageView { let lImageView = NSImageView() lImageView.translatesAutoresizingMaskIntoConstraints = false lImageView.imageScaling = .scaleProportionallyUpOrDown lImageView.image = image if #available(OSX 10.14, *) { if let menuItemImageTintColor = configuration.menuItemImageTintColor, let image = image, image.isTemplate { lImageView.contentTintColor = menuItemImageTintColor } } lImageView.isHidden = image == nil return lImageView } private func makeHoverControl(update label: NSTextField, leftImageView: NSImageView?, rightImageView: NSImageView?, isEnabled: Bool, isSelected: Bool) -> Control { let control = Control(with: configuration) control.isEnabled = isEnabled control.hover = { [weak self] isHover in guard let self = self else { return } label.textColor = isHover ? self.configuration.menuItemHoverTextColor : isSelected ? self.configuration.menuItemHoverImageTintColor : self.configuration.menuItemTextColor if #available(OSX 10.14, *) { leftImageView?.contentTintColor = isHover ? self.configuration.menuItemHoverImageTintColor : isSelected ? self.configuration.menuItemHoverImageTintColor : self.configuration.menuItemImageTintColor rightImageView?.contentTintColor = isHover ? self.configuration.menuItemHoverImageTintColor : isSelected ? self.configuration.menuItemHoverImageTintColor : self.configuration.menuItemImageTintColor } } control.translatesAutoresizingMaskIntoConstraints = false control.target = self control.action = #selector(menuElementClicked(_:)) control.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) return control } }