7 Commits
Author SHA1 Message Date
Ivan Sapozhnik f51f80c5e0 Documentation and small refactoring 2020-04-04 23:38:01 +02:00
Ivan Sapozhnik bb87a2dc3a Right edge margin 2020-04-04 23:09:40 +02:00
Ivan Sapozhnik f39f238846 Handling arrow position 2020-04-04 22:51:02 +02:00
Ivan Sapozhnik 846a5b702a Documentation, access level and renaming 2020-04-04 12:07:19 +02:00
Ivan Sapozhnik 7f3a81e0e2 Access level for NSStatusBarItem 2020-04-04 11:53:11 +02:00
Ivan Sapozhnik b08ed8e3a8 Files copyright 2020-04-04 11:51:48 +02:00
Ivan Sapozhnik 14cfd70594 Simple Event Monitor 2020-04-03 18:09:42 +02:00
7 changed files with 209 additions and 106 deletions
+34
View File
@@ -0,0 +1,34 @@
//
// EventMonitor.swift
// Popover
//
// Created by Ivan Sapozhnik on 03.04.20.
//
import Cocoa
class EventMonitor {
private var monitor: Any?
private let mask: NSEvent.EventTypeMask
private let handler: (NSEvent?) -> Void
init(mask: NSEvent.EventTypeMask, handler: @escaping (NSEvent?) -> Void) {
self.mask = mask
self.handler = handler
}
deinit {
stop()
}
func start() {
monitor = NSEvent.addGlobalMonitorForEvents(matching: mask, handler: handler)
}
func stop() {
guard let monitor = self.monitor else { return }
NSEvent.removeMonitor(monitor)
self.monitor = nil
}
}
+40 -49
View File
@@ -3,61 +3,64 @@
// Popover
//
// Created by Ivan Sapozhnik on 29.03.20.
// Copyright © 2020 heavylightapps. All rights reserved.
//
import Cocoa
public class Popover: NSObject {
private class StatusItemContainerView: NSView {
enum Constants {
static let maxContainerSize: CGFloat = 22.0
static let maxContentHeight: CGFloat = 18.0
}
func embed(_ view: NSView) {
addSubview(view)
let selfFrame = NSRect(
x: 0,
y: 0,
width: max(NSWidth(view.bounds), Constants.maxContainerSize),
height: Constants.maxContainerSize
)
let contentFrame = NSRect(
x: 0,
y: (Constants.maxContainerSize-Constants.maxContentHeight) / 2,
width: NSWidth(view.bounds),
height: Constants.maxContentHeight
)
frame = selfFrame
view.frame = contentFrame
}
}
public var item: NSStatusItem!
var item: NSStatusItem!
private let wConfig: PopoverConfiguration
private var popoverWindowController: PopoverWindowController?
private let windowConfiguration: PopoverConfiguration
private var statusItemContainer: StatusItemContainerView?
private var eventMonitor: EventMonitor?
private var isPopoverWindowVisible: Bool {
return (popoverWindowController != nil) ? popoverWindowController!.windowIsOpen : false
}
public init(with configuration: PopoverConfiguration) {
windowConfiguration = configuration
/// 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()
})
}
public func presentPopover(with image: NSImage, contentViewController viewController: NSViewController) {
/// 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 createPopover(with image: NSImage, contentViewController viewController: NSViewController) {
configureStatusBarButton(with: image)
popoverWindowController = PopoverWindowController(with: self, contentViewController: viewController, windowConfiguration: windowConfiguration)
popoverWindowController = PopoverWindowController(with: self, contentViewController: viewController, windowConfiguration: wConfig)
}
public func presentPopover(with view: NSView, contentViewController viewController: NSViewController) {
/// 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) {
configureStatusBarButton(with: view)
popoverWindowController = PopoverWindowController(with: self, contentViewController: viewController, windowConfiguration: windowConfiguration)
popoverWindowController = PopoverWindowController(with: self, contentViewController: viewController, windowConfiguration: wConfig)
}
/// Shows the Popover with no animation
public func show() {
popoverWindowController?.show()
eventMonitor?.start()
}
/// Dismisses the Popover with default animation. Animation behaviour is `AnimationBehavior.utilityWindow`
public func dismiss() {
popoverWindowController?.dismiss()
eventMonitor?.stop()
}
private func configureStatusBarButton(with image: NSImage) {
@@ -87,18 +90,6 @@ public class Popover: NSObject {
}
@objc private func handleStatusItemButtonAction(_ sender: Any?) {
if isPopoverWindowVisible {
dismissPopoverWindow()
} else {
showPopoverWindow()
}
}
public func dismissPopoverWindow() {
popoverWindowController?.dismiss()
}
public func showPopoverWindow() {
popoverWindowController?.show()
isPopoverWindowVisible ? dismiss() : show()
}
}
+40 -11
View File
@@ -3,20 +3,45 @@
// Popover
//
// Created by Ivan Sapozhnik on 29.03.20.
// Copyright © 2020 heavylightapps. All rights reserved.
//
import Cocoa
public protocol PopoverConfiguration {
var popoverToStatusItemMargin: CGFloat { get }
var backgroundColor: NSColor { get }
var borderColor: NSColor? { get }
var borderWidth: CGFloat { get }
var arrowHeight: CGFloat { get }
var arrowWidth: CGFloat { get }
var cornerRadius: CGFloat { get }
var contentInset: NSEdgeInsets { get }
/// The distance from Popover's arrow to a status item.
var popoverToStatusItemMargin: CGFloat { get }
/// Popover's background color.
var backgroundColor: NSColor { get }
/// Popover's border color.
/// - Important:
/// If `borderColor` returns `nil`, settings `borderWidth` won't make any affect. See also: `borderWidth`.
var borderColor: NSColor? { get }
/// Popover's border width.
/// - Important:
/// If Popover's border color is set to `nil`, setting `borderWidth` won't make any affect.
var borderWidth: CGFloat { get }
/// Defines Popover arrow height.
var arrowHeight: CGFloat { get }
/// Defines Popover arrow width.
var arrowWidth: CGFloat { get }
/// Defines Popover corner radius.
/// - Warning:
/// If this value is too big and if the Popover's status item (menu bar view) is too close to the right edge, the appearence of the Popover might be odd.
var cornerRadius: CGFloat { get }
/// Defines Popover content edge insets.
var contentEdgeInsets: NSEdgeInsets { get }
/// The distance from the right side of the Popover to the screen's edge.
/// - Warning:
/// If this value is too big and if the Popover's status item (menu bar view) is too close to the right edge, the appearence of the Popover might be odd.
var rightEdgeMargin: CGFloat { get }
}
open class DefaultConfiguration: PopoverConfiguration {
@@ -47,10 +72,14 @@ open class DefaultConfiguration: PopoverConfiguration {
}
open var cornerRadius: CGFloat {
return 12
return 5
}
open var contentInset: NSEdgeInsets {
open var contentEdgeInsets: NSEdgeInsets {
return NSEdgeInsetsZero
}
open var rightEdgeMargin: CGFloat {
return 12.0
}
}
+30 -21
View File
@@ -3,17 +3,34 @@
// Popover
//
// Created by Ivan Sapozhnik on 29.03.20.
// Copyright © 2020 heavylightapps. All rights reserved.
//
import Cocoa
public class PopoverWindow: NSPanel {
class PopoverWindow: NSPanel {
var arrowXLocation: CGFloat = 0.0 {
didSet {
updateArrowPosition()
}
}
private let wConfig: PopoverConfiguration
private var childContentView: NSView?
private var backgroundView: PopoverWindowBackgroundView?
public static func window(with configuration: PopoverConfiguration) -> PopoverWindow {
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)
isOpaque = false
hasShadow = true
level = .statusBar
animationBehavior = .utilityWindow
backgroundColor = .clear
collectionBehavior = [.canJoinAllSpaces, .ignoresCycle]
appearance = NSAppearance.current
}
static func window(with configuration: PopoverConfiguration) -> PopoverWindow {
let window = PopoverWindow.init(
contentRect: .zero,
styleMask: .nonactivatingPanel,
@@ -25,11 +42,11 @@ public class PopoverWindow: NSPanel {
return window
}
public override var canBecomeKey: Bool {
override var canBecomeKey: Bool {
return true
}
public override var contentView: NSView? {
override var contentView: NSView? {
set {
guard childContentView != newValue, let bounds = newValue?.bounds else { return }
@@ -57,10 +74,10 @@ public class PopoverWindow: NSPanel {
backgroundView.addSubview(userContentView)
let borderWidth = wConfig.borderColor != nil ? wConfig.borderWidth : 0
let left = borderWidth + wConfig.contentInset.left
let right = borderWidth + wConfig.contentInset.right
let top = wConfig.arrowHeight + borderWidth + wConfig.contentInset.top
let bottom = borderWidth + wConfig.contentInset.bottom
let left = borderWidth + wConfig.contentEdgeInsets.left
let right = borderWidth + wConfig.contentEdgeInsets.right
let top = wConfig.arrowHeight + borderWidth + wConfig.contentEdgeInsets.top
let bottom = borderWidth + wConfig.contentEdgeInsets.bottom
NSLayoutConstraint.activate([
userContentView.leadingAnchor.constraint(equalTo: backgroundView.leadingAnchor, constant: left),
@@ -75,20 +92,12 @@ public class PopoverWindow: NSPanel {
}
}
public override func frameRect(forContentRect contentRect: NSRect) -> NSRect {
override func frameRect(forContentRect contentRect: NSRect) -> NSRect {
NSMakeRect(NSMinX(contentRect), NSMinY(contentRect), NSWidth(contentRect), NSHeight(contentRect) + wConfig.arrowHeight)
}
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)
isOpaque = false
hasShadow = true
level = .statusBar
animationBehavior = .utilityWindow
backgroundColor = .clear
collectionBehavior = [.canJoinAllSpaces, .ignoresCycle]
appearance = NSAppearance.current
private func updateArrowPosition() {
guard let backgroundView = self.backgroundView as? PopoverWindowBackgroundView else { return }
backgroundView.arrowXLocation = arrowXLocation
}
}
@@ -3,13 +3,17 @@
// Popover
//
// Created by Ivan Sapozhnik on 29.03.20.
// Copyright © 2020 heavylightapps. All rights reserved.
//
import Cocoa
class PopoverWindowBackgroundView: NSView {
private let wConfig: PopoverConfiguration
var arrowXLocation: CGFloat = 0.0 {
didSet {
needsDisplay = true
}
}
init(frame frameRect: NSRect, windowConfiguration: PopoverConfiguration) {
self.wConfig = windowConfiguration
@@ -36,13 +40,14 @@ class PopoverWindowBackgroundView: NSView {
let arrowPath = NSBezierPath()
let backgroundPath = NSBezierPath(roundedRect: backgroundRect, xRadius: wConfig.cornerRadius, yRadius: wConfig.cornerRadius)
let leftPoint = NSPoint(x: NSWidth(backgroundRect) / 2 - wConfig.arrowWidth / 2, y: NSMaxY(backgroundRect))
let toPoint = NSPoint(x: NSWidth(backgroundRect) / 2, y: NSMaxY(backgroundRect) + wConfig.arrowHeight)
let rightPoint = NSPoint(x: NSWidth(backgroundRect) / 2 + wConfig.arrowWidth / 2, y: NSMaxY(backgroundRect))
let cp11 = NSPoint(x: NSWidth(backgroundRect) / 2 - wConfig.arrowWidth / 6, y: NSMaxY(backgroundRect))
let cp12 = NSPoint(x: NSWidth(backgroundRect) / 2 - wConfig.arrowWidth / 9, y: NSMaxY(backgroundRect) + wConfig.arrowHeight)
let cp21 = NSPoint(x: NSWidth(backgroundRect) / 2 + wConfig.arrowWidth / 9, y: NSMaxY(backgroundRect) + wConfig.arrowHeight)
let cp22 = NSPoint(x: NSWidth(backgroundRect) / 2 + wConfig.arrowWidth / 6, y: NSMaxY(backgroundRect))
let x = arrowXLocation
let leftPoint = NSPoint(x: x - wConfig.arrowWidth / 2, y: NSMaxY(backgroundRect))
let toPoint = NSPoint(x: x, y: NSMaxY(backgroundRect) + wConfig.arrowHeight)
let rightPoint = NSPoint(x: x + wConfig.arrowWidth / 2, y: NSMaxY(backgroundRect))
let cp11 = NSPoint(x: x - wConfig.arrowWidth / 6, y: NSMaxY(backgroundRect))
let cp12 = NSPoint(x: x - wConfig.arrowWidth / 9, y: NSMaxY(backgroundRect) + wConfig.arrowHeight)
let cp21 = NSPoint(x: x + wConfig.arrowWidth / 9, y: NSMaxY(backgroundRect) + wConfig.arrowHeight)
let cp22 = NSPoint(x: x + wConfig.arrowWidth / 6, y: NSMaxY(backgroundRect))
controlPoints.append(NSBezierPath(ovalIn: NSMakeRect(leftPoint.x - 2, leftPoint.y - 2, 4, 4)))
controlPoints.append(NSBezierPath(ovalIn: NSMakeRect(toPoint.x - 2, toPoint.y - 2, 4, 4)))
@@ -71,8 +76,8 @@ class PopoverWindowBackgroundView: NSView {
wConfig.backgroundColor.setFill()
windowPath.fill()
// NSColor.red.setFill()
// controlPoints.fill()
}
override var frame: NSRect {
+16 -16
View File
@@ -3,7 +3,6 @@
// Popover
//
// Created by Ivan Sapozhnik on 29.03.20.
// Copyright © 2020 heavylightapps. All rights reserved.
//
import Cocoa
@@ -27,7 +26,7 @@ class PopoverWindowController: NSWindowController {
fatalError("init(coder:) has not been implemented")
}
public func show() {
func show() {
guard !isAnimating else { return }
updateWindowFrame()
@@ -38,7 +37,7 @@ class PopoverWindowController: NSWindowController {
// TODO: animation
}
public func dismiss() {
func dismiss() {
guard !isAnimating else { return }
window?.orderOut(self)
@@ -47,27 +46,28 @@ class PopoverWindowController: NSWindowController {
}
private func updateWindowFrame() {
guard let window = self.window as? PopoverWindow else { return }
let windowFrame = visibleStatusItemWindowFrame()
window?.setFrame(windowFrame, display: true)
window?.appearance = NSAppearance.current
window.arrowXLocation = windowFrame.arrowXLocation
window.setFrame(windowFrame.frame, display: true)
window.appearance = NSAppearance.current
}
private func visibleStatusItemWindowFrame() -> NSRect {
private func visibleStatusItemWindowFrame() -> (frame: NSRect, arrowXLocation: CGFloat) {
let screenFrame = currentMouseScreen().frame
let popoverRect = popover.item.button?.window?.frame ?? .zero
let statusItemRect = popover.item.button?.window?.frame ?? .zero
guard let window = self.window else { return .zero }
guard let window = self.window else { return (.zero, 0.0) }
let borderWidth = wConfig.borderColor != nil ? wConfig.borderWidth : 0
let x = NSMinX(popoverRect) - NSWidth(window.frame) / 2 + NSWidth(popoverRect) / 2 + borderWidth
let y = min(NSMinY(popoverRect), NSMaxY(screenFrame)) - NSHeight(window.frame) - wConfig.popoverToStatusItemMargin + borderWidth / 2
let x = min(NSMinX(statusItemRect) - NSWidth(window.frame) / 2 + NSWidth(statusItemRect) / 2 + borderWidth, NSMaxX(screenFrame) - NSWidth(window.frame) - wConfig.rightEdgeMargin)
let y = min(NSMinY(statusItemRect), NSMaxY(screenFrame)) - NSHeight(window.frame) - wConfig.popoverToStatusItemMargin + borderWidth / 2
return NSMakeRect(
x,
y,
NSWidth(window.frame),
NSHeight(window.frame)
)
let windowFrame = NSMakeRect(x, y, NSWidth(window.frame), NSHeight(window.frame))
let arrowXLocation = NSMinX(statusItemRect) + NSWidth(statusItemRect) / 2 - NSMinX(windowFrame)
return (windowFrame, arrowXLocation)
}
private func currentMouseScreen() -> NSScreen {
@@ -0,0 +1,35 @@
//
// StatusItemContainerView.swift
// Popover
//
// Created by Ivan Sapozhnik on 03.04.20.
//
import Cocoa
class StatusItemContainerView: NSView {
enum Constants {
static let maxContainerSize: CGFloat = 22.0
static let maxContentHeight: CGFloat = 18.0
}
func embed(_ view: NSView) {
addSubview(view)
let selfFrame = NSRect(
x: 0,
y: 0,
width: max(NSWidth(view.bounds), Constants.maxContainerSize),
height: Constants.maxContainerSize
)
let contentFrame = NSRect(
x: 0,
y: (Constants.maxContainerSize-Constants.maxContentHeight) / 2,
width: NSWidth(view.bounds),
height: Constants.maxContentHeight
)
frame = selfFrame
view.frame = contentFrame
}
}