Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53771db7fe | ||
|
|
08efd190ee | ||
|
|
dc5346fbda | ||
|
|
664d0c881b | ||
|
|
7f62847149 | ||
|
|
0e47c6f786 | ||
|
|
7d2b8bc923 | ||
|
|
168b893236 | ||
|
|
c95768cc8a | ||
|
|
d4743915c2 | ||
|
|
cde8f5a3ad | ||
|
|
36d8baa3d4 | ||
|
|
10edd0c425 | ||
|
|
17391871bd | ||
|
|
a0016a047c | ||
|
|
34fb2f3444 | ||
|
|
78d9aeccf2 | ||
|
|
edc4ace94b | ||
|
|
4599224598 | ||
|
|
3bf529aaa6 | ||
|
|
95c5e6ee5e | ||
|
|
a840c1f3a1 | ||
|
|
f51f80c5e0 | ||
|
|
bb87a2dc3a | ||
|
|
f39f238846 | ||
|
|
846a5b702a | ||
|
|
7f3a81e0e2 | ||
|
|
b08ed8e3a8 | ||
|
|
14cfd70594 | ||
|
|
82f39581a7 | ||
|
|
1b34b23c58 | ||
|
|
d63379d099 | ||
|
|
ee8634c724 | ||
|
|
19a6598f7e |
@@ -0,0 +1,32 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: iSapozhnik
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. Mojave]
|
||||
- Version [e.g. 1.0.7]
|
||||
- ...
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: iSapozhnik
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -0,0 +1,76 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||
level of experience, education, socio-economic status, nationality, personal
|
||||
appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at sapozhnik.ivan@gmail.com. All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see
|
||||
https://www.contributor-covenant.org/faq
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Sapozhnik Ivan
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+1
-1
@@ -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.
|
||||
|
||||
@@ -1,6 +1,137 @@
|
||||
# Popover
|
||||
|
||||
Mac OS custom popover. Made with ❤️
|
||||
<div align="center">
|
||||
|
||||

|
||||

