Popover implementation

This commit is contained in:
Ivan Sapozhnik 2020-04-01 23:39:27 +02:00
parent c602af59de
commit 547d1b1128
8 changed files with 359 additions and 3 deletions

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded</key>
<true/>
</dict>
</plist>

View File

@ -5,6 +5,9 @@ import PackageDescription
let package = Package(
name: "Popover",
platforms: [
.macOS(.v10_14)
],
products: [
// Products define the executables and libraries produced by a package, and make them visible to other packages.
.library(

View File

@ -1,3 +1,63 @@
struct Popover {
var text = "Hello, World!"
//
// Popover.swift
// 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
}
public var item: NSStatusItem!
private var popoverWindowController: PopoverWindowController?
private let windowConfiguration: PopoverConfiguration
private var observation: NSKeyValueObservation?
private var isPopoverWindowVisible: Bool {
return (popoverWindowController != nil) ? popoverWindowController!.windowIsOpen : false
}
public init(with configuration: PopoverConfiguration) {
windowConfiguration = configuration
}
public func presentPopover(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
item = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
guard let button = item.button else { return }
button.target = self
button.action = #selector(handleStatusItemButtonAction(_:))
button.image = image
}
@objc private func handleStatusItemButtonAction(_ sender: Any?) {
if isPopoverWindowVisible {
dismissPopoverWindow()
} else {
showPopoverWindow()
}
}
public func dismissPopoverWindow() {
popoverWindowController?.dismiss()
}
public func showPopoverWindow() {
popoverWindowController?.show()
}
}

View File

@ -0,0 +1,56 @@
//
// WindowConfiguration.swift
// 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 backgroundColor: NSColor { get }
var lineColor: NSColor? { get }
var lineWidth: CGFloat { get }
var arrowHeight: CGFloat { get }
var arrowWidth: CGFloat { get }
var cornerRadius: CGFloat { get }
var contentInset: NSEdgeInsets { get }
}
public class DefaultConfiguration: PopoverConfiguration {
public init() {}
public var windowToPopoverMargin: CGFloat {
return 22
}
public var backgroundColor: NSColor {
return NSColor.windowBackgroundColor//NSColor(calibratedRed: 0.213, green: 0.213, blue: 0.213, alpha: 1.0)
}
public var lineColor: NSColor? {
return nil//NSColor(calibratedRed: 0.413, green: 0.413, blue: 0.413, alpha: 1.0)
}
public var lineWidth: CGFloat {
return lineColor != nil ? 6 : 0
}
public var arrowHeight: CGFloat {
return 11.0
}
public var arrowWidth: CGFloat {
return 62.0
}
public var cornerRadius: CGFloat {
return 20.0
}
public var contentInset: NSEdgeInsets {
return NSEdgeInsets(top: 0, left: 0.0, bottom: 0, right: 0.0)
}
}

View File

@ -0,0 +1,88 @@
//
// PopoverWindow.swift
// 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
private var childContentView: NSView?
private var backgroundView: PopoverWindowBackgroundView?
public 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 {
return true
}
public override var contentView: NSView? {
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: windowConfiguration)
backgroundView?.layer?.edgeAntialiasingMask = antialiasingMask
super.contentView = backgroundView
}
if (self.childContentView != nil) {
self.childContentView?.removeFromSuperview()
}
childContentView = newValue
childContentView?.translatesAutoresizingMaskIntoConstraints = false
childContentView?.wantsLayer = true
childContentView?.layer?.cornerRadius = windowConfiguration.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
NSLayoutConstraint.activate([
userContentView.leadingAnchor.constraint(equalTo: backgroundView.leadingAnchor, constant: left),
userContentView.trailingAnchor.constraint(equalTo: backgroundView.trailingAnchor, constant: -right),
userContentView.topAnchor.constraint(equalTo: backgroundView.topAnchor, constant: top),
userContentView.bottomAnchor.constraint(equalTo: backgroundView.bottomAnchor, constant: -bottom)
])
}
get {
childContentView
}
}
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
}
}

View File

