diff --git a/ClashX.xcodeproj/project.pbxproj b/ClashX.xcodeproj/project.pbxproj index be9959f..f69afa8 100644 --- a/ClashX.xcodeproj/project.pbxproj +++ b/ClashX.xcodeproj/project.pbxproj @@ -58,6 +58,7 @@ F9203A26236342820020D57D /* AppDelegate+..swift in Sources */ = {isa = PBXBuildFile; fileRef = F9203A25236342820020D57D /* AppDelegate+..swift */; }; F92D0B24236BC12000575E15 /* SavedProxyModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F92D0B23236BC12000575E15 /* SavedProxyModel.swift */; }; F92D0B2A236C759100575E15 /* NSTextField+Vibrancy.swift in Sources */ = {isa = PBXBuildFile; fileRef = F92D0B29236C759100575E15 /* NSTextField+Vibrancy.swift */; }; + F92D0B2C236C7C3600575E15 /* MenuItemBaseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F92D0B2B236C7C3600575E15 /* MenuItemBaseView.swift */; }; F935B2F02307C52E009E4D33 /* com.west2online.ClashX.ProxyConfigHelper in Copy Files */ = {isa = PBXBuildFile; fileRef = F9A7C0692306E874007163C7 /* com.west2online.ClashX.ProxyConfigHelper */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; F935B2F42307CD32009E4D33 /* ProxyConfigHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = F935B2F32307CD32009E4D33 /* ProxyConfigHelper.m */; }; F935B2FA23083EE6009E4D33 /* ProxySettingTool.m in Sources */ = {isa = PBXBuildFile; fileRef = F935B2F923083EE6009E4D33 /* ProxySettingTool.m */; }; @@ -166,6 +167,7 @@ F9203A25236342820020D57D /* AppDelegate+..swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+..swift"; sourceTree = ""; }; F92D0B23236BC12000575E15 /* SavedProxyModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedProxyModel.swift; sourceTree = ""; }; F92D0B29236C759100575E15 /* NSTextField+Vibrancy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSTextField+Vibrancy.swift"; sourceTree = ""; }; + F92D0B2B236C7C3600575E15 /* MenuItemBaseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuItemBaseView.swift; sourceTree = ""; }; F935B2EA2307B6BA009E4D33 /* Helper-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "Helper-Info.plist"; sourceTree = ""; }; F935B2F12307C802009E4D33 /* Helper-Launchd.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Helper-Launchd.plist"; sourceTree = ""; }; F935B2F22307CD32009E4D33 /* ProxyConfigHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ProxyConfigHelper.h; sourceTree = ""; }; @@ -281,6 +283,7 @@ 499A485922ED781100F6C675 /* RemoteConfigAddView.xib */, 49D176A8235614340093DD7B /* ProxyGroupSpeedTestMenuItem.swift */, 49D176AA23575BB20093DD7B /* ProxyGroupMenuItemView.swift */, + F92D0B2B236C7C3600575E15 /* MenuItemBaseView.swift */, ); path = Views; sourceTree = ""; @@ -638,6 +641,7 @@ F976275C23634DF8000EDEFE /* LoginServiceKit.swift in Sources */, 4966E9E6211824F300A391FB /* NSImage+extension.swift in Sources */, 49B1086A216A356D0064FFCE /* String+Extension.swift in Sources */, + F92D0B2C236C7C3600575E15 /* MenuItemBaseView.swift in Sources */, 4960A6DB2136529200B940C9 /* JSBridgeHandler.swift in Sources */, 493AEAE5221AE7230016FE98 /* ProxyMenuItem.swift in Sources */, 499A485E22ED9B7C00F6C675 /* NSTableView+Reload.swift in Sources */, diff --git a/ClashX/Views/MenuItemBaseView.swift b/ClashX/Views/MenuItemBaseView.swift new file mode 100644 index 0000000..6474465 --- /dev/null +++ b/ClashX/Views/MenuItemBaseView.swift @@ -0,0 +1,161 @@ +// +// MenuItemBaseView.swift +// ClashX +// +// Created by yicheng on 2019/11/1. +// Copyright © 2019 west2online. All rights reserved. +// + +import Cocoa +import Carbon + +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 { + if #available(macOS 10.15.1, *) { + return isMouseInsideView || isMenuOpen + } else { + return enclosingMenuItem?.isHighlighted ?? false + } + } + + let effectView: NSVisualEffectView = { + let effectView = NSVisualEffectView() + effectView.material = .popover + effectView.state = .active + effectView.isEmphasized = true + effectView.blendingMode = .behindWindow + return effectView + }() + + static let labelFont = NSFont.menuFont(ofSize: 14) + + init(frame frameRect: NSRect = .zero, handleClick:Bool, autolayout:Bool) { + self.handleClick = handleClick + self.autolayout = autolayout + super.init(frame: frameRect) + setupView() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func setNeedsDisplay() { + setNeedsDisplay(bounds) + } + + func didClickView() { + assertionFailure("Please override this method") + } + + func updateBackground(_ label: NSTextField) { + label.cell?.backgroundStyle = isHighlighted ? .emphasized : .normal + } + + // MARK: Private + private func setupView() { + translatesAutoresizingMaskIntoConstraints = false + heightAnchor.constraint(equalToConstant: 20).isActive = true + // background + addSubview(effectView) + effectView.translatesAutoresizingMaskIntoConstraints = false + effectView.leftAnchor.constraint(equalTo: leftAnchor).isActive = true + effectView.rightAnchor.constraint(equalTo: rightAnchor).isActive = true + effectView.topAnchor.constraint(equalTo: topAnchor).isActive = true + effectView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true + + } + + private func updateCarbon() { + if window != nil { + if let dispatcher = GetEventDispatcherTarget() { + let eventHandlerCallback: EventHandlerUPP = { eventHandlerCallRef, eventRef, userData in + guard let userData = userData else { return 0 } + let itemView: MenuItemBaseView = bridge(ptr: userData) + itemView.didClickView() + return 0 + } + + let eventSpecs = [EventTypeSpec(eventClass: OSType(kEventClassMouse), eventKind: UInt32(kEventMouseUp))] + + InstallEventHandler(dispatcher, eventHandlerCallback, 1, eventSpecs, bridge(obj: self), &eventHandler) + } + } else { + RemoveEventHandler(eventHandler) + } + } + + + + // MARK: Override + + override func draw(_ dirtyRect: NSRect) { + super.draw(dirtyRect) + effectView.material = isHighlighted ? .selection : .popover + } + + override func viewDidMoveToWindow() { + super.viewDidMoveToWindow() + if handleClick { + updateCarbon() + } + } + + override func viewDidMoveToSuperview() { + super.viewDidMoveToSuperview() + guard autolayout else { return } + if #available(macOS 10.15, *) {} else { + if let view = superview { + view.autoresizingMask = [.width] + } + } + } + + override func updateTrackingAreas() { + super.updateTrackingAreas() + if #available(macOS 10.15.1, *) { + 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) { + if #available(macOS 10.15.1, *) { + isMouseInsideView = true + setNeedsDisplay() + } + } + + override func mouseExited(with event: NSEvent) { + if #available(macOS 10.15.1, *) { + isMouseInsideView = false + setNeedsDisplay() + } + } +} + +extension MenuItemBaseView: NSMenuDelegate { + func menuWillOpen(_ menu: NSMenu) { + if #available(macOS 10.15.1, *) { + isMenuOpen = true + setNeedsDisplay() + } + } + + func menuDidClose(_ menu: NSMenu) { + if #available(macOS 10.15.1, *) { + isMenuOpen = false + setNeedsDisplay() + } + } +} + diff --git a/ClashX/Views/ProxyGroupMenuItemView.swift b/ClashX/Views/ProxyGroupMenuItemView.swift index 87854bc..54c2b1e 100644 --- a/ClashX/Views/ProxyGroupMenuItemView.swift +++ b/ClashX/Views/ProxyGroupMenuItemView.swift @@ -8,40 +8,16 @@ import Cocoa -class ProxyGroupMenuItemView: NSView { +class ProxyGroupMenuItemView: MenuItemBaseView { let groupNameLabel: NSTextField let selectProxyLabel: NSTextField let arrowLabel = NSTextField(labelWithString: "▶") - var isMouseInsideView = false - var isMenuOpen = false - let effectView: NSVisualEffectView = { - let effectView = NSVisualEffectView() - effectView.material = .popover - effectView.state = .active - effectView.isEmphasized = true - effectView.blendingMode = .behindWindow - return effectView - }() init(group: ClashProxyName, targetProxy: ClashProxyName) { groupNameLabel = VibrancyTextField(labelWithString: group) selectProxyLabel = VibrancyTextField(labelWithString: targetProxy) - if #available(macOS 10.15, *) { - super.init(frame: .zero) - } else { - super.init(frame: NSRect(x: 0, y: 0, width: 0, height: 20)) - } - // self - translatesAutoresizingMaskIntoConstraints = false - heightAnchor.constraint(equalToConstant: 20).isActive = true - - // background - addSubview(effectView) - effectView.translatesAutoresizingMaskIntoConstraints = false - effectView.leftAnchor.constraint(equalTo: leftAnchor).isActive = true - effectView.rightAnchor.constraint(equalTo: rightAnchor).isActive = true - effectView.topAnchor.constraint(equalTo: topAnchor).isActive = true - effectView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true + let rect = NSRect(x: 0, y: 0, width: 0, height: 20) // requeie for system before 10.15 + super.init(frame: rect, handleClick:false, autolayout: true) // arrow effectView.addSubview(arrowLabel) @@ -54,9 +30,9 @@ class ProxyGroupMenuItemView: NSView { effectView.addSubview(groupNameLabel) groupNameLabel.leftAnchor.constraint(equalTo: effectView.leftAnchor, constant: 20).isActive = true groupNameLabel.centerYAnchor.constraint(equalTo: effectView.centerYAnchor).isActive = true + // select selectProxyLabel.translatesAutoresizingMaskIntoConstraints = false - effectView.addSubview(selectProxyLabel) selectProxyLabel.rightAnchor.constraint(equalTo: effectView.rightAnchor, constant: -30).isActive = true selectProxyLabel.centerYAnchor.constraint(equalTo: effectView.centerYAnchor).isActive = true @@ -64,13 +40,11 @@ class ProxyGroupMenuItemView: NSView { // space selectProxyLabel.leftAnchor.constraint(greaterThanOrEqualTo: groupNameLabel.rightAnchor, constant: 30).isActive = true - // font - let font = NSFont.menuFont(ofSize: 14) - groupNameLabel.font = font - selectProxyLabel.font = font - + // font & color + groupNameLabel.font = type(of: self).labelFont + selectProxyLabel.font = type(of: self).labelFont groupNameLabel.textColor = NSColor.labelColor - selectProxyLabel.textColor = NSColor.secondaryLabelColor + selectProxyLabel.textColor = NSColor.tertiaryLabelColor arrowLabel.textColor = NSColor.labelColor } @@ -78,69 +52,13 @@ class ProxyGroupMenuItemView: NSView { fatalError("init(coder:) has not been implemented") } - override func updateTrackingAreas() { - super.updateTrackingAreas() - if #available(macOS 10.15.1, *) { - trackingAreas.forEach { removeTrackingArea($0) } - enclosingMenuItem?.submenu?.delegate = self - addTrackingArea(NSTrackingArea(rect: bounds, options: [.mouseEnteredAndExited, .activeAlways], owner: self, userInfo: nil)) - } - } - - override func viewDidMoveToSuperview() { - super.viewDidMoveToSuperview() - if #available(macOS 10.15, *) {} else { - if let view = superview { - view.autoresizingMask = [.width] - } - } - } - - override func mouseEntered(with event: NSEvent) { - if #available(macOS 10.15.1, *) { - isMouseInsideView = true - setNeedsDisplay(bounds) - } - } - - override func mouseExited(with event: NSEvent) { - if #available(macOS 10.15.1, *) { - isMouseInsideView = false - setNeedsDisplay(bounds) - } - } - override func draw(_ dirtyRect: NSRect) { super.draw(dirtyRect) - guard let menu = enclosingMenuItem else { return } - let isHighlighted: Bool - if #available(macOS 10.15.1, *) { - isHighlighted = isMouseInsideView || isMenuOpen - } else { - isHighlighted = menu.isHighlighted - } - let labelBgStyle: NSView.BackgroundStyle = isHighlighted ? .emphasized : .normal - groupNameLabel.cell?.backgroundStyle = labelBgStyle - selectProxyLabel.cell?.backgroundStyle = labelBgStyle - arrowLabel.cell?.backgroundStyle = labelBgStyle - effectView.material = isHighlighted ? .selection : .popover - } -} - -extension ProxyGroupMenuItemView: NSMenuDelegate { - func menuWillOpen(_ menu: NSMenu) { - if #available(macOS 10.15.1, *) { - isMenuOpen = true - setNeedsDisplay(bounds) - } - } - - func menuDidClose(_ menu: NSMenu) { - if #available(macOS 10.15.1, *) { - isMenuOpen = false - setNeedsDisplay(bounds) - } + updateBackground(groupNameLabel) + updateBackground(selectProxyLabel) + updateBackground(arrowLabel) } } + diff --git a/ClashX/Views/ProxyGroupSpeedTestMenuItem.swift b/ClashX/Views/ProxyGroupSpeedTestMenuItem.swift index daf3351..9581072 100644 --- a/ClashX/Views/ProxyGroupSpeedTestMenuItem.swift +++ b/ClashX/Views/ProxyGroupSpeedTestMenuItem.swift @@ -41,26 +41,27 @@ class ProxyGroupSpeedTestMenuItem: NSMenuItem { } } -fileprivate class ProxyGroupSpeedTestMenuItemView: NSView { +fileprivate class ProxyGroupSpeedTestMenuItemView: MenuItemBaseView { private let label: NSTextField - private let font = NSFont.menuFont(ofSize: 14) - private var isMouseInsideView = false - private var eventHandler: EventHandlerRef? init(_ title: String) { label = NSTextField(labelWithString: title) - label.font = font + label.font = Self.labelFont label.sizeToFit() - super.init(frame: NSRect(x: 0, y: 0, width: label.bounds.width + 40, height: 20)) - translatesAutoresizingMaskIntoConstraints = false - heightAnchor.constraint(equalToConstant: 20).isActive = true + let rect = NSRect(x: 0, y: 0, width: label.bounds.width + 40, height: 20) + super.init(frame: rect, handleClick: true, autolayout: false) addSubview(label) label.frame = NSRect(x: 20, y: 0, width: label.bounds.width, height: 20) + label.textColor = NSColor.labelColor } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + override func didClickView() { + startBenchmark() + } private func startBenchmark() { guard let group = (enclosingMenuItem as? ProxyGroupSpeedTestMenuItem)?.proxyGroup @@ -84,99 +85,15 @@ fileprivate class ProxyGroupSpeedTestMenuItemView: NSView { [weak self] in guard let self = self, let menu = self.enclosingMenuItem else { return } self.label.stringValue = menu.title - self.label.textColor = NSColor.labelColor menu.isEnabled = true - self.setNeedsDisplay(self.bounds) + self.setNeedsDisplay() } } - - override func updateTrackingAreas() { - super.updateTrackingAreas() - if #available(macOS 10.15.1, *) { - trackingAreas.forEach { removeTrackingArea($0) } - addTrackingArea(NSTrackingArea(rect: bounds, options: [.mouseEnteredAndExited, .activeAlways], owner: self, userInfo: nil)) - addTrackingArea(NSTrackingArea(rect: bounds, options: [.mouseMoved, .activeAlways], owner: self, userInfo: nil)) - } - } - - override func viewDidMoveToWindow() { - super.viewDidMoveToWindow() - if #available(macOS 10.15.1, *) { - setupCarbon() - } - } - -// https://gist.github.com/p0deje/da5e5cfda6be8cb87c2e7caad3a3df63 -// https://stackoverflow.com/questions/53273191/custom-carbon-key-event-handler-fails-after-mouse-events - @available(macOS 10.15.1, *) - private func setupCarbon() { - if window != nil { - if let dispatcher = GetEventDispatcherTarget() { - let eventHandlerCallback: EventHandlerUPP = { eventHandlerCallRef, eventRef, userData in - guard let userData = userData else { return 0 } - let itemView: ProxyGroupSpeedTestMenuItemView = bridge(ptr: userData) - itemView.startBenchmark() - return 0 - } - - let eventSpecs = [EventTypeSpec(eventClass: OSType(kEventClassMouse), eventKind: UInt32(kEventMouseUp))] - - InstallEventHandler(dispatcher, eventHandlerCallback, 1, eventSpecs, bridge(obj: self), &eventHandler) - } - } else { - RemoveEventHandler(eventHandler) - } - } - - override func mouseEntered(with event: NSEvent) { - if #available(macOS 10.15.1, *) { - isMouseInsideView = true - setNeedsDisplay(bounds) - } - } - - override func mouseExited(with event: NSEvent) { - if #available(macOS 10.15.1, *) { - isMouseInsideView = false - setNeedsDisplay(bounds) - } - } - - override func hitTest(_ point: NSPoint) -> NSView? { - if bounds.contains(point) { - return label - } - return super.hitTest(point) - } - - override func mouseUp(with event: NSEvent) { - if #available(macOS 10.15.1, *) {} else { - startBenchmark() - } - } - + override func draw(_ dirtyRect: NSRect) { super.draw(dirtyRect) - guard let menu = enclosingMenuItem else { return } - - let isHighlighted: Bool - if #available(macOS 10.15.1, *) { - isHighlighted = isMouseInsideView - } else { - isHighlighted = menu.isHighlighted - } - if isHighlighted && menu.isEnabled { - NSColor.selectedMenuItemColor.setFill() - label.textColor = NSColor.white - } else { - NSColor.clear.setFill() - if enclosingMenuItem?.isEnabled ?? true { - label.textColor = NSColor.labelColor - } else { - label.textColor = NSColor.secondaryLabelColor - } - } - dirtyRect.fill() + label.textColor = (enclosingMenuItem?.isEnabled ?? true) ? NSColor.labelColor : NSColor.placeholderTextColor + updateBackground(label) } }