From aad10c007fe97f59a62f11c14059cdcc08032112 Mon Sep 17 00:00:00 2001 From: Ivan Sapozhnik Date: Thu, 16 Apr 2020 20:07:50 +0200 Subject: [PATCH] Scroll view support --- Sources/Menu/ContentViewController.swift | 72 ++++++++++++-- Sources/Menu/MenuConfiguration.swift | 10 ++ Sources/Menu/MenuElement.swift | 6 +- Sources/Menu/MenuScroller.swift | 118 +++++++++++++++++++++++ 4 files changed, 194 insertions(+), 12 deletions(-) create mode 100644 Sources/Menu/MenuScroller.swift diff --git a/Sources/Menu/ContentViewController.swift b/Sources/Menu/ContentViewController.swift index a8aa410..4418bd9 100644 --- a/Sources/Menu/ContentViewController.swift +++ b/Sources/Menu/ContentViewController.swift @@ -11,6 +11,32 @@ protocol ContentViewControllerDelegate: AnyObject { func didClickMenuElement(with index: Int) } +final class FlippedClipView: NSClipView { + override var isFlipped: Bool { + return true + } +} + +//final class MenuScroller: NSScroller { +// override init(frame frameRect: NSRect) { +// super.init(frame: frameRect) +// scrollerStyle = .overlay +// knobStyle = .light +// } +// +// required init?(coder: NSCoder) { +// fatalError("init(coder:) has not been implemented") +// } +// +// override class func scrollerWidth(for controlSize: NSControl.ControlSize, scrollerStyle: NSScroller.Style) -> CGFloat { +// return 30.0 +// } +// +// override var usableParts: NSScroller.UsableParts { +// return .noScrollerParts +// } +//} + class ContentViewController: NSViewController { weak var delegate: ContentViewControllerDelegate? @@ -29,6 +55,19 @@ class ContentViewController: NSViewController { return stackView }() + private let clipView: FlippedClipView = { + let clipView = FlippedClipView() + clipView.drawsBackground = false + return clipView + }() + + private let scrollView: NSScrollView = { + let scrollView = NSScrollView() + scrollView.verticalScroller = MenuScroller(frame: .zero) + scrollView.drawsBackground = false + return scrollView + }() + init(with titleString: String?, menuItems: [MenuItem], selectedIndex: Int, configuration: Configuration) { self.titleString = titleString self.menuItems = menuItems @@ -61,19 +100,36 @@ class ContentViewController: NSViewController { ]) } + scrollView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(scrollView) + stackView.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(stackView) + scrollView.contentView = clipView + scrollView.documentView = stackView NSLayoutConstraint.activate([ - stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - stackView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -configuration.contentEdgeInsets.bottom) + scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -configuration.contentEdgeInsets.bottom), +// scrollView.heightAnchor.constraint(equalToConstant: 200), + + stackView.leadingAnchor.constraint(equalTo: clipView.leadingAnchor), + stackView.trailingAnchor.constraint(equalTo: clipView.trailingAnchor), + stackView.topAnchor.constraint(equalTo: clipView.topAnchor), + stackView.widthAnchor.constraint(equalTo: clipView.widthAnchor), ]) - if let titleLabel = titleLabel { - stackView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: configuration.titleBottomSpace).isActive = true + scrollView.hasVerticalScroller = configuration.maximumContentHeight != nil + if let maxContentHeight = configuration.maximumContentHeight { + scrollView.heightAnchor.constraint(equalToConstant: abs(maxContentHeight)).isActive = true } else { - stackView.topAnchor.constraint(equalTo: view.topAnchor, constant: configuration.contentEdgeInsets.top).isActive = true + stackView.bottomAnchor.constraint(equalTo: clipView.bottomAnchor).isActive = true + } + + if let titleLabel = titleLabel { + scrollView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: configuration.titleBottomSpace).isActive = true + } else { + scrollView.topAnchor.constraint(equalTo: view.topAnchor, constant: configuration.contentEdgeInsets.top).isActive = true } menuItems.enumerated().forEach { index, item in @@ -141,7 +197,7 @@ class ContentViewController: NSViewController { label.isEditable = false label.isBordered = false label.font = configuration.titleFont - label.textColor = configuration.menuItemTextColor + label.textColor = configuration.titleColor switch configuration.textAlignment { case .left: label.alignment = .left diff --git a/Sources/Menu/MenuConfiguration.swift b/Sources/Menu/MenuConfiguration.swift index 8345f97..0ba2996 100644 --- a/Sources/Menu/MenuConfiguration.swift +++ b/Sources/Menu/MenuConfiguration.swift @@ -49,11 +49,13 @@ extension Padding.Vertical { public protocol Configuration { var titleBottomSpace: CGFloat { get } var titleFont: NSFont? { get } + var titleColor: NSColor { get } var backgroundColor: NSColor { get } var cornerRadius: CGFloat { get } var hasShadow: Bool { get } var appearsBelowSender: Bool { get } var contentEdgeInsets: NSEdgeInsets { get } + var maximumContentHeight: CGFloat? { get } var separatorColor: NSColor { get } var separatorThickness: CGFloat { get } var separatorHorizontalPadding: Padding.Horizontal { get } @@ -86,6 +88,10 @@ open class MenuConfiguration: Configuration { return NSFont.systemFont(ofSize: 18, weight: .light) } + open var titleColor: NSColor { + return NSColor.white + } + open var backgroundColor: NSColor { return NSColor.init(calibratedRed: 84/255, green: 181/255, blue: 146/255, alpha: 1.0) } @@ -106,6 +112,10 @@ open class MenuConfiguration: Configuration { return NSEdgeInsets(top: .grid2, left: .grid2, bottom: .grid2, right: .grid2) } + open var maximumContentHeight: CGFloat? { + return nil + } + open var separatorColor: NSColor { return NSColor.init(calibratedRed: 76/255, green: 161/255, blue: 132/255, alpha: 1.0) } diff --git a/Sources/Menu/MenuElement.swift b/Sources/Menu/MenuElement.swift index d301477..1d103a3 100644 --- a/Sources/Menu/MenuElement.swift +++ b/Sources/Menu/MenuElement.swift @@ -144,8 +144,7 @@ class MenuElement: NSView { rImageView.translatesAutoresizingMaskIntoConstraints = false rImageView.image = image if #available(OSX 10.14, *) { - if let menuItemImageTintColor = configuration.menuItemImageTintColor { - assert(image?.isTemplate ?? true, "In order to set an image tint color, the image should have template rendering mode") + if let menuItemImageTintColor = configuration.menuItemImageTintColor, let image = image, image.isTemplate { rImageView.contentTintColor = menuItemImageTintColor } } @@ -159,8 +158,7 @@ class MenuElement: NSView { lImageView.imageScaling = .scaleProportionallyUpOrDown lImageView.image = image if #available(OSX 10.14, *) { - if let menuItemImageTintColor = configuration.menuItemImageTintColor { - assert(image?.isTemplate ?? true, "In order to set an image tint color, the image should have template rendering mode") + if let menuItemImageTintColor = configuration.menuItemImageTintColor, let image = image, image.isTemplate { lImageView.contentTintColor = menuItemImageTintColor } } diff --git a/Sources/Menu/MenuScroller.swift b/Sources/Menu/MenuScroller.swift new file mode 100644 index 0000000..158cc63 --- /dev/null +++ b/Sources/Menu/MenuScroller.swift @@ -0,0 +1,118 @@ +// +// File.swift +// +// +// Created by Ivan Sapozhnik on 15.04.20. +// + +import Cocoa + +class MenuScroller: NSScroller { + + // MARK: - + // MARK: Variables + + override var floatValue: Float { + get { + return super.floatValue + } + set { + super.floatValue = newValue + animator().alphaValue = 1.0 + NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(fadeOut), object: nil) + perform(#selector(fadeOut), with: nil, afterDelay: 1.0) + } + } + + // MARK: - + // MARK: Initialization + + required init?(coder: NSCoder) { + super.init(coder: coder) + self.initialize() + } + + override init(frame frameRect: NSRect) { + super.init(frame: frameRect) + self.initialize() + } + + override func awakeFromNib() { + super.awakeFromNib() + self.initialize() + } + + func initialize() { + let trackingArea = NSTrackingArea.init(rect: self.bounds, + options: [.mouseEnteredAndExited, .activeInActiveApp, .mouseMoved], + owner: self, + userInfo: nil) + self.addTrackingArea(trackingArea) + } + + // MARK: - + // MARK: Custom Methods + + @objc func fadeOut() { + NSAnimationContext.runAnimationGroup({ (context) in + context.duration = 0.25 + self.animator().alphaValue = 0.3 + }) {} + } + + // MARK: - + // MARK: NSView Method Overrides + + override func draw(_ dirtyRect: NSRect) { + // Only draw the knob. drawRect: should only be invoked when overlay scrollers are not used + self.drawKnob() + self.drawKnobSlot(in: bounds, highlight: false) + } + + // MARK: - + // MARK: - NSScroller Method Overrides + + override func drawKnob() { + NSColor.white.setFill() + + let dx, dy: CGFloat +// if isHorizontal { +// dx = 0; dy = 3 +// } else { + dx = 5; dy = 0 +// } + + let frame = rect(for: .knob).insetBy(dx: dx, dy: dy) + NSBezierPath.init(roundedRect: frame, xRadius: 3, yRadius: 3).fill() + } + + override func drawKnobSlot(in slotRect: NSRect, highlight flag: Bool) { + // Don't draw the background. Should only be invoked when using overlay scrollers + NSColor.init(white: 1.0, alpha: 0.15).setFill() + let frame = rect(for: .knobSlot).insetBy(dx: 3, dy: 0) + NSBezierPath.init(roundedRect: frame, xRadius: 5, yRadius: 5).fill() + + } + + // MARK: - + // MARK: NSResponder Method Overrides + + override func mouseExited(with event: NSEvent) { + super.mouseExited(with: event) + self.fadeOut() + } + + override func mouseEntered(with event: NSEvent) { + super.mouseEntered(with: event) + NSAnimationContext.runAnimationGroup({ (context) in + context.duration = 0.1 + self.animator().alphaValue = 1.0 + }) {} + NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(self.fadeOut), object: nil) + } + + override func mouseMoved(with event: NSEvent) { + super.mouseMoved(with: event) + self.alphaValue = 1.0 //TODO prevent multiple calls + } +}