@ -0,0 +1,83 @@
//
// PopoverWindowBackgroundView.swift
// Popover
//
// Created by Ivan Sapozhnik on 29.03.20.
// Copyright © 2020 heavylightapps. All rights reserved.
//
import Cocoa
class PopoverWindowBackgroundView: NSView {
private let windowConfiguration: PopoverConfiguration
init(frame frameRect: NSRect, windowConfiguration: PopoverConfiguration) {
self.windowConfiguration = windowConfiguration
super.init(frame: frameRect)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private var lineWidth: CGFloat {
return windowConfiguration.lineColor != nil ? windowConfiguration.lineWidth : 0
}
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
// Drawing code here.
let backgroundRect = NSMakeRect(NSMinX(bounds), NSMinY(bounds), NSWidth(bounds), NSHeight(bounds) - windowConfiguration.arrowHeight).insetBy(dx: lineWidth, dy: lineWidth)
let controlPoints = NSBezierPath()
let windowPath = NSBezierPath()
let arrowPath = NSBezierPath()
let backgroundPath = NSBezierPath(roundedRect: backgroundRect, xRadius: windowConfiguration.cornerRadius, yRadius: windowConfiguration.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))
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)))
controlPoints.append(NSBezierPath(ovalIn: NSMakeRect(rightPoint.x - 2, rightPoint.y - 2, 4, 4)))
controlPoints.append(NSBezierPath(ovalIn: NSMakeRect(cp11.x - 2, cp11.y - 2, 4, 4)))
controlPoints.append(NSBezierPath(ovalIn: NSMakeRect(cp12.x - 2, cp12.y - 2, 4, 4)))
controlPoints.append(NSBezierPath(ovalIn: NSMakeRect(cp21.x - 2, cp21.y - 2, 4, 4)))
controlPoints.append(NSBezierPath(ovalIn: NSMakeRect(cp22.x - 2, cp22.y - 2, 4, 4)))
arrowPath.move(to: leftPoint)
arrowPath.curve(to: toPoint, controlPoint1: cp11, controlPoint2: cp12)
arrowPath.curve(to: rightPoint, controlPoint1: cp21, controlPoint2: cp22)
arrowPath.line(to: leftPoint)
arrowPath.close()
windowPath.append(arrowPath)
windowPath.append(backgroundPath)
if let lineColor = windowConfiguration.lineColor {
lineColor.setStroke()
windowPath.lineWidth = windowConfiguration.lineWidth
windowPath.stroke()
}
windowConfiguration.backgroundColor.setFill()
windowPath.fill()
// controlPoints.fill()
}
override var frame: NSRect {
didSet {
setNeedsDisplay(frame)
}
}
}

View File

@ -0,0 +1,59 @@
//
// PopoverWindowController.swift
// Popover
//
// Created by Ivan Sapozhnik on 29.03.20.
// Copyright © 2020 heavylightapps. All rights reserved.
//
import Cocoa
class PopoverWindowController: NSWindowController {
public private(set) var windowIsOpen: Bool = false
public private(set) var isAnimating: Bool = false
private let popover: Popover
private let windowConfiguration: PopoverConfiguration
init(with popover: Popover, contentViewController: NSViewController, windowConfiguration: PopoverConfiguration) {
self.popover = popover
self.windowConfiguration = windowConfiguration
super.init(window: PopoverWindow.window(with: windowConfiguration))
self.contentViewController = contentViewController
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func show() {
guard !isAnimating else { return }
updateWindwFrame()
window?.alphaValue = 1.0
showWindow(nil)
windowIsOpen = true
window?.makeKey()
// TODO: animation
}
public func dismiss() {
guard !isAnimating else { return }
window?.orderOut(self)
window?.close()
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)
let windowFrame = NSMakeRect(x, y, NSWidth(window.frame), NSHeight(window.frame))
window.setFrame(windowFrame, display: true)
window.appearance = NSAppearance.current
}
}

View File

@ -6,7 +6,6 @@ final class PopoverTests: XCTestCase {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct
// results.
XCTAssertEqual(Popover().text, "Hello, World!")
}
static var allTests = [