feat(menu): use view to draw proxy item [beta] [appcenter] [notarize]

This commit is contained in:
yicheng 2020-02-22 18:31:13 +08:00
parent 0a1d259b72
commit b8d5d007e5
9 changed files with 139 additions and 147 deletions

View File

@ -54,6 +54,7 @@
49D176A72355FE680093DD7B /* NetworkChangeNotifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49D176A62355FE680093DD7B /* NetworkChangeNotifier.swift */; };
49D176A9235614340093DD7B /* ProxyGroupSpeedTestMenuItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49D176A8235614340093DD7B /* ProxyGroupSpeedTestMenuItem.swift */; };
49D176AB23575BB20093DD7B /* ProxyGroupMenuItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49D176AA23575BB20093DD7B /* ProxyGroupMenuItemView.swift */; };
F910AA24240134AF00116E95 /* ProxyGroupMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = F910AA23240134AF00116E95 /* ProxyGroupMenu.swift */; };
F915A4622366ADEF004840BE /* ClashConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = F915A4612366ADEF004840BE /* ClashConnection.swift */; };
F9203A26236342820020D57D /* AppDelegate+..swift in Sources */ = {isa = PBXBuildFile; fileRef = F9203A25236342820020D57D /* AppDelegate+..swift */; };
F92D0B24236BC12000575E15 /* SavedProxyModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F92D0B23236BC12000575E15 /* SavedProxyModel.swift */; };
@ -168,6 +169,7 @@
49D176AA23575BB20093DD7B /* ProxyGroupMenuItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxyGroupMenuItemView.swift; sourceTree = "<group>"; };
5217C006C5A22A1CEA24BFC1 /* Pods-ClashX.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ClashX.debug.xcconfig"; path = "Pods/Target Support Files/Pods-ClashX/Pods-ClashX.debug.xcconfig"; sourceTree = "<group>"; };
A1485BCE642059532D01B8BA /* Pods-ClashX.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ClashX.release.xcconfig"; path = "Pods/Target Support Files/Pods-ClashX/Pods-ClashX.release.xcconfig"; sourceTree = "<group>"; };
F910AA23240134AF00116E95 /* ProxyGroupMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxyGroupMenu.swift; sourceTree = "<group>"; };
F915A4612366ADEF004840BE /* ClashConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClashConnection.swift; sourceTree = "<group>"; };
F9203A25236342820020D57D /* AppDelegate+..swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+..swift"; sourceTree = "<group>"; };
F92D0B23236BC12000575E15 /* SavedProxyModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedProxyModel.swift; sourceTree = "<group>"; };
@ -301,6 +303,7 @@
499A485922ED781100F6C675 /* RemoteConfigAddView.xib */,
495340B220DE68C300B0D3FF /* StatusItemView.swift */,
495340AF20DE5F7200B0D3FF /* StatusItemView.xib */,
F910AA23240134AF00116E95 /* ProxyGroupMenu.swift */,
);
path = Views;
sourceTree = "<group>";
@ -668,6 +671,7 @@
499A486522EEA3FD00F6C675 /* Array+Safe.swift in Sources */,
F92D0B24236BC12000575E15 /* SavedProxyModel.swift in Sources */,
F92D0B2A236C759100575E15 /* NSTextField+Vibrancy.swift in Sources */,
F910AA24240134AF00116E95 /* ProxyGroupMenu.swift in Sources */,
4952C3BF2115C7CA004A4FA8 /* MenuItemFactory.swift in Sources */,
F977FAAC2366790500C17F1F /* AutoUpgardeManager.swift in Sources */,
499A485822ED715200F6C675 /* RemoteConfigModel.swift in Sources */,

View File