|
||||
[](https://opensource.org/licenses/MIT)
|
||||
|
||||
</div>
|
||||
|
||||
>Custom macOS popover.
|
||||
|
||||

|
||||
|
||||
Heavily inspired by [CCNStatusItem](https://github.com/phranck/CCNStatusItem)
|
||||
# Install
|
||||
|
||||
Since this is a Swift Package, the installation process is pretty stright forward.
|
||||
|
||||
### Manual way
|
||||
Update your `Package.swift` dependencies:
|
||||
|
||||
```
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/iSapozhnik/Popover", from: "1.0.8")
|
||||
]
|
||||
```
|
||||
|
||||
### Via Xcode:
|
||||
1. Go to `File -> Swift Packages -> Add Package Dependency`.
|
||||
2. Put GitHub URL `https://github.com/iSapozhnik/Popover` and click `Next`
|
||||
3. Select the latest version
|
||||
4. Click `Finish`
|
||||
|
||||
# How to use
|
||||
|
||||
1. In your `AppDelegate` import Popover
|
||||
2. Create a view you want to put into a status bar.
|
||||
3. Create a content view controller which would be embedded inside Popover
|
||||
4. Optionally create menu items
|
||||
5. Create a Popover instance. Here you can use either standard configuration (you don't need to pass `windowConfiguration` parameter in this case) or you can subclass `DefaultConfiguration`, override some properties and pass a new instance as a parameter. If in step 4 you have created menu items, pass it here as well. You can see them by making right click on menu bar item.
|
||||
6. Call `prepare` to set everything up.
|
||||
|
||||
And here is how typical `AppDelegate` may look like:
|
||||
|
||||
```swift
|
||||
import Cocoa
|
||||
import Popover
|
||||
|
||||
class MyPopoverConfiguration: DefaultConfiguration {
|
||||
override var backgroundColor: NSColor {
|
||||
return NSColor.systemRed
|
||||
}
|
||||
|
||||
override var borderColor: NSColor? {
|
||||
return NSColor.red
|
||||
}
|
||||
}
|
||||
|
||||
@NSApplicationMain
|
||||
class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
var popover: Popover!
|
||||
|
||||
func applicationDidFinishLaunching(_ aNotification: Notification) {
|
||||
let statusItemView = StatusItemView(frame: NSRect(width: 22.0, height: 20))
|
||||
|
||||
let viewController = MainViewController()
|
||||
|
||||
let menuItems: [Popover.MenuItemType] = [
|
||||
.item(Popover.MenuItem(title: "Settings", action: viewController.showSettings)),
|
||||
.separator,
|
||||
.item(Popover.MenuItem(title: "Quit", key: "q", action: viewController.quit))
|
||||
]
|
||||
|
||||
popover = Popover(with: MyPopoverConfiguration(), menuItems: menuItems)
|
||||
popover.prepare(with: statusItemView, contentViewController: viewController)
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
# What can be customized?
|
||||
|
||||
Quite a lot of things:
|
||||
|
||||
```swift
|
||||
public protocol PopoverConfiguration {
|
||||
/// 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 effect. 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 effect.
|
||||
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 }
|
||||
}
|
||||
```
|
||||
|
||||
## Mentions
|
||||
- [AppCoda Weekly - Issue #172](http://digest.appcoda.com/issues/appcoda-weekly-issue-172-239907)
|
||||
|
||||
## Credits
|
||||
|
||||
Created and maintained by [**@iSapozhnik**](https://twitter.com/iSapozhnik).
|
||||
|
||||
## License
|
||||
|
||||
Released under the MIT License. See `LICENSE` for details.
|
||||
|
||||
>**Copyright © 2020-present Sapozhnik Ivan.**
|
||||
|
||||
Heavily inspired by [CCNStatusItem](https://github.com/phranck/CCNStatusItem)
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
//
|
||||
// EventMonitor.swift
|
||||
// Popover
|
||||
//
|
||||
// Created by Ivan Sapozhnik on 03.04.20.
|
||||
//
|
||||
|
||||
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?
|
||||
|
||||
init(monitorType: MonitorType, mask: NSEvent.EventTypeMask, globalHandler: GlobalEventHandler?, localHandler: LocalEventHandler?) {
|
||||
self.mask = mask
|
||||
self.monitorType = monitorType
|
||||
self.globalHandler = globalHandler
|
||||
self.localHandler = localHandler
|
||||
}
|
||||
|
||||
deinit {
|
||||
stop()
|
||||
}
|
||||
|
||||
func start() {
|
||||
switch monitorType {
|
||||
case .global:
|
||||
startGlobalMonitor()
|
||||
case .local:
|
||||
startLocalMonitor()
|
||||
}
|
||||
}
|
||||
|
||||
func stop() {
|
||||
guard let monitor = self.monitor else { return }
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
+167
-28
@@ -3,61 +3,200 @@
|
||||
// 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 window: NSWindow? {
|
||||
return popoverWindowController?.window
|
||||
}
|
||||
|
||||
public var item: NSStatusItem!
|
||||
public var keepPopoverVisible: Bool = false {
|
||||
didSet {
|
||||
keepPopoverVisible ? removeMonitors() : setupMonitors()
|
||||
}
|
||||
}
|
||||
|
||||
@objc dynamic var item: NSStatusItem! {
|
||||
didSet {
|
||||
prepareMenu()
|
||||
}
|
||||
}
|
||||
|
||||
private let wConfig: PopoverConfiguration
|
||||
private var popoverWindowController: PopoverWindowController?
|
||||
private let windowConfiguration: PopoverConfiguration
|
||||
private var observation: NSKeyValueObservation?
|
||||
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
|
||||
}
|
||||
|
||||
public init(with configuration: PopoverConfiguration) {
|
||||
windowConfiguration = configuration
|
||||
/// Convenience initializer which is using default configuration
|
||||
public convenience override init() {
|
||||
self.init(with: DefaultConfiguration(), menuItems: nil)
|
||||
}
|
||||
|
||||
public func presentPopover(with image: NSImage, contentViewController viewController: NSViewController) {
|
||||
/// Convenience initializer
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - windowConfiguration: Popover window configuration. Subclass `DefaultConfiguration` and override properties or conform to `PopoverConfiguration` protocol.
|
||||
public convenience init(with windowConfiguration: PopoverConfiguration) {
|
||||
self.init(with: windowConfiguration, menuItems: nil)
|
||||
}
|
||||
|
||||
/// Convenience initializer which is using default configuration and optiona menu items
|
||||
///
|
||||
/// - 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 prepare(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)
|
||||
localEventMonitor?.start()
|
||||
}
|
||||
|
||||
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 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()
|
||||
globalEventMonitor?.start()
|
||||
|
||||
item = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
|
||||
guard let button = item.button else { return }
|
||||
|
||||
button.target = self
|
||||
button.action = #selector(handleStatusItemButtonAction(_:))
|
||||
button.image = image
|
||||
DispatchQueue.main.async(execute: {
|
||||
button.isHighlighted = true
|
||||
})
|
||||
}
|
||||
|
||||
@objc private func handleStatusItemButtonAction(_ sender: Any?) {
|
||||
if isPopoverWindowVisible {
|
||||
dismissPopoverWindow()
|
||||
} else {
|
||||
showPopoverWindow()
|
||||
/// Dismisses the Popover with default animation. Animation behavior is `AnimationBehavior.utilityWindow`
|
||||
public func dismiss() {
|
||||
guard isPopoverWindowVisible else { return }
|
||||
popoverWindowController?.dismiss()
|
||||
globalEventMonitor?.stop()
|
||||
|
||||
guard let button = item.button else { return }
|
||||
DispatchQueue.main.async(execute: {
|
||||
button.isHighlighted = false
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public func dismissPopoverWindow() {
|
||||
popoverWindowController?.dismiss()
|
||||
private func removeMonitors() {
|
||||
globalEventMonitor?.stop()
|
||||
globalEventMonitor = nil
|
||||
|
||||
localEventMonitor?.stop()
|
||||
localEventMonitor = nil
|
||||
}
|
||||
|
||||
public func showPopoverWindow() {
|
||||
popoverWindowController?.show()
|
||||
private func configureStatusBarButton(with image: NSImage) {
|
||||
item = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
|
||||
guard let button = item.button else { return }
|
||||
|
||||
image.isTemplate = true
|
||||
button.image = image
|
||||
setTargetAction(for: button)
|
||||
}
|
||||
|
||||
private func configureStatusBarButton(with view: NSView) {
|
||||
item = NSStatusBar.system.statusItem(withLength: NSWidth(view.bounds))
|
||||
guard let button = item.button else { return }
|
||||
|
||||
let statusItemContainer = StatusItemContainerView()
|
||||
|
||||
setTargetAction(for: button)
|
||||
button.addSubview(statusItemContainer)
|
||||
|
||||
statusItemContainer.embed(view)
|
||||
}
|
||||
|
||||
@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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,54 +3,83 @@
|
||||
// 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 }
|
||||
/// 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 effect. 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 effect.
|
||||
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 }
|
||||
}
|
||||
|
||||
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 5
|
||||
}
|
||||
|
||||
public var contentInset: NSEdgeInsets {
|
||||
return NSEdgeInsets(top: 0, left: 0.0, bottom: 0, right: 0.0)
|
||||
open var contentEdgeInsets: NSEdgeInsets {
|
||||
return NSEdgeInsetsZero
|
||||
}
|
||||
|
||||
open var rightEdgeMargin: CGFloat {
|
||||
return 12.0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,37 +3,68 @@
|
||||
// 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 {
|
||||
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
|
||||
)
|
||||
|
||||
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 }
|
||||
|
||||
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
|
||||
backgroundView = PopoverWindowBackgroundView(frame: bounds, windowConfiguration: wConfig)
|
||||
backgroundView?.layer?.edgeAntialiasingMask = .all
|
||||
super.contentView = backgroundView
|
||||
}
|
||||
|
||||
@@ -44,17 +75,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
|
||||
childContentView?.layer?.edgeAntialiasingMask = .all
|
||||
|
||||
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.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),
|
||||
@@ -69,20 +101,12 @@ public class PopoverWindow: NSPanel {
|
||||
}
|
||||
}
|
||||
|
||||
public override func frameRect(forContentRect contentRect: NSRect) -> NSRect {
|
||||
NSMakeRect(NSMinX(contentRect), NSMinY(contentRect), NSWidth(contentRect), NSHeight(contentRect) + windowConfiguration.arrowHeight)
|
||||
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) {
|
||||
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
|
||||
private func updateArrowPosition() {
|
||||
guard let backgroundView = self.backgroundView else { return }
|
||||
backgroundView.arrowXLocation = arrowXLocation
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,16 +3,20 @@
|
||||
// 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
|
||||
var arrowXLocation: CGFloat = 0.0 {
|
||||
didSet {
|
||||
needsDisplay = true
|
||||
}
|
||||
}
|
||||
|
||||
init(frame frameRect: NSRect, windowConfiguration: PopoverConfiguration) {
|
||||
self.windowConfiguration = windowConfiguration
|
||||
self.wConfig = windowConfiguration
|
||||
super.init(frame: frameRect)
|
||||
}
|
||||
|
||||
@@ -20,8 +24,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 +33,21 @@ 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 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)))
|
||||
@@ -61,18 +66,18 @@ 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()
|
||||
|
||||
// NSColor.red.setFill()
|
||||
// controlPoints.fill()
|
||||
|
||||
}
|
||||
|
||||
override var frame: NSRect {
|
||||
|
||||
@@ -3,23 +3,24 @@
|
||||
// Popover
|
||||
//
|
||||
// Created by Ivan Sapozhnik on 29.03.20.
|
||||
// Copyright © 2020 heavylightapps. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
|
||||
class PopoverWindowController: NSWindowController {
|
||||
class PopoverWindowController: NSWindowController, NSWindowDelegate {
|
||||
public private(set) var windowIsOpen: Bool = false
|
||||
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))
|
||||
let window = PopoverWindow.window(with: windowConfiguration)
|
||||
super.init(window: window)
|
||||
window.delegate = self
|
||||
self.contentViewController = contentViewController
|
||||
}
|
||||
|
||||
@@ -27,18 +28,18 @@ class PopoverWindowController: NSWindowController {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
public func show() {
|
||||
func show() {
|
||||
guard !isAnimating else { return }
|
||||
|
||||
updateWindwFrame()
|
||||
window?.alphaValue = 1.0
|
||||
updateWindowFrame()
|
||||
|
||||
showWindow(nil)
|
||||
windowIsOpen = true
|
||||
window?.makeKey()
|
||||
// TODO: animation
|
||||
}
|
||||
|
||||
public func dismiss() {
|
||||
func dismiss() {
|
||||
guard !isAnimating else { return }
|
||||
|
||||
window?.orderOut(self)
|
||||
@@ -46,14 +47,45 @@ 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)
|
||||
|
||||
let windowFrame = NSMakeRect(x, y, NSWidth(window.frame), NSHeight(window.frame))
|
||||
window.setFrame(windowFrame, display: true)
|
||||
func updateWindowFrame() {
|
||||
guard let window = self.window as? PopoverWindow else { return }
|
||||
let windowFrame = visibleStatusItemWindowFrame()
|
||||
window.arrowXLocation = windowFrame.arrowXLocation
|
||||
window.setFrame(windowFrame.frame, display: true)
|
||||
window.appearance = NSAppearance.current
|
||||
}
|
||||
|
||||
private func visibleStatusItemWindowFrame() -> (frame: NSRect, arrowXLocation: CGFloat) {
|
||||
let screenFrame = currentMouseScreen().frame
|
||||
let statusItemRect = popover.item.button?.window?.frame ?? .zero
|
||||
|
||||
guard let window = self.window else { return (.zero, 0.0) }
|
||||
|
||||
let borderWidth = wConfig.borderColor != nil ? wConfig.borderWidth : 0
|
||||
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
|
||||
|
||||
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 {
|
||||
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()
|
||||
}
|
||||
|
||||
func windowDidResize(_ notification: Notification) {
|
||||
updateWindowFrame()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 141 KiB After Width: | Height: | Size: 173 KiB |
Reference in New Issue
Block a user