20 Commits

Author SHA1 Message Date
Sapozhnik Ivan 76733940b2 Update README.md 2021-11-19 23:04:48 +01:00
Sapozhnik Ivan a58be98a55 Update README.md 2020-09-15 11:22:31 +02:00
Sapozhnik Ivan 5f506926e0 Update README.md 2020-04-23 14:57:03 +02:00
Ivan Sapozhnik bde7399d29 Merge branch 'master' of https://github.com/iSapozhnik/Menu
# Conflicts:
#	README.md
2020-04-23 14:55:42 +02:00
Ivan Sapozhnik bcf6ce5e79 update readme 2020-04-23 14:53:23 +02:00
Sapozhnik Ivan 85a7c47899 Update README.md 2020-04-22 21:47:42 +02:00
Sapozhnik Ivan a0ab9eb3db Merge pull request #9 from iSapozhnik/feature/spacing-between-items
Spacing between items
2020-04-22 21:44:58 +02:00
Ivan Sapozhnik 322413636d Spacing between items 2020-04-22 21:43:25 +02:00
Sapozhnik Ivan e6489c051b Update README.md 2020-04-22 21:37:02 +02:00
Sapozhnik Ivan f7122c6a55 Merge pull request #7 from iSapozhnik/feature/Custom-views
Custom views
2020-04-22 21:34:28 +02:00
Ivan Sapozhnik 78b2512f31 Custom views implementation 2020-04-22 20:14:33 +02:00
Ivan Sapozhnik 0084cee59c More configuration, fixing appearance position, selection item option 2020-04-22 17:10:04 +02:00
Ivan Sapozhnik 159d0a9a01 Readme 2020-04-19 17:21:11 +02:00
Ivan Sapozhnik d0bdb178c8 Readme 2020-04-19 17:06:41 +02:00
Ivan Sapozhnik 18fe80d65d update examples 2020-04-19 17:04:12 +02:00
Ivan Sapozhnik 34c1038265 Shoelds 2020-04-19 16:52:16 +02:00
Ivan Sapozhnik 05548b0201 Readme 2020-04-19 16:40:32 +02:00
Ivan Sapozhnik cb13380343 Removing Button -> will make another package for it 2020-04-18 11:39:01 +02:00
Ivan Sapozhnik ec077ed717 Add everything back 2020-04-17 18:02:40 +02:00
Ivan Sapozhnik 538d5a31be Remove all 2020-04-17 18:02:20 +02:00
16 changed files with 168 additions and 110 deletions
+33 -5
View File
@@ -1,13 +1,22 @@
# Menu
>Fully customizable Mac OS drop-down menu. It includes **30** settings you can play with.
<div align="center">
![](screenshot1.png)
![Swift](https://img.shields.io/badge/%20in-swift%205.0-orange.svg)
![macOS](https://img.shields.io/badge/macOS-10.12-green.svg)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FiSapozhnik%2FMenu%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/iSapozhnik/Menu)
</div>
>Fully customizable macOS drop-down menu. It includes **30** settings you can play with.
![](screenshot2.png)
## Requirements
* Xcode 11+
* Mac OS 10.12+
* macOS 10.12+
* Swift 5.0 and higher
## Installation
@@ -19,7 +28,7 @@ Update your `Package.swift` dependencies:
```
dependencies: [
.package(url: "https://github.com/iSapozhnik/Menu", from: "1.0.8")
.package(url: "https://github.com/iSapozhnik/Menu", from: "1.10.1")
]
```
@@ -39,6 +48,7 @@ public protocol Configuration {
var cornerRadius: CGFloat { get }
var hasShadow: Bool { get }
var appearsBelowSender: Bool { get }
var presentingOffset: CGFloat { get }
var animationDuration: TimeInterval { get }
var contentEdgeInsets: NSEdgeInsets { get }
var maximumContentHeight: CGFloat? { get }
@@ -58,6 +68,7 @@ public protocol Configuration {
var menuItemHoverCheckmarkColor: NSColor { get }
var menuItemCheckmarkHeight: CGFloat { get }
var menuItemCheckmarkThikness: CGFloat { get }
var menuItemHorizontalSpacing: CGFloat { get }
var menuItemImageHeight: CGFloat? { get }
var menuItemImageTintColor: NSColor? { get }
var menuItemHoverImageTintColor: NSColor? { get }
@@ -111,12 +122,23 @@ class ViewController: NSViewController {
myMenu.addItems(menuItems)
}
@IBAction func didClickedButton(_ sender: NSButton) {
@IBAction func didClickButton(_ sender: NSButton) {
myMenu.show(from: sender)
}
}
```
## Examples
In this section I've collected some examples of what can be i,plemented do using **Menu** control. On the left side some random example from Dribbble and on the right side my implementation.
| Dribbble | Menu | Code |
| ------------- |:-------------:|:-------------:|
| [link](https://dribbble.com/shots/4233782-Snooze-notifications-in-Twist) | |
| ![](examples/twist.png) | ![](examples/menu_twist.png) | |
| [link](https://dribbble.com/shots/7055473-Dropdowns) | | |
| ![](examples/example_0.png) | ![](examples/menu_0.png) | [code](examples/examples.md) |
## Credits
Created and maintained by [**@iSapozhnik**](https://twitter.com/iSapozhnik).
@@ -127,3 +149,9 @@ Released under the MIT License. See `LICENSE` for details.
>**Copyright &copy; 2020-present Sapozhnik Ivan.**
<!--
https://dribbble.com/shots/4953294-Daily-UI-Challenge-04-Dropdown-Menu
https://dribbble.com/shots/7055473-Dropdowns
-->
+1 -8
View File
@@ -136,14 +136,7 @@ class ContentViewController: NSViewController {
}
private func addMenuElement(with menuItem: MenuItem, isSelected: Bool) {
let menuElement = MenuElement(
text: menuItem.title,
image: menuItem.image,
isSelected: isSelected,
isEnabled: menuItem.isEnabled,
configuration: configuration,
action: menuItem.action ?? {}
)
let menuElement = MenuElement(with: menuItem, isSelected: isSelected, configuration: configuration)
menuElement.translatesAutoresizingMaskIntoConstraints = false
menuElement.delegate = self
menuElement.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
+8 -8
View File
@@ -174,19 +174,19 @@ extension NSEdgeInsets {
extension CGFloat {
/// 0 points
public static let grid0: CGFloat = 0.0
static let grid0: CGFloat = 0.0
/// 4 points
public static let gridHalf: CGFloat = 4.0
static let gridHalf: CGFloat = 4.0
/// 8 points
public static let grid1: CGFloat = 8.0
static let grid1: CGFloat = 8.0
/// 16 points
public static let grid2: CGFloat = 16.0
static let grid2: CGFloat = 16.0
/// 24 points
public static let grid3: CGFloat = 24.0
static let grid3: CGFloat = 24.0
/// 32 points
public static let grid4: CGFloat = 32.0
static let grid4: CGFloat = 32.0
/// 40 points
public static let grid5: CGFloat = 40.0
static let grid5: CGFloat = 40.0
/// 48 points
public static let grid6: CGFloat = 48.0
static let grid6: CGFloat = 48.0
}
+15 -5
View File
@@ -58,6 +58,13 @@ public final class Menu {
}
}
// MARK: - Selection
public func selectItem(at index: Int) {
guard let item = item(at: index) else { return }
selectedId = item.id
item.action?()
}
// MARK: - Adding and Removing Menu Items
public func insertItem(_ item: MenuItem, at index: Int) {
items.insert(item, at: index)
@@ -208,13 +215,16 @@ public final class Menu {
}
private func setFrame(for window: NSWindow, relativeTo view: NSView) {
guard let parentWindow = view.window else { return }
guard let parentWindow = view.window, let topMostSuperView = parentWindow.contentView else { return }
let presentationFrame = parentWindow.convertToScreen(view.frame)
let presentationPoint = presentationFrame.origin
let additionalYOffset = configuration.appearsBelowSender ? 0 : NSHeight(view.frame)
let locationInWindow = view.convert(topMostSuperView.frame.origin, to: nil)
let rectInWindow = NSRect(origin: locationInWindow, size: CGSize(width: NSWidth(view.frame), height: NSHeight(window.frame)))
let rectInScreen = parentWindow.convertToScreen(rectInWindow)
let origin = rectInScreen.origin
var additionalYOffset = configuration.appearsBelowSender ? NSHeight(view.frame) : 0
additionalYOffset += abs(configuration.presentingOffset)
let newFrame = NSRect(x: origin.x, y: origin.y - NSHeight(window.frame) - additionalYOffset, 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)
}
}
-63
View File
@@ -1,63 +0,0 @@
//
// File.swift
//
//
// Created by Ivan Sapozhnik on 16.04.20.
//
import Cocoa
public class MenuButton: NSButton, CALayerDelegate {
private var containerLayer = CALayer()
private var titleLayer = CATextLayer()
required public init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
override init(frame: NSRect) {
super.init(frame: frame)
setup()
}
func setup() {
wantsLayer = true
layer?.masksToBounds = false
layer?.cornerRadius = 4
layer?.borderWidth = 1
layer?.delegate = self
titleLayer.delegate = self
if let scale = window?.backingScaleFactor {
titleLayer.contentsScale = scale
}
containerLayer.masksToBounds = false
containerLayer.shadowOffset = NSSize.zero
containerLayer.shadowColor = NSColor.clear.cgColor
containerLayer.frame = NSMakeRect(0, 0, bounds.width, bounds.height)
containerLayer.addSublayer(titleLayer)
layer?.addSublayer(containerLayer)
setupTitle()
}
func setupTitle() {
guard let font = font else { return }
titleLayer.string = title
titleLayer.font = font
titleLayer.fontSize = font.pointSize
let attributes = [NSAttributedString.Key.font: font as Any]
let titleSize = title.size(withAttributes: attributes)
var titleRect = NSMakeRect(0, 0, titleSize.width, titleSize.height)
titleRect.origin.y = round((bounds.height - titleSize.height)/2)
titleRect.origin.x = round((bounds.width - titleSize.width)/2)
titleLayer.frame = titleRect
}
}
+10
View File
@@ -54,6 +54,7 @@ public protocol Configuration {
var cornerRadius: CGFloat { get }
var hasShadow: Bool { get }
var appearsBelowSender: Bool { get }
var presentingOffset: CGFloat { get }
var animationDuration: TimeInterval { get }
var contentEdgeInsets: NSEdgeInsets { get }
var maximumContentHeight: CGFloat? { get }
@@ -73,6 +74,7 @@ public protocol Configuration {
var menuItemHoverCheckmarkColor: NSColor { get }
var menuItemCheckmarkHeight: CGFloat { get }
var menuItemCheckmarkThikness: CGFloat { get }
var menuItemHorizontalSpacing: CGFloat { get }
var menuItemImageHeight: CGFloat? { get }
var menuItemImageTintColor: NSColor? { get }
var menuItemHoverImageTintColor: NSColor? { get }
@@ -110,6 +112,10 @@ open class MenuConfiguration: Configuration {
return true
}
open var presentingOffset: CGFloat {
return 0.0
}
open var animationDuration: TimeInterval {
return 0.15
}
@@ -186,6 +192,10 @@ open class MenuConfiguration: Configuration {
return 1.0
}
open var menuItemHorizontalSpacing: CGFloat {
return .grid1
}
open var menuItemImageHeight: CGFloat? {
return .grid3
}
+54 -19
View File
@@ -17,18 +17,58 @@ class MenuElement: NSView {
private let configuration: Configuration
private var checkmark: CheckmarkView!
init(text: String, image: NSImage? = nil, isSelected: Bool = false, isEnabled: Bool, configuration: Configuration, action: @escaping () -> Void) {
init(with menuItem: MenuItem, isSelected: Bool = false, configuration: Configuration) {
self.configuration = configuration
handler = action
handler = menuItem.action ?? {}
super.init(frame: .zero)
alphaValue = isEnabled ? 1.0 : 0.5
alphaValue = menuItem.isEnabled ? 1.0 : 0.5
if let customView = menuItem.customView {
makeCustomViewElement(with: customView)
} else {
makeStandardElement(with: menuItem, isSelected: isSelected)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func menuElementClicked(_ sender: Control) {
handler()
delegate?.didClickMenuElement(self)
}
private func makeCustomViewElement(with customView: NSView) {
customView.translatesAutoresizingMaskIntoConstraints = false
addSubview(customView)
let leadingConstraint: NSLayoutConstraint
let trailingConstraint: NSLayoutConstraint
switch configuration.textAlignment {
case .left:
leadingConstraint = customView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: configuration.contentEdgeInsets.left)
trailingConstraint = customView.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor, constant: -configuration.contentEdgeInsets.right)
case .right:
leadingConstraint = customView.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor, constant: configuration.contentEdgeInsets.left)
trailingConstraint = customView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -configuration.contentEdgeInsets.right)
}
NSLayoutConstraint.activate([
leadingConstraint,
trailingConstraint,
customView.topAnchor.constraint(equalTo: topAnchor),
customView.bottomAnchor.constraint(equalTo: bottomAnchor)
])
}
private func makeStandardElement(with menuItem: MenuItem, isSelected: Bool) {
let stackView = makeHorizontalStackView()
var lImageView: NSImageView? = nil
var rImageView: NSImageView? = nil
let image = menuItem.image
switch configuration.iconAlignment {
case .left:
lImageView = makeLeftImageView(with: image)
@@ -36,7 +76,7 @@ class MenuElement: NSView {
rImageView = makeRightImageView(with: image)
}
let label = makeLabel(with: text)
let label = makeLabel(with: menuItem.title)
if let lImageView = lImageView {
stackView.addArrangedSubview(lImageView)
@@ -70,11 +110,15 @@ class MenuElement: NSView {
if isSelected {
checkmarkView.animate(duration: 0)
if #available(OSX 10.14, *) {
lImageView?.contentTintColor = isSelected ? configuration.menuItemHoverImageTintColor : configuration.menuItemImageTintColor
}
label.textColor = isSelected ? configuration.menuItemHoverTextColor : configuration.menuItemTextColor
}
checkmark = checkmarkView
}
let control = makeHoverControl(update: label, leftImageView: lImageView, rightImageView: rImageView, isEnabled: isEnabled)
let control = makeHoverControl(update: label, leftImageView: lImageView, rightImageView: rImageView, isEnabled: menuItem.isEnabled, isSelected: isSelected)
addSubview(control)
addSubview(stackView)
@@ -102,21 +146,12 @@ class MenuElement: NSView {
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func menuElementClicked(_ sender: Control) {
handler()
delegate?.didClickMenuElement(self)
}
private func makeHorizontalStackView() -> NSStackView {
let stackView = NSStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.orientation = .horizontal
stackView.distribution = .fill
stackView.spacing = .grid1
stackView.spacing = configuration.menuItemHorizontalSpacing
return stackView
}
@@ -168,15 +203,15 @@ class MenuElement: NSView {
return lImageView
}
private func makeHoverControl(update label: NSTextField, leftImageView: NSImageView?, rightImageView: NSImageView?, isEnabled: Bool) -> Control {
private func makeHoverControl(update label: NSTextField, leftImageView: NSImageView?, rightImageView: NSImageView?, isEnabled: Bool, isSelected: Bool) -> Control {
let control = Control(with: configuration)
control.isEnabled = isEnabled
control.hover = { [weak self] isHover in
guard let self = self else { return }
label.textColor = isHover ? self.configuration.menuItemHoverTextColor : self.configuration.menuItemTextColor
label.textColor = isHover ? self.configuration.menuItemHoverTextColor : isSelected ? self.configuration.menuItemHoverImageTintColor : self.configuration.menuItemTextColor
if #available(OSX 10.14, *) {
leftImageView?.contentTintColor = isHover ? self.configuration.menuItemHoverImageTintColor : self.configuration.menuItemImageTintColor
rightImageView?.contentTintColor = isHover ? self.configuration.menuItemHoverImageTintColor : self.configuration.menuItemImageTintColor
leftImageView?.contentTintColor = isHover ? self.configuration.menuItemHoverImageTintColor : isSelected ? self.configuration.menuItemHoverImageTintColor : self.configuration.menuItemImageTintColor
rightImageView?.contentTintColor = isHover ? self.configuration.menuItemHoverImageTintColor : isSelected ? self.configuration.menuItemHoverImageTintColor : self.configuration.menuItemImageTintColor
}
}
+9 -2
View File
@@ -9,18 +9,25 @@ import Cocoa
public struct MenuItem: CustomDebugStringConvertible {
public var isEnabled = true
public let id: UUID
let action: (() -> Void)?
let title: String
let image: NSImage?
let isSelectable: Bool
var customView: NSView?
var isSeparator = false
public let id: UUID
public init(_ title: String, image: NSImage? = nil, isSelectable: Bool = true, action: (() -> Void)?) {
public init(_ customView: NSView) {
self.init("", image: nil, customView: customView, isSelectable: false, action: nil)
}
public init(_ title: String, image: NSImage? = nil, customView: NSView? = nil, isSelectable: Bool = true, action: (() -> Void)?) {
self.action = action
self.title = title
self.image = image
self.customView = customView
self.isSelectable = isSelectable
id = UUID()
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

+38
View File
@@ -0,0 +1,38 @@
## Example
![](menu_0.png)
### Code:
```
class Config: MenuConfiguration {
override var cornerRadius: CGFloat {
return 15.0
}
override var backgroundColor: NSColor {
return NSColor(red: 63/255, green: 59/255, blue: 59/255, alpha: 1.0)
}
override var menuItemHoverBackgroundColor: NSColor {
return NSColor(red: 86/255, green: 81/255, blue: 81/255, alpha: 1.0)
}
override var menuItemHoverCornerRadius: CGFloat {
return 10.0
}
override var contentEdgeInsets: NSEdgeInsets {
return NSEdgeInsets(top: 8, left: 16, bottom: 8, right: 16)
}
override var menuItemHeight: CGFloat {
return 40.0
}
override var menuItemHoverEdgeInsets: NSEdgeInsets {
return NSEdgeInsets(top: 0, left: 8, bottom: 0, right: 8)
}
}
```
Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 211 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 458 KiB