mirror of
https://github.com/gosticks/Popover.git
synced 2025-10-16 11:55:38 +00:00
Merge pull request #4 from iSapozhnik/NSMenu
NSMenu and rightClicks support
This commit is contained in:
commit
34fb2f3444
@ -8,13 +8,25 @@
|
||||
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 handler: (NSEvent?) -> Void
|
||||
private let monitorType: MonitorType
|
||||
private let globalHandler: GlobalEventHandler?
|
||||
private let localHandler: LocalEventHandler?
|
||||
|
||||
init(mask: NSEvent.EventTypeMask, handler: @escaping (NSEvent?) -> Void) {
|
||||
init(monitorType: MonitorType, mask: NSEvent.EventTypeMask, globalHandler: GlobalEventHandler?, localHandler: LocalEventHandler?) {
|
||||
self.mask = mask
|
||||
self.handler = handler
|
||||
self.monitorType = monitorType
|
||||
self.globalHandler = globalHandler
|
||||
self.localHandler = localHandler
|
||||
}
|
||||
|
||||
deinit {
|
||||
@ -22,7 +34,12 @@ class EventMonitor {
|
||||
}
|
||||
|
||||
func start() {
|
||||
monitor = NSEvent.addGlobalMonitorForEvents(matching: mask, handler: handler)
|
||||
switch monitorType {
|
||||
case .global:
|
||||
startGlobalMonitor()
|
||||
case .local:
|
||||
startLocalMonitor()
|
||||
}
|
||||
}
|
||||
|
||||
func stop() {
|
||||
@ -31,4 +48,20 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
20
Sources/Popover/Extensions.swift
Normal file
20
Sources/Popover/Extensions.swift
Normal file
@ -0,0 +1,20 @@
|
||||
//
|
||||
// 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
|
||||
}
|
||||
}
|
||||
36
Sources/Popover/MenuItem.swift
Normal file
36
Sources/Popover/MenuItem.swift
Normal file
@ -0,0 +1,36 @@
|
||||
//
|
||||
// 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,59 +8,105 @@
|
||||
import Cocoa
|
||||
|
||||
public class Popover: NSObject {
|
||||
var item: NSStatusItem!
|
||||
var item: NSStatusItem! {
|
||||
didSet {
|
||||
prepareMenu()
|
||||
}
|
||||
}
|
||||
|
||||
private let wConfig: PopoverConfiguration
|
||||
private var popoverWindowController: PopoverWindowController?
|
||||
private var statusItemContainer: StatusItemContainerView?
|
||||
private var eventMonitor: EventMonitor?
|
||||
private var globalEventMonitor: EventMonitor?
|
||||
private var localEventMonitor: EventMonitor?
|
||||
private var menu: NSMenu?
|
||||
private var menuItems: [MenuItemType]?
|
||||
|
||||
private var isPopoverWindowVisible: Bool {
|
||||
return (popoverWindowController != nil) ? popoverWindowController!.windowIsOpen : false
|
||||
}
|
||||
|
||||
/// 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.
|
||||
public init(with windowConfiguration: PopoverConfiguration) {
|
||||
wConfig = windowConfiguration
|
||||
|
||||
super.init()
|
||||
|
||||
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(), menuItems: nil)
|
||||
}
|
||||
|
||||
/// Convenience initializer which is using default configuration
|
||||
public convenience override init() {
|
||||
self.init(with: DefaultConfiguration())
|
||||
///
|
||||
/// - 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]?) {
|
||||
wConfig = windowConfiguration
|
||||
self.menuItems = menuItems
|
||||
|
||||
super.init()
|
||||
|
||||
setupMonitors()
|
||||
}
|
||||
|
||||
/// 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 createPopover(with image: NSImage, contentViewController viewController: NSViewController) {
|
||||
public func prepare(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 createPopover(with view: NSView, contentViewController viewController: NSViewController) {
|
||||
public func prepare(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()
|
||||
eventMonitor?.start()
|
||||
globalEventMonitor?.start()
|
||||
}
|
||||
|
||||
/// Dismisses the Popover with default animation. Animation behaviour is `AnimationBehavior.utilityWindow`
|
||||
/// Dismisses the Popover with default animation. Animation behavior is `AnimationBehavior.utilityWindow`
|
||||
public func dismiss() {
|
||||
guard isPopoverWindowVisible else { return }
|
||||
popoverWindowController?.dismiss()
|
||||
eventMonitor?.stop()
|
||||
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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private func configureStatusBarButton(with image: NSImage) {
|
||||
@ -68,9 +114,8 @@ 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) {
|
||||
@ -79,17 +124,43 @@ public class Popover: NSObject {
|
||||
|
||||
let statusItemContainer = StatusItemContainerView()
|
||||
|
||||
button.target = self
|
||||
button.action = #selector(handleStatusItemButtonAction(_:))
|
||||
setTargetAction(for: button)
|
||||
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,9 +17,20 @@ 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
|
||||
@ -50,12 +61,10 @@ 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 = antialiasingMask
|
||||
backgroundView?.layer?.edgeAntialiasingMask = .all
|
||||
super.contentView = backgroundView
|
||||
}
|
||||
|
||||
@ -68,7 +77,7 @@ class PopoverWindow: NSPanel {
|
||||
childContentView?.wantsLayer = true
|
||||
childContentView?.layer?.cornerRadius = wConfig.cornerRadius
|
||||
childContentView?.layer?.masksToBounds = true
|
||||
childContentView?.layer?.edgeAntialiasingMask = antialiasingMask
|
||||
childContentView?.layer?.edgeAntialiasingMask = .all
|
||||
|
||||
guard let userContentView = self.childContentView, let backgroundView = self.backgroundView else { return }
|
||||
backgroundView.addSubview(userContentView)
|
||||
@ -97,7 +106,7 @@ class PopoverWindow: NSPanel {
|
||||
}
|
||||
|
||||
private func updateArrowPosition() {
|
||||
guard let backgroundView = self.backgroundView as? PopoverWindowBackgroundView else { return }
|
||||
guard let backgroundView = self.backgroundView else { return }
|
||||
backgroundView.arrowXLocation = arrowXLocation
|
||||
}
|
||||
}
|
||||
|
||||
@ -82,5 +82,4 @@ 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
|
||||
)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user