Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6799c5eeec |
+1
-1
@@ -6,7 +6,7 @@ import PackageDescription
|
||||
let package = Package(
|
||||
name: "Popover",
|
||||
platforms: [
|
||||
.macOS(.v10_11)
|
||||
.macOS(.v10_14)
|
||||
],
|
||||
products: [
|
||||
// Products define the executables and libraries produced by a package, and make them visible to other packages.
|
||||
|
||||
@@ -8,25 +8,13 @@
|
||||
import Cocoa
|
||||
|
||||
class EventMonitor {
|
||||
enum MonitorType {
|
||||
case global
|
||||
case local
|
||||
}
|
||||
|
||||
typealias GlobalEventHandler = (NSEvent?) -> Void
|
||||
typealias LocalEventHandler = (NSEvent?) -> NSEvent?
|
||||
|
||||
private var monitor: Any?
|
||||
private let mask: NSEvent.EventTypeMask
|
||||
private let monitorType: MonitorType
|
||||
private let globalHandler: GlobalEventHandler?
|
||||
private let localHandler: LocalEventHandler?
|
||||
private let handler: (NSEvent?) -> Void
|
||||
|
||||
init(monitorType: MonitorType, mask: NSEvent.EventTypeMask, globalHandler: GlobalEventHandler?, localHandler: LocalEventHandler?) {
|
||||
init(mask: NSEvent.EventTypeMask, handler: @escaping (NSEvent?) -> Void) {
|
||||
self.mask = mask
|
||||
self.monitorType = monitorType
|
||||
self.globalHandler = globalHandler
|
||||
self.localHandler = localHandler
|
||||
self.handler = handler
|
||||
}
|
||||
|
||||
deinit {
|
||||
@@ -34,12 +22,7 @@ class EventMonitor {
|
||||
}
|
||||
|
||||
func start() {
|
||||
switch monitorType {
|
||||
case .global:
|
||||
startGlobalMonitor()
|
||||
case .local:
|
||||
startLocalMonitor()
|
||||
}
|
||||
monitor = NSEvent.addGlobalMonitorForEvents(matching: mask, handler: handler)
|
||||
}
|
||||
|
||||
func stop() {
|
||||
@@ -48,20 +31,4 @@ class EventMonitor {
|
||||
NSEvent.removeMonitor(monitor)
|
||||
self.monitor = nil
|
||||
}
|
||||
|
||||
private func startGlobalMonitor() {
|
||||
guard let handler = globalHandler else {
|
||||
assertionFailure("Global event handler is not set.")
|
||||
return
|
||||
}
|
||||
monitor = NSEvent.addGlobalMonitorForEvents(matching: mask, handler: handler)
|
||||
}
|
||||
|
||||
private func startLocalMonitor() {
|
||||
guard let handler = localHandler else {
|
||||
assertionFailure("Local event handler is not set.")
|
||||
return
|
||||
}
|
||||
monitor = NSEvent.addLocalMonitorForEvents(matching: mask, handler: handler)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
//
|
||||
// File.swift
|
||||
//
|
||||
//
|
||||
// Created by Ivan Sapozhnik on 06.04.20.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
|
||||
extension CAEdgeAntialiasingMask {
|
||||
static var all: CAEdgeAntialiasingMask {
|
||||
return [.layerLeftEdge, .layerRightEdge, .layerBottomEdge, .layerTopEdge]
|
||||
}
|
||||
}
|
||||
|
||||
extension Array {
|
||||
var isNotEmpty: Bool {
|
||||
return !isEmpty
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
//
|
||||
// File.swift
|
||||
//
|
||||
//
|
||||
// Created by Ivan Sapozhnik on 07.04.20.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension Popover {
|
||||
enum MenuItemType {
|
||||
case item(MenuItem)
|
||||
case separator
|
||||
}
|
||||
|
||||
class MenuItem: NSObject {
|
||||
let title: String
|
||||
let key: String
|
||||
let _action: () -> Void
|
||||
|
||||
public convenience init(title: String, action: @escaping () -> Void) {
|
||||
self.init(title: title, key: "", action: action)
|
||||
}
|
||||
|
||||
public init(title: String, key: String, action: @escaping () -> Void) {
|
||||
self.title = title
|
||||
self.key = key
|
||||
_action = action
|
||||
}
|
||||
|
||||
@objc
|
||||
func action() {
|
||||
_action()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,105 +8,59 @@
|
||||
import Cocoa
|
||||
|
||||
public class Popover: NSObject {
|
||||
var item: NSStatusItem! {
|
||||
didSet {
|
||||
prepareMenu()
|
||||
}
|
||||
}
|
||||
var item: NSStatusItem!
|
||||
|
||||
private let wConfig: PopoverConfiguration
|
||||
private var popoverWindowController: PopoverWindowController?
|
||||
private var globalEventMonitor: EventMonitor?
|
||||
private var localEventMonitor: EventMonitor?
|
||||
private var menu: NSMenu?
|
||||
private var menuItems: [MenuItemType]?
|
||||
private var statusItemContainer: StatusItemContainerView?
|
||||
private var eventMonitor: EventMonitor?
|
||||
|
||||
private var isPopoverWindowVisible: Bool {
|
||||
return (popoverWindowController != nil) ? popoverWindowController!.windowIsOpen : false
|
||||
}
|
||||
|
||||
/// Convenience initializer which is using default configuration
|
||||
public convenience override init() {
|
||||
self.init(with: DefaultConfiguration(), menuItems: nil)
|
||||
}
|
||||
|
||||
/// Convenience initializer which is using default configuration
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - menuItems: Array of items that NSMenu will show
|
||||
public convenience init(with menuItems: [MenuItemType]?) {
|
||||
self.init(with: DefaultConfiguration(), menuItems: menuItems)
|
||||
}
|
||||
|
||||
/// Creates a Popover with given PopoverConfiguration. Popover configuration can be either `DefaultConfiguration` or you can sublclass this class and override some of the properties.
|
||||
///
|
||||
/// By default Popover is using Event Monitor and by doing left or right click ouside of the Popover's frame, the Popover will be automatically dismissed.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - windowConfiguration: Popover window configuration. By default `DefaultConfiguration` instance is being used.
|
||||
/// - menuItems: Array of items that NSMenu will show
|
||||
public init(with windowConfiguration: PopoverConfiguration, menuItems: [MenuItemType]?) {
|
||||
public init(with windowConfiguration: PopoverConfiguration) {
|
||||
wConfig = windowConfiguration
|
||||
self.menuItems = menuItems
|
||||
|
||||
super.init()
|
||||
|
||||
setupMonitors()
|
||||
eventMonitor = EventMonitor(mask: [.rightMouseDown, .leftMouseDown], handler: { [weak self] event in
|
||||
guard let self = self else { return }
|
||||
self.dismiss()
|
||||
})
|
||||
}
|
||||
|
||||
/// Convenience initializer which is using default configuration
|
||||
public convenience override init() {
|
||||
self.init(with: DefaultConfiguration())
|
||||
}
|
||||
|
||||
/// Creates a Popover with given image and contentViewController
|
||||
///
|
||||
/// By default it won't present any Popover until the user clicks on status bar item
|
||||
public func prepare(with image: NSImage, contentViewController viewController: NSViewController) {
|
||||
public func createPopover(with image: NSImage, contentViewController viewController: NSViewController) {
|
||||
configureStatusBarButton(with: image)
|
||||
popoverWindowController = PopoverWindowController(with: self, contentViewController: viewController, windowConfiguration: wConfig)
|
||||
localEventMonitor?.start()
|
||||
}
|
||||
|
||||
/// Creates a Popover with given view and contentViewController. The view's height will be scaled down to 18pt and also will be vertically aligned. The wdith can be any.
|
||||
///
|
||||
/// By default it won't present any Popover until the user clicks on status bar item
|
||||
public func prepare(with view: NSView, contentViewController viewController: NSViewController) {
|
||||
public func createPopover(with view: NSView, contentViewController viewController: NSViewController) {
|
||||
configureStatusBarButton(with: view)
|
||||
popoverWindowController = PopoverWindowController(with: self, contentViewController: viewController, windowConfiguration: wConfig)
|
||||
localEventMonitor?.start()
|
||||
}
|
||||
|
||||
/// Shows the Popover with no animation
|
||||
public func show() {
|
||||
guard !isPopoverWindowVisible else { return }
|
||||
popoverWindowController?.show()
|
||||
globalEventMonitor?.start()
|
||||
eventMonitor?.start()
|
||||
}
|
||||
|
||||
/// Dismisses the Popover with default animation. Animation behavior is `AnimationBehavior.utilityWindow`
|
||||
/// Dismisses the Popover with default animation. Animation behaviour is `AnimationBehavior.utilityWindow`
|
||||
public func dismiss() {
|
||||
guard isPopoverWindowVisible else { return }
|
||||
popoverWindowController?.dismiss()
|
||||
globalEventMonitor?.stop()
|
||||
}
|
||||
|
||||
private func setupMonitors() {
|
||||
globalEventMonitor = EventMonitor(monitorType: .global, mask: [.leftMouseDown, .rightMouseDown], globalHandler: { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
self.dismiss()
|
||||
}, localHandler: nil)
|
||||
|
||||
if menuItems != nil, menuItems?.isNotEmpty ?? false {
|
||||
localEventMonitor = EventMonitor(monitorType: .local, mask: [.leftMouseDown, .rightMouseDown], globalHandler: nil, localHandler: { [weak self] event -> NSEvent? in
|
||||
guard let self = self, let currentEvent = event else { return event }
|
||||
|
||||
let isRightMouseClick = currentEvent.type == .rightMouseDown
|
||||
let isControlLeftClick = currentEvent.type == .leftMouseDown && currentEvent.modifierFlags.contains(.control)
|
||||
|
||||
if isRightMouseClick || isControlLeftClick {
|
||||
self.dismiss()
|
||||
self.item.menu = self.menu
|
||||
}
|
||||
|
||||
return event
|
||||
})
|
||||
}
|
||||
eventMonitor?.stop()
|
||||
}
|
||||
|
||||
private func configureStatusBarButton(with image: NSImage) {
|
||||
@@ -114,8 +68,9 @@ public class Popover: NSObject {
|
||||
guard let button = item.button else { return }
|
||||
|
||||
image.isTemplate = true
|
||||
button.target = self
|
||||
button.action = #selector(handleStatusItemButtonAction(_:))
|
||||
button.image = image
|
||||
setTargetAction(for: button)
|
||||
}
|
||||
|
||||
private func configureStatusBarButton(with view: NSView) {
|
||||
@@ -124,43 +79,17 @@ public class Popover: NSObject {
|
||||
|
||||
let statusItemContainer = StatusItemContainerView()
|
||||
|
||||
setTargetAction(for: button)
|
||||
button.target = self
|
||||
button.action = #selector(handleStatusItemButtonAction(_:))
|
||||
button.addSubview(statusItemContainer)
|
||||
|
||||
statusItemContainer.embed(view)
|
||||
|
||||
button.frame = statusItemContainer.frame
|
||||
self.statusItemContainer = statusItemContainer
|
||||
}
|
||||
|
||||
@objc private func handleStatusItemButtonAction(_ sender: Any?) {
|
||||
isPopoverWindowVisible ? dismiss() : show()
|
||||
}
|
||||
|
||||
private func setTargetAction(for button: NSButton) {
|
||||
button.target = self
|
||||
button.action = #selector(handleStatusItemButtonAction(_:))
|
||||
}
|
||||
|
||||
private func prepareMenu() {
|
||||
guard menuItems != nil, menuItems?.isNotEmpty ?? false else { return }
|
||||
|
||||
let menu = NSMenu()
|
||||
menuItems?.forEach { menuItemType in
|
||||
switch menuItemType {
|
||||
case .item(let actionItem):
|
||||
let menuItem = NSMenuItem(title: actionItem.title, action: #selector(MenuItem.action), keyEquivalent: actionItem.key)
|
||||
menuItem.target = actionItem
|
||||
menu.addItem(menuItem)
|
||||
case .separator:
|
||||
menu.addItem(NSMenuItem.separator())
|
||||
}
|
||||
}
|
||||
|
||||
menu.delegate = self
|
||||
self.menu = menu
|
||||
}
|
||||
}
|
||||
|
||||
extension Popover: NSMenuDelegate {
|
||||
public func menuDidClose(_ menu: NSMenu) {
|
||||
item.menu = nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,20 +17,9 @@ class PopoverWindow: NSPanel {
|
||||
private var childContentView: NSView?
|
||||
private var backgroundView: PopoverWindowBackgroundView?
|
||||
|
||||
private init(
|
||||
contentRect: NSRect,
|
||||
styleMask style: NSWindow.StyleMask,
|
||||
backing backingStoreType: NSWindow.BackingStoreType,
|
||||
defer flag: Bool,
|
||||
configuration: PopoverConfiguration
|
||||
) {
|
||||
private init(contentRect: NSRect, styleMask style: NSWindow.StyleMask, backing backingStoreType: NSWindow.BackingStoreType, defer flag: Bool, configuration: PopoverConfiguration) {
|
||||
wConfig = configuration
|
||||
super.init(
|
||||
contentRect: contentRect,
|
||||
styleMask: style,
|
||||
backing: backingStoreType,
|
||||
defer: flag
|
||||
)
|
||||
super.init(contentRect: contentRect, styleMask: style, backing: backingStoreType, defer: flag)
|
||||
|
||||
isOpaque = false
|
||||
hasShadow = true
|
||||
@@ -61,10 +50,12 @@ class PopoverWindow: NSPanel {
|
||||
set {
|
||||
guard childContentView != newValue, let bounds = newValue?.bounds else { return }
|
||||
|
||||
let antialiasingMask: CAEdgeAntialiasingMask = [.layerLeftEdge, .layerRightEdge, .layerBottomEdge, .layerTopEdge]
|
||||
|
||||
backgroundView = super.contentView as? PopoverWindowBackgroundView
|
||||
if (backgroundView == nil) {
|
||||
backgroundView = PopoverWindowBackgroundView(frame: bounds, windowConfiguration: wConfig)
|
||||
backgroundView?.layer?.edgeAntialiasingMask = .all
|
||||
backgroundView?.layer?.edgeAntialiasingMask = antialiasingMask
|
||||
super.contentView = backgroundView
|
||||
}
|
||||
|
||||
@@ -77,7 +68,7 @@ class PopoverWindow: NSPanel {
|
||||
childContentView?.wantsLayer = true
|
||||
childContentView?.layer?.cornerRadius = wConfig.cornerRadius
|
||||
childContentView?.layer?.masksToBounds = true
|
||||
childContentView?.layer?.edgeAntialiasingMask = .all
|
||||
childContentView?.layer?.edgeAntialiasingMask = antialiasingMask
|
||||
|
||||
guard let userContentView = self.childContentView, let backgroundView = self.backgroundView else { return }
|
||||
backgroundView.addSubview(userContentView)
|
||||
@@ -106,7 +97,7 @@ class PopoverWindow: NSPanel {
|
||||
}
|
||||
|
||||
private func updateArrowPosition() {
|
||||
guard let backgroundView = self.backgroundView else { return }
|
||||
guard let backgroundView = self.backgroundView as? PopoverWindowBackgroundView else { return }
|
||||
backgroundView.arrowXLocation = arrowXLocation
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,9 @@ import Cocoa
|
||||
|
||||
class PopoverWindowBackgroundView: NSView {
|
||||
private let wConfig: PopoverConfiguration
|
||||
|
||||
private var visualEffect: NSVisualEffectView?
|
||||
|
||||
var arrowXLocation: CGFloat = 0.0 {
|
||||
didSet {
|
||||
needsDisplay = true
|
||||
@@ -18,6 +21,36 @@ class PopoverWindowBackgroundView: NSView {
|
||||
init(frame frameRect: NSRect, windowConfiguration: PopoverConfiguration) {
|
||||
self.wConfig = windowConfiguration
|
||||
super.init(frame: frameRect)
|
||||
|
||||
// setupVisualEffect()
|
||||
}
|
||||
|
||||
private func setupVisualEffect() {
|
||||
let visualEffect = NSVisualEffectView()
|
||||
visualEffect.translatesAutoresizingMaskIntoConstraints = false
|
||||
visualEffect.blendingMode = .behindWindow
|
||||
visualEffect.state = .active
|
||||
visualEffect.appearance = NSAppearance.current
|
||||
guard let appearence = NSAppearance.current else { return }
|
||||
switch appearence.name {
|
||||
case .aqua:
|
||||
visualEffect.material = .dark
|
||||
case .darkAqua:
|
||||
visualEffect.material = .dark
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
self.visualEffect = visualEffect
|
||||
|
||||
addSubview(visualEffect)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
visualEffect.leadingAnchor.constraint(equalTo: self.leadingAnchor),
|
||||
visualEffect.trailingAnchor.constraint(equalTo: self.trailingAnchor),
|
||||
visualEffect.topAnchor.constraint(equalTo: self.topAnchor),
|
||||
visualEffect.bottomAnchor.constraint(equalTo: self.bottomAnchor)
|
||||
])
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
@@ -72,10 +105,16 @@ class PopoverWindowBackgroundView: NSView {
|
||||
windowPath.stroke()
|
||||
}
|
||||
|
||||
|
||||
wConfig.backgroundColor.setFill()
|
||||
windowPath.fill()
|
||||
|
||||
let antialiasingMask: CAEdgeAntialiasingMask = [.layerLeftEdge, .layerRightEdge, .layerBottomEdge, .layerTopEdge]
|
||||
let mask = CAShapeLayer()
|
||||
mask.edgeAntialiasingMask = antialiasingMask
|
||||
mask.path = windowPath.cgPath
|
||||
visualEffect?.layer?.mask = mask
|
||||
|
||||
|
||||
// NSColor.red.setFill()
|
||||
// controlPoints.fill()
|
||||
}
|
||||
@@ -86,3 +125,27 @@ class PopoverWindowBackgroundView: NSView {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension NSBezierPath {
|
||||
var cgPath: CGPath {
|
||||
let path = CGMutablePath()
|
||||
var points = [CGPoint](repeating: .zero, count: 3)
|
||||
for i in 0 ..< self.elementCount {
|
||||
let type = self.element(at: i, associatedPoints: &points)
|
||||
|
||||
switch type {
|
||||
case .moveTo:
|
||||
path.move(to: points[0])
|
||||
case .lineTo:
|
||||
path.addLine(to: points[0])
|
||||
case .curveTo:
|
||||
path.addCurve(to: points[2], control1: points[0], control2: points[1])
|
||||
case .closePath:
|
||||
path.closeSubpath()
|
||||
@unknown default:
|
||||
break
|
||||
}
|
||||
}
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,4 +82,5 @@ class PopoverWindowController: NSWindowController {
|
||||
|
||||
return currentScreen ?? NSScreen.main ?? NSScreen()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ class StatusItemContainerView: NSView {
|
||||
)
|
||||
let contentFrame = NSRect(
|
||||
x: 0,
|
||||
y: (Constants.maxContainerSize - Constants.maxContentHeight) / 2,
|
||||
y: (Constants.maxContainerSize-Constants.maxContentHeight) / 2,
|
||||
width: NSWidth(view.bounds),
|
||||
height: Constants.maxContentHeight
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user