Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1cf55aa859 | |||
| db47f2cbd6 | |||
| e976b0818f | |||
| 65abdd6244 | |||
| 781fb84d7a | |||
| 994681e321 | |||
| 44c7d2b415 | |||
| 84579bccce |
@@ -2,16 +2,23 @@
|
||||
|
||||
Fully customizable Mac OS drop-down menu
|
||||
|
||||

|
||||

|
||||
|
||||
## Requirements
|
||||
|
||||
* Xcode 11+
|
||||
* Mac OS 10.12+
|
||||
* Swift 5.0 and higher
|
||||
|
||||
## What can be customized? Everything!
|
||||
```swift
|
||||
public protocol Configuration {
|
||||
var titlePadding: Padding.Vertical { get }
|
||||
var titleBottomSpace: CGFloat { get }
|
||||
var titleFont: NSFont? { get }
|
||||
var backgroundColor: NSColor { get }
|
||||
var cornerRadius: CGFloat { get }
|
||||
var hasShadow: Bool { get }
|
||||
var appearsBelowSender: Bool { get }
|
||||
var contentEdgeInsets: NSEdgeInsets { get }
|
||||
var separatorColor: NSColor { get }
|
||||
var separatorThickness: CGFloat { get }
|
||||
@@ -42,7 +49,7 @@ import Cocoa
|
||||
import Menu
|
||||
|
||||
class ViewController: NSViewController {
|
||||
private let myMenu = Menu(with: "Select search engine:")
|
||||
private let myMenu = Menu(with: "Select a search engine:")
|
||||
|
||||
@IBAction func didClickedButton(_ sender: NSButton) {
|
||||
myMenu.show(items: [
|
||||
|
||||
@@ -57,7 +57,7 @@ class ContentViewController: NSViewController {
|
||||
NSLayoutConstraint.activate([
|
||||
label.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: configuration.contentEdgeInsets.left),
|
||||
label.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -configuration.contentEdgeInsets.right),
|
||||
label.topAnchor.constraint(equalTo: view.topAnchor, constant: configuration.titlePadding.top),
|
||||
label.topAnchor.constraint(equalTo: view.topAnchor, constant: configuration.contentEdgeInsets.top)
|
||||
])
|
||||
}
|
||||
|
||||
@@ -67,13 +67,13 @@ class ContentViewController: NSViewController {
|
||||
NSLayoutConstraint.activate([
|
||||
stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
stackView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
|
||||
stackView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -configuration.contentEdgeInsets.bottom)
|
||||
])
|
||||
|
||||
if let titleLabel = titleLabel {
|
||||
stackView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: configuration.titlePadding.bottom).isActive = true
|
||||
stackView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: configuration.titleBottomSpace).isActive = true
|
||||
} else {
|
||||
stackView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
|
||||
stackView.topAnchor.constraint(equalTo: view.topAnchor, constant: configuration.contentEdgeInsets.top).isActive = true
|
||||
}
|
||||
|
||||
menuItems.enumerated().forEach { index, item in
|
||||
|
||||
+17
-10
@@ -43,13 +43,19 @@ public final class Menu {
|
||||
fadeIn(window)
|
||||
}
|
||||
|
||||
public func dismiss() {
|
||||
public func dismiss(animated: Bool) {
|
||||
let actualDismiss: (NSWindow) -> Void = { [weak self] menuWindow in
|
||||
self?.window?.parent?.removeChildWindow(menuWindow)
|
||||
self?.window?.orderOut(self)
|
||||
self?.window = nil
|
||||
}
|
||||
if let menuWindow = window {
|
||||
fadeOut(window: menuWindow) { [weak self] in
|
||||
self?.window?.parent?.removeChildWindow(menuWindow)
|
||||
self?.window?.orderOut(self)
|
||||
self?.window?.close()
|
||||
self?.window = nil
|
||||
if animated {
|
||||
fadeOut(window: menuWindow) {
|
||||
actualDismiss(menuWindow)
|
||||
}
|
||||
} else {
|
||||
actualDismiss(menuWindow)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +90,7 @@ public final class Menu {
|
||||
|
||||
private func setupMonitors(for parentWindow: NSWindow, targetView: NSView) {
|
||||
lostFocusObserver = NotificationCenter.default.addObserver(forName: NSWindow.didResignKeyNotification, object: parentWindow, queue: nil, using: { [weak self] (_ arg1: Notification) -> Void in
|
||||
self?.dismiss()
|
||||
self?.dismiss(animated: false)
|
||||
})
|
||||
|
||||
localMonitor = EventMonitor(monitorType: .local, mask: [.leftMouseDown, .rightMouseDown, .otherMouseDown], globalHandler: nil, localHandler: { [weak self] event -> NSEvent? in
|
||||
@@ -92,7 +98,7 @@ public final class Menu {
|
||||
|
||||
if localEvent.window != self?.window {
|
||||
if localEvent.window == parentWindow {
|
||||
self?.dismiss()
|
||||
self?.dismiss(animated: true)
|
||||
// Ignore clicking on presenting view
|
||||
// let contentView = parentWindow.contentView
|
||||
// let locationTest = contentView?.convert(localEvent.locationInWindow, from: nil)
|
||||
@@ -112,8 +118,9 @@ public final class Menu {
|
||||
|
||||
let presentationFrame = presentationWindow.convertToScreen(view.frame)
|
||||
let presentationPoint = presentationFrame.origin
|
||||
let additionalYOffset = configuration.appearsBelowSender ? 0 : NSHeight(view.frame)
|
||||
|
||||
let newFrame = NSRect(x: presentationPoint.x, y: presentationPoint.y - NSHeight(window.frame), width: NSWidth(view.frame), height: NSHeight(window.frame))
|
||||
let newFrame = NSRect(x: presentationPoint.x, y: presentationPoint.y - NSHeight(window.frame) + additionalYOffset, width: NSWidth(view.frame), height: NSHeight(window.frame))
|
||||
window.setFrame(newFrame, display: true, animate: false)
|
||||
}
|
||||
}
|
||||
@@ -121,6 +128,6 @@ public final class Menu {
|
||||
extension Menu: ContentViewControllerDelegate {
|
||||
func didClickMenuElement(with index: Int) {
|
||||
selectedIndex = index
|
||||
dismiss()
|
||||
dismiss(animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,11 +16,21 @@ public enum Padding {
|
||||
public struct Horizontal {
|
||||
let left: CGFloat
|
||||
let right: CGFloat
|
||||
|
||||
public init(left: CGFloat, right: CGFloat) {
|
||||
self.left = left
|
||||
self.right = right
|
||||
}
|
||||
}
|
||||
|
||||
public struct Vertical {
|
||||
let top: CGFloat
|
||||
let bottom: CGFloat
|
||||
|
||||
public init(top: CGFloat, bottom: CGFloat) {
|
||||
self.top = top
|
||||
self.bottom = bottom
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,11 +47,12 @@ extension Padding.Vertical {
|
||||
}
|
||||
|
||||
public protocol Configuration {
|
||||
var titlePadding: Padding.Vertical { get }
|
||||
var titleBottomSpace: CGFloat { get }
|
||||
var titleFont: NSFont? { get }
|
||||
var backgroundColor: NSColor { get }
|
||||
var cornerRadius: CGFloat { get }
|
||||
var hasShadow: Bool { get }
|
||||
var appearsBelowSender: Bool { get }
|
||||
var contentEdgeInsets: NSEdgeInsets { get }
|
||||
var separatorColor: NSColor { get }
|
||||
var separatorThickness: CGFloat { get }
|
||||
@@ -67,8 +78,8 @@ public protocol Configuration {
|
||||
open class MenuConfiguration: Configuration {
|
||||
public init() {}
|
||||
|
||||
open var titlePadding: Padding.Vertical {
|
||||
return .init(top: .grid1, bottom: .grid1)
|
||||
open var titleBottomSpace: CGFloat {
|
||||
return .grid1
|
||||
}
|
||||
|
||||
open var titleFont: NSFont? {
|
||||
@@ -87,6 +98,10 @@ open class MenuConfiguration: Configuration {
|
||||
return true
|
||||
}
|
||||
|
||||
open var appearsBelowSender: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
open var contentEdgeInsets: NSEdgeInsets {
|
||||
return NSEdgeInsets(top: .grid2, left: .grid2, bottom: .grid2, right: .grid2)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import Cocoa
|
||||
|
||||
final class Window: NSPanel {
|
||||
final class Window: NSWindow {
|
||||
private var childContentView: NSView?
|
||||
private var backgroundView: RoundedRectangleView?
|
||||
private let configuration: Configuration
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 315 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 326 KiB |
Reference in New Issue
Block a user