@ -88,8 +88,7 @@ class MenuItemFactory {
if !ConfigManager.shared.disableShowCurrentProxyInMenu {
menu.view = ProxyGroupMenuItemView(group: proxyGroup.name, targetProxy: selectedName)
}
let submenu = NSMenu(title: proxyGroup.name)
var hasSelected = false
let submenu = ProxyGroupMenu(title: proxyGroup.name)
for proxy in proxyGroup.all ?? [] {
guard let proxyModel = proxyMap[proxy] else { continue }
@ -98,17 +97,11 @@ class MenuItemFactory {
continue
}
let proxyItem = ProxyMenuItem(proxy: proxyModel, action: #selector(MenuItemFactory.actionSelectProxy(sender:)),
maxProxyNameLength: proxyGroup.maxProxyNameLength)
selected: proxy == selectedName)
proxyItem.target = MenuItemFactory.self
proxyItem.isSelected = proxy == selectedName
if proxyItem.isSelected { hasSelected = true }
submenu.add(delegate: proxyItem)
submenu.addItem(proxyItem)
}
if !hasSelected && submenu.items.count > 0 {
actionSelectProxy(sender: submenu.items[0] as! ProxyMenuItem)
}
addSpeedTestMenuItem(submenu, proxyGroup: proxyGroup)
menu.submenu = submenu
if !ConfigManager.shared.disableShowCurrentProxyInMenu {
@ -145,17 +138,18 @@ class MenuItemFactory {
return menu
}
private static func addSpeedTestMenuItem(_ menus: NSMenu, proxyGroup: ClashProxy) {
private static func addSpeedTestMenuItem(_ menu: NSMenu, proxyGroup: ClashProxy) {
guard proxyGroup.speedtestAble.count > 0 else { return }
let speedTestItem = ProxyGroupSpeedTestMenuItem(group: proxyGroup)
let separator = NSMenuItem.separator()
if showSpeedTestItemAtTop {
menus.insertItem(separator, at: 0)
menus.insertItem(speedTestItem, at: 0)
menu.insertItem(separator, at: 0)
menu.insertItem(speedTestItem, at: 0)
} else {
menus.addItem(separator)
menus.addItem(speedTestItem)
menu.addItem(separator)
menu.addItem(speedTestItem)
}
(menu as? ProxyGroupMenu)?.add(delegate: speedTestItem)
}
private static func generateHistoryMenu(_ proxy: ClashProxy) -> NSMenu? {
@ -171,15 +165,15 @@ class MenuItemFactory {
let proxyMap = proxyInfo.proxiesMap
let menu = NSMenuItem(title: proxyGroup.name, action: nil, keyEquivalent: "")
let submenu = NSMenu(title: proxyGroup.name)
let submenu = ProxyGroupMenu(title: proxyGroup.name)
for proxy in proxyGroup.all ?? [] {
guard let proxyModel = proxyMap[proxy] else { continue }
let proxyItem = ProxyMenuItem(proxy: proxyModel,
action: #selector(empty),
maxProxyNameLength: proxyGroup.maxProxyNameLength)
proxyItem.isSelected = false
selected: false)
proxyItem.target = MenuItemFactory.self
submenu.add(delegate: proxyItem)
submenu.addItem(proxyItem)
}
addSpeedTestMenuItem(submenu, proxyGroup: proxyGroup)

View File

@ -99,19 +99,6 @@ class ClashProxy: Codable {
private enum CodingKeys: String, CodingKey {
case type, all, history, now, name
}
lazy var maxProxyName: String = {
return all?.max { $1.count > $0.count } ?? ""
}()
lazy var maxProxyNameLength: CGFloat = {
let rect = CGSize(width: CGFloat.greatestFiniteMagnitude, height: 20)
let attr = [NSAttributedString.Key.font: NSFont.menuBarFont(ofSize: 14)]
return (self.maxProxyName as NSString)
.boundingRect(with: rect,
options: .usesLineFragmentOrigin,
attributes: attr).width
}()
}
class ClashProxyResp {

View File

@ -6,22 +6,16 @@
// Copyright © 2019 west2online. All rights reserved.
//
import Carbon
import Cocoa
class MenuItemBaseView: NSView {
private var isMouseInsideView = false
private var isMenuOpen = false
private var eventHandler: EventHandlerRef?
private let handleClick: Bool
private let autolayout: Bool
// MARK: Public
var isHighlighted: Bool {
let enable = enclosingMenuItem?.isEnabled ?? true
return (isMouseInsideView || isMenuOpen) && enable
}
var isHighlighted: Bool = false
let effectView: NSVisualEffectView = {
let effectView = NSVisualEffectView()
@ -32,17 +26,18 @@ class MenuItemBaseView: NSView {
return effectView
}()
var labels: [NSTextField] {
var cells: [NSCell?] {
assertionFailure("Please override")
return []
}
static let labelFont = NSFont.menuFont(ofSize: 14)
var labels: [NSTextField] {
return []
}
init(frame frameRect: NSRect = NSRect(x: 0, y: 0, width: 0, height: 20),
handleClick: Bool,
autolayout: Bool) {
self.handleClick = handleClick
static let labelFont = NSFont.menuBarFont(ofSize: 0)
init(frame frameRect: NSRect = NSRect(x: 0, y: 0, width: 0, height: 20), autolayout: Bool) {
self.autolayout = autolayout
super.init(frame: frameRect)
setupView()
@ -60,10 +55,6 @@ class MenuItemBaseView: NSView {
assertionFailure("Please override this method")
}
func updateBackground(_ label: NSTextField) {
label.cell?.backgroundStyle = isHighlighted ? .emphasized : .normal
}
// MARK: Private
private func setupView() {
@ -82,21 +73,19 @@ class MenuItemBaseView: NSView {
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
effectView.material = isHighlighted ? .selection : .popover
labels.forEach { updateBackground($0) }
labels.forEach { $0.textColor = (enclosingMenuItem?.isEnabled ?? true) ? NSColor.labelColor : NSColor.placeholderTextColor }
let highlighted = isHighlighted && (enclosingMenuItem?.isEnabled ?? false)
effectView.material = highlighted ? .selection : .popover
cells.forEach { $0?.backgroundStyle = isHighlighted ? .emphasized : .normal }
}
override func viewWillMove(toWindow newWindow: NSWindow?) {
super.viewWillMove(toWindow: newWindow)
if let newWindow = newWindow,!newWindow.isKeyWindow {
if let newWindow = newWindow, !newWindow.isKeyWindow {
newWindow.becomeKey()
}
updateTrackingAreas()
}
override func viewDidMoveToWindow() {
super.viewDidMoveToWindow()
}
override func viewDidMoveToSuperview() {
super.viewDidMoveToSuperview()
@ -108,38 +97,13 @@ class MenuItemBaseView: NSView {
}
}
override func updateTrackingAreas() {
super.updateTrackingAreas()
}
override func mouseUp(with event: NSEvent) {
DispatchQueue.main.async {
self.didClickView()
}
}
override func updateTrackingAreas() {
super.updateTrackingAreas()
trackingAreas.forEach { removeTrackingArea($0) }
enclosingMenuItem?.submenu?.delegate = self
addTrackingArea(NSTrackingArea(rect: bounds, options: [.mouseEnteredAndExited, .activeAlways], owner: self, userInfo: nil))
}
override func mouseEntered(with event: NSEvent) {
isMouseInsideView = true
setNeedsDisplay()
}
override func mouseExited(with event: NSEvent) {
isMouseInsideView = false
setNeedsDisplay()
}
}
extension MenuItemBaseView: NSMenuDelegate {
func menuWillOpen(_ menu: NSMenu) {
isMenuOpen = true
setNeedsDisplay()
}
func menuDidClose(_ menu: NSMenu) {
isMenuOpen = false
setNeedsDisplay()
}
}

View File

@ -0,0 +1,43 @@
//
// ProxyGroupMenu.swift
// ClashX
//
// Created by yicheng on 2020/2/22.
// Copyright © 2020 west2online. All rights reserved.
//
import AppKit
@objc protocol ProxyGroupMenuHighlightDelegate: class {
func highlight(item: NSMenuItem?)
}
class ProxyGroupMenu: NSMenu {
var highlightDelegates = NSHashTable<ProxyGroupMenuHighlightDelegate>.weakObjects()
override init(title: String) {
super.init(title: title)
delegate = self
}
required init(coder: NSCoder) {
super.init(coder: coder)
}
func add(delegate: ProxyGroupMenuHighlightDelegate) {
highlightDelegates.add(delegate)
}
func remove(_ delegate: ProxyGroupMenuHighlightDelegate) {
highlightDelegates.remove(delegate)
}
}
extension ProxyGroupMenu: NSMenuDelegate {
func menuDidClose(_ menu: NSMenu) {
highlightDelegates.allObjects.forEach { $0.highlight(item: nil) }
}
func menu(_ menu: NSMenu, willHighlight item: NSMenuItem?) {
highlightDelegates.allObjects.forEach { $0.highlight(item: item) }
}
}

View File

@ -13,21 +13,29 @@ class ProxyGroupMenuItemView: MenuItemBaseView {
let selectProxyLabel: NSTextField
let arrowLabel = NSTextField(labelWithString: "")
override var labels: [NSTextField] {
return [groupNameLabel, selectProxyLabel, arrowLabel]
override var cells: [NSCell?] {
return [groupNameLabel.cell, selectProxyLabel.cell, arrowLabel.cell]
}
override var isHighlighted: Bool {
set {}
get {
return enclosingMenuItem?.isHighlighted ?? false
}
}
init(group: ClashProxyName, targetProxy: ClashProxyName) {
groupNameLabel = VibrancyTextField(labelWithString: group)
selectProxyLabel = VibrancyTextField(labelWithString: targetProxy)
super.init(handleClick: false, autolayout: true)
super.init(autolayout: true)
// arrow
effectView.addSubview(arrowLabel)
arrowLabel.translatesAutoresizingMaskIntoConstraints = false
arrowLabel.rightAnchor.constraint(equalTo: effectView.rightAnchor, constant: -10).isActive = true
arrowLabel.centerYAnchor.constraint(equalTo: effectView.centerYAnchor).isActive = true
arrowLabel.setContentHuggingPriority(.required, for: .horizontal)
arrowLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
// group
groupNameLabel.translatesAutoresizingMaskIntoConstraints = false
effectView.addSubview(groupNameLabel)

View File

@ -49,6 +49,12 @@ class ProxyGroupSpeedTestMenuItem: NSMenuItem {
}
}
extension ProxyGroupSpeedTestMenuItem: ProxyGroupMenuHighlightDelegate {
func highlight(item: NSMenuItem?) {
(view as? ProxyGroupSpeedTestMenuItemView)?.isHighlighted = item == self
}
}
fileprivate class ProxyGroupSpeedTestMenuItemView: MenuItemBaseView {
private let label: NSTextField
@ -57,7 +63,7 @@ fileprivate class ProxyGroupSpeedTestMenuItemView: MenuItemBaseView {
label.font = type(of: self).labelFont
label.sizeToFit()
let rect = NSRect(x: 0, y: 0, width: label.bounds.width + 40, height: 20)
super.init(frame: rect, handleClick: true, autolayout: false)
super.init(frame: rect, autolayout: false)
addSubview(label)
label.frame = NSRect(x: 20, y: 0, width: label.bounds.width, height: 20)
label.textColor = NSColor.labelColor
@ -67,6 +73,10 @@ fileprivate class ProxyGroupSpeedTestMenuItemView: MenuItemBaseView {
fatalError("init(coder:) has not been implemented")
}
override var cells: [NSCell?] {
return [label.cell]
}
override var labels: [NSTextField] {
return [label]
}
@ -121,12 +131,6 @@ fileprivate class ProxyGroupSpeedTestMenuItemView: MenuItemBaseView {
}
}
}
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
label.textColor = (enclosingMenuItem?.isEnabled ?? true) ? NSColor.labelColor : NSColor.placeholderTextColor
updateBackground(label)
}
}
extension ProxyGroupSpeedTestMenuItem {

View File

@ -11,42 +11,54 @@ import Cocoa
class ProxyItemView: MenuItemBaseView {
let nameLabel: NSTextField
let delayLabel: NSTextField
let imageView: NSImageView?
deinit {
NotificationCenter.default.removeObserver(self)
}
init(name: ClashProxyName, history: ClashProxySpeedHistory) {
init(name: ClashProxyName, selected: Bool, delay: String?) {
nameLabel = VibrancyTextField(labelWithString: name)
delayLabel = VibrancyTextField(labelWithString: history.delayDisplay)
super.init(handleClick: true, autolayout: true)
delayLabel = VibrancyTextField(labelWithString: delay ?? " ")
if selected {
imageView = NSImageView(image: NSImage(named: NSImage.menuOnStateTemplateName)!)
} else {
imageView = nil
}
super.init(autolayout: true)
effectView.addSubview(nameLabel)
effectView.addSubview(delayLabel)
if let imageView = imageView {
effectView.addSubview(imageView)
}
imageView?.translatesAutoresizingMaskIntoConstraints = false
nameLabel.translatesAutoresizingMaskIntoConstraints = false
delayLabel.translatesAutoresizingMaskIntoConstraints = false
delayLabel.centerYAnchor.constraint(equalTo: effectView.centerYAnchor).isActive = true
nameLabel.centerYAnchor.constraint(equalTo: effectView.centerYAnchor).isActive = true
imageView?.centerYAnchor.constraint(equalTo: effectView.centerYAnchor).isActive = true
delayLabel.rightAnchor.constraint(equalTo: effectView.rightAnchor, constant: -15).isActive = true
nameLabel.leftAnchor.constraint(equalTo: effectView.leftAnchor, constant: 20).isActive = true
nameLabel.leftAnchor.constraint(equalTo: effectView.leftAnchor, constant: 25).isActive = true
imageView?.leftAnchor.constraint(equalTo: effectView.leftAnchor, constant: 8).isActive = true
delayLabel.leftAnchor.constraint(greaterThanOrEqualTo: nameLabel.rightAnchor, constant: 30).isActive = true
nameLabel.font = type(of: self).labelFont
delayLabel.font = NSFont.menuFont(ofSize: 13)
delayLabel.font = NSFont.menuBarFont(ofSize: 12)
}
override func didClickView() {
enclosingMenuItem?.menu?.cancelTracking()
func update(delay: String?) {
delayLabel.stringValue = delay ?? " "
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var labels: [NSTextField] {
return [nameLabel, delayLabel]
override func didClickView() {
(enclosingMenuItem as? ProxyMenuItem)?.didClick()
}
override var cells: [NSCell?] {
return [nameLabel.cell, delayLabel.cell, imageView?.cell]
}
}

View File

@ -10,23 +10,17 @@ import Cocoa
class ProxyMenuItem: NSMenuItem {
let proxyName: String
var maxProxyNameLength: CGFloat
var isSelected: Bool = false {
didSet {
state = isSelected ? .on : .off
}
}
deinit {
NotificationCenter.default.removeObserver(self)
}
init(proxy: ClashProxy, action selector: Selector?, maxProxyNameLength: CGFloat) {
self.maxProxyNameLength = maxProxyNameLength
init(proxy: ClashProxy,
action selector: Selector?,
selected: Bool) {
proxyName = proxy.name
super.init(title: proxyName, action: selector, keyEquivalent: "")
attributedTitle = getAttributedTitle(name: proxyName, delay: proxy.history.last?.delayDisplay)
view = ProxyItemView(name: proxyName, selected: selected, delay: proxy.history.last?.delayDisplay)
NotificationCenter.default.addObserver(self, selector: #selector(updateDelayNotification(note:)), name: kSpeedTestFinishForProxy, object: nil)
}
@ -34,35 +28,11 @@ class ProxyMenuItem: NSMenuItem {
fatalError("init(coder:) has not been implemented")
}
func getAttributedTitle(name: String, delay: String?) -> NSAttributedString {
let paragraph = NSMutableParagraphStyle()
paragraph.tabStops = [
NSTextTab(textAlignment: .right, location: maxProxyNameLength + 90, options: [:]),
]
let proxyName = name.replacingOccurrences(of: "\t", with: " ")
let str:String
if let delay = delay {
str = "\(proxyName)\t\(delay)"
} else {
str = proxyName.appending(" ")
func didClick() {
if let action = action {
_ = target?.perform(action, with: self)
}
let attributed = NSMutableAttributedString(
string: str,
attributes: [
NSAttributedString.Key.paragraphStyle: paragraph,
NSAttributedString.Key.font: NSFont.menuBarFont(ofSize: 14)
]
)
let hackAttr = [NSAttributedString.Key.font: NSFont.menuBarFont(ofSize: 15),]
attributed.addAttributes(hackAttr, range: NSRange(name.utf16.count..<name.utf16.count + 1))
if delay != nil {
let delayAttr = [NSAttributedString.Key.font: NSFont.menuBarFont(ofSize: 12),]
attributed.addAttributes(delayAttr, range: NSRange(name.utf16.count + 1..<str.utf16.count))
}
return attributed
menu?.cancelTracking()
}
@objc private func updateDelayNotification(note: Notification) {
@ -70,7 +40,13 @@ class ProxyMenuItem: NSMenuItem {
return
}
if let delay = note.userInfo?["delay"] as? String {
attributedTitle = getAttributedTitle(name: proxyName, delay: delay)
(view as? ProxyItemView)?.update(delay: delay)
}
}
}
extension ProxyMenuItem: ProxyGroupMenuHighlightDelegate {
func highlight(item: NSMenuItem?) {
(view as? ProxyItemView)?.isHighlighted = item == self
}
}