Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6320d17adb | ||
|
|
f1f68383d8 | ||
|
|
9f92012270 | ||
|
|
dee3296f9e | ||
|
|
b30566900b | ||
|
|
53771db7fe | ||
|
|
08efd190ee | ||
|
|
dc5346fbda | ||
|
|
664d0c881b | ||
|
|
7f62847149 | ||
|
|
0e47c6f786 | ||
|
|
7d2b8bc923 | ||
|
|
168b893236 | ||
|
|
c95768cc8a | ||
|
|
d4743915c2 | ||
|
|
cde8f5a3ad | ||
|
|
36d8baa3d4 | ||
|
|
10edd0c425 | ||
|
|
17391871bd | ||
|
|
a0016a047c |
@@ -0,0 +1,19 @@
|
||||
name: Swift
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: macos-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Build
|
||||
run: swift build -v
|
||||
- name: Run tests
|
||||
run: swift test -v
|
||||
@@ -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,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.1.1")
|
||||
]
|
||||
```
|
||||
|
||||
### 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)
|
||||
|
||||
@@ -8,7 +8,13 @@
|
||||
import Cocoa
|
||||
|
||||
public class Popover: NSObject {
|
||||
var item: NSStatusItem! {
|
||||
public var window: NSWindow? {
|
||||
return popoverWindowController?.window
|
||||
}
|
||||
|
||||
public var keepPopoverVisible: Bool = false
|
||||
|
||||
@objc dynamic var item: NSStatusItem! {
|
||||
didSet {
|
||||
prepareMenu()
|
||||
}
|
||||
@@ -30,7 +36,15 @@ public class Popover: NSObject {
|
||||
self.init(with: DefaultConfiguration(), menuItems: nil)
|
||||
}
|
||||
|
||||
/// Convenience initializer which is using default configuration
|
||||
/// 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
|
||||
@@ -72,11 +86,20 @@ public class Popover: NSObject {
|
||||
localEventMonitor?.start()
|
||||
}
|
||||
|
||||
/// Shows the Popover with no animation
|
||||
public func show() {
|
||||
self.show(withFocus: true)
|
||||
}
|
||||
|
||||
/// Shows the Popover with no animation
|
||||
public func show(withFocus: Bool) {
|
||||
guard !isPopoverWindowVisible else { return }
|
||||
popoverWindowController?.show()
|
||||
popoverWindowController?.show(withFocus: withFocus)
|
||||
globalEventMonitor?.start()
|
||||
|
||||
guard let button = item.button else { return }
|
||||
DispatchQueue.main.async(execute: {
|
||||
button.isHighlighted = true
|
||||
})
|
||||
}
|
||||
|
||||
/// Dismisses the Popover with default animation. Animation behavior is `AnimationBehavior.utilityWindow`
|
||||
@@ -84,12 +107,19 @@ public class Popover: NSObject {
|
||||
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()
|
||||
if (!self.keepPopoverVisible) {
|
||||
self.dismiss()
|
||||
}
|
||||
}, localHandler: nil)
|
||||
|
||||
if menuItems != nil, menuItems?.isNotEmpty ?? false {
|
||||
@@ -109,6 +139,14 @@ public class Popover: NSObject {
|
||||
}
|
||||
}
|
||||
|
||||
private func removeMonitors() {
|
||||
globalEventMonitor?.stop()
|
||||
globalEventMonitor = nil
|
||||
|
||||
localEventMonitor?.stop()
|
||||
localEventMonitor = nil
|
||||
}
|
||||
|
||||
private func configureStatusBarButton(with image: NSImage) {
|
||||
item = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
|
||||
guard let button = item.button else { return }
|
||||
@@ -131,7 +169,7 @@ public class Popover: NSObject {
|
||||
}
|
||||
|
||||
@objc private func handleStatusItemButtonAction(_ sender: Any?) {
|
||||
isPopoverWindowVisible ? dismiss() : show()
|
||||
isPopoverWindowVisible ? dismiss() : show(withFocus: true)
|
||||
}
|
||||
|
||||
private func setTargetAction(for button: NSButton) {
|
||||
|
||||
@@ -16,12 +16,12 @@ public protocol PopoverConfiguration {
|
||||
|
||||
/// Popover's border color.
|
||||
/// - Important:
|
||||
/// If `borderColor` returns `nil`, settings `borderWidth` won't make any affect. See also: `borderWidth`.
|
||||
/// 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 affect.
|
||||
/// If Popover's border color is set to `nil`, setting `borderWidth` won't make any effect.
|
||||
var borderWidth: CGFloat { get }
|
||||
|
||||
/// Defines Popover arrow height.
|
||||
|
||||
@@ -44,7 +44,7 @@ class PopoverWindow: NSPanel {
|
||||
static func window(with configuration: PopoverConfiguration) -> PopoverWindow {
|
||||
let window = PopoverWindow.init(
|
||||
contentRect: .zero,
|
||||
styleMask: .nonactivatingPanel,
|
||||
styleMask: [.nonactivatingPanel],
|
||||
backing: .buffered,
|
||||
defer: true,
|
||||
configuration: configuration
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import Cocoa
|
||||
|
||||
class PopoverWindowController: NSWindowController {
|
||||
class PopoverWindowController: NSWindowController, NSWindowDelegate {
|
||||
public private(set) var windowIsOpen: Bool = false
|
||||
public private(set) var isAnimating: Bool = false
|
||||
|
||||
@@ -18,7 +18,9 @@ class PopoverWindowController: NSWindowController {
|
||||
self.popover = popover
|
||||
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
|
||||
}
|
||||
|
||||
@@ -26,14 +28,18 @@ class PopoverWindowController: NSWindowController {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func show() {
|
||||
func show(withFocus: Bool) {
|
||||
guard !isAnimating else { return }
|
||||
|
||||
updateWindowFrame()
|
||||
window?.alphaValue = 1.0
|
||||
showWindow(nil)
|
||||
|
||||
if (withFocus) {
|
||||
showWindow(nil)
|
||||
window?.makeKey()
|
||||
} else {
|
||||
window?.orderFrontRegardless()
|
||||
}
|
||||
windowIsOpen = true
|
||||
window?.makeKey()
|
||||
// TODO: animation
|
||||
}
|
||||
|
||||
@@ -45,7 +51,7 @@ class PopoverWindowController: NSWindowController {
|
||||
windowIsOpen = false
|
||||
}
|
||||
|
||||
private func updateWindowFrame() {
|
||||
func updateWindowFrame() {
|
||||
guard let window = self.window as? PopoverWindow else { return }
|
||||
let windowFrame = visibleStatusItemWindowFrame()
|
||||
window.arrowXLocation = windowFrame.arrowXLocation
|
||||
@@ -82,4 +88,8 @@ class PopoverWindowController: NSWindowController {
|
||||
|
||||
return currentScreen ?? NSScreen.main ?? NSScreen()
|
||||
}
|
||||
|
||||
func windowDidResize(_ notification: Notification) {
|
||||
updateWindowFrame()
|
||||
}
|
||||
}
|
||||
|
||||
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 141 KiB After Width: | Height: | Size: 173 KiB |
Reference in New Issue
Block a user