Scroll view support

This commit is contained in:
Ivan Sapozhnik
2020-04-16 20:07:50 +02:00
parent 1cf55aa859
commit aad10c007f
4 changed files with 194 additions and 12 deletions
+64 -8
View File
@@ -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
+10
View File
@@ -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)
}
+2 -4
View File
@@ -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
}
}
+118
View File
@@ -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
}
}