9 Commits
Author SHA1 Message Date
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
Ivan Sapozhnik 82f39581a7 Cleanup 2020-04-03 17:26:34 +02:00
Ivan Sapozhnik 1b34b23c58 Removing unused code 2020-04-03 17:18:10 +02:00
Ivan Sapozhnik d63379d099 Renaming and access level 2020-04-03 16:58:24 +02:00
Ivan Sapozhnik ee8634c724 Fixing typo 2020-04-03 15:51:55 +02:00
Ivan Sapozhnik 19a6598f7e Custom view inside status bar item 2020-04-03 14:15:07 +02:00
8 changed files with 231 additions and 102 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ import PackageDescription
let package = Package(
name: "Popover",
platforms: [
.macOS(.v10_14)
.macOS(.v10_11)
],
products: [
// Products define the executables and libraries produced by a package, and make them visible to other packages.
+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
}
}
+54 -23
View File
@@ -3,61 +3,92 @@
// Popover
//
// Created by Ivan Sapozhnik on 29.03.20.
// Copyright © 2020 heavylightapps. All rights reserved.
//
import Cocoa
public class Popover: NSObject {
enum PopoverPresentationMode {
case undefined
case image
case customView
}
var item: NSStatusItem!
public var item: NSStatusItem!
private var popoverWindowController: PopoverWindowController?
private let windowConfiguration: PopoverConfiguration
private var observation: NSKeyValueObservation?
private var popoverWindowController: PopoverWindowController?
private var statusItemContainer: StatusItemContainerView?
private var eventMonitor: EventMonitor?
private var isPopoverWindowVisible: Bool {
return (popoverWindowController != nil) ? popoverWindowController!.windowIsOpen : false
}
/// Create 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 clicl ouside of the Popover's frame, the system will automatically dismiss the Popover.
public init(with configuration: PopoverConfiguration) {
windowConfiguration = configuration
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) {
/// 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)
}
private func configureStatusBarButton(with image: NSImage) {
image.isTemplate = true
/// 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)
}
/// 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) {
item = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
guard let button = item.button else { return }
image.isTemplate = true
button.target = self
button.action = #selector(handleStatusItemButtonAction(_:))
button.image = image
}
private func configureStatusBarButton(with view: NSView) {
item = NSStatusBar.system.statusItem(withLength: NSWidth(view.bounds))
guard let button = item.button else { return }
let statusItemContainer = StatusItemContainerView()
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?) {
if isPopoverWindowVisible {
dismissPopoverWindow()
dismiss()
} else {
showPopoverWindow()
show()
}
}
public func dismissPopoverWindow() {
popoverWindowController?.dismiss()
}
public func showPopoverWindow() {
popoverWindowController?.show()
}
}
+19 -20
View File
@@ -3,54 +3,53 @@
// Popover
//
// Created by Ivan Sapozhnik on 29.03.20.
// Copyright © 2020 heavylightapps. All rights reserved.
//
import Cocoa
public protocol PopoverConfiguration {
var windowToPopoverMargin: CGFloat { get }
var popoverToStatusItemMargin: CGFloat { get }
var backgroundColor: NSColor { get }
var lineColor: NSColor? { get }
var lineWidth: CGFloat { 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 }
}
public class DefaultConfiguration: PopoverConfiguration {
open class DefaultConfiguration: PopoverConfiguration {
public init() {}
public var windowToPopoverMargin: CGFloat {
return 22
open var popoverToStatusItemMargin: CGFloat {
return 2
}
public var backgroundColor: NSColor {
return NSColor.windowBackgroundColor//NSColor(calibratedRed: 0.213, green: 0.213, blue: 0.213, alpha: 1.0)
open var backgroundColor: NSColor {
return NSColor.windowBackgroundColor
}
public var lineColor: NSColor? {
return nil//NSColor(calibratedRed: 0.413, green: 0.413, blue: 0.413, alpha: 1.0)
open var borderColor: NSColor? {
return NSColor.white
}
public var lineWidth: CGFloat {
return lineColor != nil ? 6 : 0
open var borderWidth: CGFloat {
return 2
}
public var arrowHeight: CGFloat {
return 11.0
open var arrowHeight: CGFloat {
return 12.0
}
public var arrowWidth: CGFloat {
open var arrowWidth: CGFloat {
return 62.0
}
public var cornerRadius: CGFloat {
return 20.0
open var cornerRadius: CGFloat {
return 12
}
public var contentInset: NSEdgeInsets {
return NSEdgeInsets(top: 0, left: 0.0, bottom: 0, right: 0.0)
open var contentInset: NSEdgeInsets {
return NSEdgeInsetsZero
}
}
+33 -28
View File
@@ -3,28 +3,45 @@
// Popover
//
// Created by Ivan Sapozhnik on 29.03.20.
// Copyright © 2020 heavylightapps. All rights reserved.
//
import Cocoa
public class PopoverWindow: NSPanel {
private let windowConfiguration: PopoverConfiguration
class PopoverWindow: NSPanel {
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)
let window = PopoverWindow.init(contentRect: .zero, styleMask: .nonactivatingPanel, backing: .buffered, defer: true, configuration: configuration)
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,
backing: .buffered,
defer: true,
configuration: configuration
)
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 }
@@ -32,7 +49,7 @@ public class PopoverWindow: NSPanel {
backgroundView = super.contentView as? PopoverWindowBackgroundView
if (backgroundView == nil) {
backgroundView = PopoverWindowBackgroundView(frame: bounds, windowConfiguration: windowConfiguration)
backgroundView = PopoverWindowBackgroundView(frame: bounds, windowConfiguration: wConfig)
backgroundView?.layer?.edgeAntialiasingMask = antialiasingMask
super.contentView = backgroundView
}
@@ -44,17 +61,18 @@ public class PopoverWindow: NSPanel {
childContentView = newValue
childContentView?.translatesAutoresizingMaskIntoConstraints = false
childContentView?.wantsLayer = true
childContentView?.layer?.cornerRadius = windowConfiguration.cornerRadius
childContentView?.layer?.cornerRadius = wConfig.cornerRadius
childContentView?.layer?.masksToBounds = true
childContentView?.layer?.edgeAntialiasingMask = antialiasingMask
guard let userContentView = self.childContentView, let backgroundView = self.backgroundView else { return }
backgroundView.addSubview(userContentView)
let left = windowConfiguration.lineWidth + windowConfiguration.contentInset.left
let right = windowConfiguration.lineWidth + windowConfiguration.contentInset.right
let top = windowConfiguration.arrowHeight + windowConfiguration.lineWidth + windowConfiguration.contentInset.top
let bottom = windowConfiguration.lineWidth + windowConfiguration.contentInset.bottom
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
NSLayoutConstraint.activate([
userContentView.leadingAnchor.constraint(equalTo: backgroundView.leadingAnchor, constant: left),
@@ -69,20 +87,7 @@ public class PopoverWindow: NSPanel {
}
}
public override func frameRect(forContentRect contentRect: NSRect) -> NSRect {
NSMakeRect(NSMinX(contentRect), NSMinY(contentRect), NSWidth(contentRect), NSHeight(contentRect) + windowConfiguration.arrowHeight)
}
private init(contentRect: NSRect, styleMask style: NSWindow.StyleMask, backing backingStoreType: NSWindow.BackingStoreType, defer flag: Bool, configuration: PopoverConfiguration) {
windowConfiguration = 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
override func frameRect(forContentRect contentRect: NSRect) -> NSRect {
NSMakeRect(NSMinX(contentRect), NSMinY(contentRect), NSWidth(contentRect), NSHeight(contentRect) + wConfig.arrowHeight)
}
}
@@ -3,16 +3,15 @@
// Popover
//
// Created by Ivan Sapozhnik on 29.03.20.
// Copyright © 2020 heavylightapps. All rights reserved.
//
import Cocoa
class PopoverWindowBackgroundView: NSView {
private let windowConfiguration: PopoverConfiguration
private let wConfig: PopoverConfiguration
init(frame frameRect: NSRect, windowConfiguration: PopoverConfiguration) {
self.windowConfiguration = windowConfiguration
self.wConfig = windowConfiguration
super.init(frame: frameRect)
}
@@ -20,8 +19,8 @@ class PopoverWindowBackgroundView: NSView {
fatalError("init(coder:) has not been implemented")
}
private var lineWidth: CGFloat {
return windowConfiguration.lineColor != nil ? windowConfiguration.lineWidth : 0
private var borderWidth: CGFloat {
return wConfig.borderColor != nil ? wConfig.borderWidth : 0
}
override func draw(_ dirtyRect: NSRect) {
@@ -29,20 +28,20 @@ class PopoverWindowBackgroundView: NSView {
// Drawing code here.
let backgroundRect = NSMakeRect(NSMinX(bounds), NSMinY(bounds), NSWidth(bounds), NSHeight(bounds) - windowConfiguration.arrowHeight).insetBy(dx: lineWidth, dy: lineWidth)
let backgroundRect = NSMakeRect(NSMinX(bounds), NSMinY(bounds), NSWidth(bounds), NSHeight(bounds) - wConfig.arrowHeight).insetBy(dx: borderWidth, dy: borderWidth)
let controlPoints = NSBezierPath()
let windowPath = NSBezierPath()
let arrowPath = NSBezierPath()
let backgroundPath = NSBezierPath(roundedRect: backgroundRect, xRadius: windowConfiguration.cornerRadius, yRadius: windowConfiguration.cornerRadius)
let backgroundPath = NSBezierPath(roundedRect: backgroundRect, xRadius: wConfig.cornerRadius, yRadius: wConfig.cornerRadius)
let leftPoint = NSPoint(x: NSWidth(backgroundRect) / 2 - windowConfiguration.arrowWidth / 2, y: NSMaxY(backgroundRect))
let toPoint = NSPoint(x: NSWidth(backgroundRect) / 2, y: NSMaxY(backgroundRect) + windowConfiguration.arrowHeight)
let rightPoint = NSPoint(x: NSWidth(backgroundRect) / 2 + windowConfiguration.arrowWidth / 2, y: NSMaxY(backgroundRect))
let cp11 = NSPoint(x: NSWidth(backgroundRect) / 2 - windowConfiguration.arrowWidth / 6, y: NSMaxY(backgroundRect))
let cp12 = NSPoint(x: NSWidth(backgroundRect) / 2 - windowConfiguration.arrowWidth / 9, y: NSMaxY(backgroundRect) + windowConfiguration.arrowHeight)
let cp21 = NSPoint(x: NSWidth(backgroundRect) / 2 + windowConfiguration.arrowWidth / 9, y: NSMaxY(backgroundRect) + windowConfiguration.arrowHeight)
let cp22 = NSPoint(x: NSWidth(backgroundRect) / 2 + windowConfiguration.arrowWidth / 6, y: NSMaxY(backgroundRect))
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))
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)))
@@ -61,14 +60,14 @@ class PopoverWindowBackgroundView: NSView {
windowPath.append(arrowPath)
windowPath.append(backgroundPath)
if let lineColor = windowConfiguration.lineColor {
if let lineColor = wConfig.borderColor {
lineColor.setStroke()
windowPath.lineWidth = windowConfiguration.lineWidth
windowPath.lineWidth = wConfig.borderWidth
windowPath.stroke()
}
windowConfiguration.backgroundColor.setFill()
wConfig.backgroundColor.setFill()
windowPath.fill()
// controlPoints.fill()
+39 -13
View File
@@ -3,7 +3,6 @@
// Popover
//
// Created by Ivan Sapozhnik on 29.03.20.
// Copyright © 2020 heavylightapps. All rights reserved.
//
import Cocoa
@@ -13,11 +12,11 @@ class PopoverWindowController: NSWindowController {
public private(set) var isAnimating: Bool = false
private let popover: Popover
private let windowConfiguration: PopoverConfiguration
private let wConfig: PopoverConfiguration
init(with popover: Popover, contentViewController: NSViewController, windowConfiguration: PopoverConfiguration) {
self.popover = popover
self.windowConfiguration = windowConfiguration
self.wConfig = windowConfiguration
super.init(window: PopoverWindow.window(with: windowConfiguration))
self.contentViewController = contentViewController
@@ -27,10 +26,10 @@ class PopoverWindowController: NSWindowController {
fatalError("init(coder:) has not been implemented")
}
public func show() {
func show() {
guard !isAnimating else { return }
updateWindwFrame()
updateWindowFrame()
window?.alphaValue = 1.0
showWindow(nil)
windowIsOpen = true
@@ -38,7 +37,7 @@ class PopoverWindowController: NSWindowController {
// TODO: animation
}
public func dismiss() {
func dismiss() {
guard !isAnimating else { return }
window?.orderOut(self)
@@ -46,14 +45,41 @@ class PopoverWindowController: NSWindowController {
windowIsOpen = false
}
private func updateWindwFrame() {
guard let popoverRect = popover.item.button?.window?.frame, let window = window else { return }
let x = NSMinX(popoverRect) - NSWidth(window.frame)/2 + NSWidth(popoverRect)/2 + windowConfiguration.lineWidth
let y = min(NSMinY(popoverRect), NSScreen.main!.frame.size.height - NSHeight(window.frame) - windowConfiguration.windowToPopoverMargin)
private func updateWindowFrame() {
let windowFrame = visibleStatusItemWindowFrame()
window?.setFrame(windowFrame, display: true)
window?.appearance = NSAppearance.current
}
let windowFrame = NSMakeRect(x, y, NSWidth(window.frame), NSHeight(window.frame))
window.setFrame(windowFrame, display: true)
window.appearance = NSAppearance.current
private func visibleStatusItemWindowFrame() -> NSRect {
let screenFrame = currentMouseScreen().frame
let popoverRect = popover.item.button?.window?.frame ?? .zero
guard let window = self.window else { return .zero }
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
return NSMakeRect(
x,
y,
NSWidth(window.frame),
NSHeight(window.frame)
)
}
private func currentMouseScreen() -> NSScreen {
let currentMousePoint = NSEvent.mouseLocation
var currentScreen: NSScreen?
while currentScreen != nil {
NSScreen.screens.forEach { screen in
currentScreen = NSMouseInRect(currentMousePoint, screen.frame, false) ? screen : nil
}
}
return currentScreen ?? NSScreen.main ?? 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
}
}