diff --git a/ClashX.xcodeproj/project.pbxproj b/ClashX.xcodeproj/project.pbxproj index 9f4018a..5f2aa74 100644 --- a/ClashX.xcodeproj/project.pbxproj +++ b/ClashX.xcodeproj/project.pbxproj @@ -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 = ""; }; 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 = ""; }; 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 = ""; }; + F910AA23240134AF00116E95 /* ProxyGroupMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxyGroupMenu.swift; sourceTree = ""; }; F915A4612366ADEF004840BE /* ClashConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClashConnection.swift; sourceTree = ""; }; F9203A25236342820020D57D /* AppDelegate+..swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+..swift"; sourceTree = ""; }; F92D0B23236BC12000575E15 /* SavedProxyModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedProxyModel.swift; sourceTree = ""; }; @@ -301,6 +303,7 @@ 499A485922ED781100F6C675 /* RemoteConfigAddView.xib */, 495340B220DE68C300B0D3FF /* StatusItemView.swift */, 495340AF20DE5F7200B0D3FF /* StatusItemView.xib */, + F910AA23240134AF00116E95 /* ProxyGroupMenu.swift */, ); path = Views; sourceTree = ""; @@ -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 */, diff --git a/ClashX/General/Managers/MenuItemFactory.swift b/ClashX/General/Managers/MenuItemFactory.swift index 0385ed0..e60f269 100644 --- a/ClashX/General/Managers/MenuItemFactory.swift +++ b/ClashX/General/Managers/MenuItemFactory.swift @@ -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) diff --git a/ClashX/Models/ClashProxy.swift b/ClashX/Models/ClashProxy.swift index 0f1900e..8915c55 100644 --- a/ClashX/Models/ClashProxy.swift +++ b/ClashX/Models/ClashProxy.swift @@ -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 { diff --git a/ClashX/Views/MenuItemBaseView.swift b/ClashX/Views/MenuItemBaseView.swift index 5a521fd..ea156da 100644 --- a/ClashX/Views/MenuItemBaseView.swift +++ b/ClashX/Views/MenuItemBaseView.swift @@ -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() - } } diff --git a/ClashX/Views/ProxyGroupMenu.swift b/ClashX/Views/ProxyGroupMenu.swift new file mode 100644 index 0000000..597b7fd --- /dev/null +++ b/ClashX/Views/ProxyGroupMenu.swift @@ -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.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) } + } +} diff --git a/ClashX/Views/ProxyGroupMenuItemView.swift b/ClashX/Views/ProxyGroupMenuItemView.swift index ff4c1f4..a78230a 100644 --- a/ClashX/Views/ProxyGroupMenuItemView.swift +++ b/ClashX/Views/ProxyGroupMenuItemView.swift @@ -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) diff --git a/ClashX/Views/ProxyGroupSpeedTestMenuItem.swift b/ClashX/Views/ProxyGroupSpeedTestMenuItem.swift index 99f9df9..a0d3259 100644 --- a/ClashX/Views/ProxyGroupSpeedTestMenuItem.swift +++ b/ClashX/Views/ProxyGroupSpeedTestMenuItem.swift @@ -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 { diff --git a/ClashX/Views/ProxyItemView.swift b/ClashX/Views/ProxyItemView.swift index b4ce4c6..6350d32 100644 --- a/ClashX/Views/ProxyItemView.swift +++ b/ClashX/Views/ProxyItemView.swift @@ -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] } } diff --git a/ClashX/Views/ProxyMenuItem.swift b/ClashX/Views/ProxyMenuItem.swift index 78b8635..a6811e5 100644 --- a/ClashX/Views/ProxyMenuItem.swift +++ b/ClashX/Views/ProxyMenuItem.swift @@ -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..