Scroll view support
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user