Fix menu bar status item
This commit is contained in:
@@ -4,7 +4,7 @@ import Foundation
|
|||||||
public enum MenuBarExtraSpeedMode: Int, CaseIterable {
|
public enum MenuBarExtraSpeedMode: Int, CaseIterable {
|
||||||
case disabled = 0
|
case disabled = 0
|
||||||
case enabled = 1
|
case enabled = 1
|
||||||
case separate = 2
|
case unified = 2
|
||||||
|
|
||||||
public var name: String {
|
public var name: String {
|
||||||
switch self {
|
switch self {
|
||||||
@@ -12,8 +12,8 @@ import Foundation
|
|||||||
return NSLocalizedString("Disabled", comment: "")
|
return NSLocalizedString("Disabled", comment: "")
|
||||||
case .enabled:
|
case .enabled:
|
||||||
return NSLocalizedString("Enabled", comment: "")
|
return NSLocalizedString("Enabled", comment: "")
|
||||||
case .separate:
|
case .unified:
|
||||||
return NSLocalizedString("Detailed", comment: "")
|
return NSLocalizedString("Unified", comment: "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -66,7 +66,7 @@ public enum SharedPreferences {
|
|||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
public static let showMenuBarExtra = Preference<Bool>("show_menu_bar_extra", defaultValue: true)
|
public static let showMenuBarExtra = Preference<Bool>("show_menu_bar_extra", defaultValue: true)
|
||||||
public static let menuBarExtraInBackground = Preference<Bool>("menu_bar_extra_in_background", defaultValue: false)
|
public static let menuBarExtraInBackground = Preference<Bool>("menu_bar_extra_in_background", defaultValue: false)
|
||||||
public static let menuBarExtraSpeedMode = Preference<Int>("menu_bar_extra_speed_mode", defaultValue: MenuBarExtraSpeedMode.enabled.rawValue)
|
public static let menuBarExtraSpeedMode = Preference<Int>("menu_bar_extra_speed_mode_1", defaultValue: MenuBarExtraSpeedMode.enabled.rawValue)
|
||||||
public static let startedByUser = Preference<Bool>("started_by_user", defaultValue: false)
|
public static let startedByUser = Preference<Bool>("started_by_user", defaultValue: false)
|
||||||
|
|
||||||
public static func resetMacOS() async {
|
public static func resetMacOS() async {
|
||||||
|
|||||||
@@ -741,6 +741,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Detailed" : {
|
"Detailed" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"zh-Hans" : {
|
"zh-Hans" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -2816,6 +2817,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Unified" : {
|
||||||
|
"localizations" : {
|
||||||
|
"zh-Hans" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "统一"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"Uninstall" : {
|
"Uninstall" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"zh-Hans" : {
|
"zh-Hans" : {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import Library
|
|||||||
@MainActor
|
@MainActor
|
||||||
public class StatusBarController: NSObject, NSMenuDelegate {
|
public class StatusBarController: NSObject, NSMenuDelegate {
|
||||||
private var statusItem: NSStatusItem?
|
private var statusItem: NSStatusItem?
|
||||||
|
private var statusItemView: StatusBarItemView?
|
||||||
private let environments: ExtensionEnvironments
|
private let environments: ExtensionEnvironments
|
||||||
private var commandClient: CommandClient?
|
private var commandClient: CommandClient?
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
@@ -62,9 +63,24 @@ public class StatusBarController: NSObject, NSMenuDelegate {
|
|||||||
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
|
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
|
||||||
|
|
||||||
let button = statusItem!.button!
|
let button = statusItem!.button!
|
||||||
button.image = NSImage(named: "MenuIcon")
|
button.image = nil
|
||||||
button.image?.isTemplate = true
|
button.title = ""
|
||||||
button.imagePosition = .imageTrailing
|
let itemView = StatusBarItemView()
|
||||||
|
itemView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
button.addSubview(itemView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
itemView.leadingAnchor.constraint(equalTo: button.leadingAnchor),
|
||||||
|
itemView.trailingAnchor.constraint(equalTo: button.trailingAnchor),
|
||||||
|
itemView.topAnchor.constraint(equalTo: button.topAnchor),
|
||||||
|
itemView.bottomAnchor.constraint(equalTo: button.bottomAnchor),
|
||||||
|
])
|
||||||
|
statusItemView = itemView
|
||||||
|
itemView.attach(to: button)
|
||||||
|
if let image = NSImage(named: "MenuIcon") {
|
||||||
|
image.isTemplate = true
|
||||||
|
itemView.setIcon(image)
|
||||||
|
}
|
||||||
|
updateStatusItemLength()
|
||||||
|
|
||||||
menu = NSMenu()
|
menu = NSMenu()
|
||||||
menu!.delegate = self
|
menu!.delegate = self
|
||||||
@@ -79,6 +95,7 @@ public class StatusBarController: NSObject, NSMenuDelegate {
|
|||||||
NSStatusBar.system.removeStatusItem(statusItem)
|
NSStatusBar.system.removeStatusItem(statusItem)
|
||||||
self.statusItem = nil
|
self.statusItem = nil
|
||||||
}
|
}
|
||||||
|
statusItemView = nil
|
||||||
menu = nil
|
menu = nil
|
||||||
headerView = nil
|
headerView = nil
|
||||||
urlTestAllView = nil
|
urlTestAllView = nil
|
||||||
@@ -503,14 +520,20 @@ public class StatusBarController: NSObject, NSMenuDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func updateSpeedDisplay(status: LibboxStatusMessage?) {
|
private func updateSpeedDisplay(status: LibboxStatusMessage?) {
|
||||||
guard let button = statusItem?.button else { return }
|
guard statusItem != nil else { return }
|
||||||
if speedMode == .disabled || status == nil || !status!.trafficAvailable {
|
if speedMode == .disabled || status == nil || !status!.trafficAvailable {
|
||||||
button.title = ""
|
statusItemView?.setTitle("")
|
||||||
} else if speedMode == .separate {
|
} else if speedMode == .enabled {
|
||||||
button.title = "↑ \(LibboxFormatBytes(status!.uplink))/s ↓ \(LibboxFormatBytes(status!.downlink))/s "
|
statusItemView?.setTitle("\(LibboxFormatBytes(status!.uplink))/s\n\(LibboxFormatBytes(status!.downlink))/s")
|
||||||
} else {
|
} else {
|
||||||
button.title = "\(LibboxFormatBytes(status!.uplink + status!.downlink))/s "
|
statusItemView?.setTitle("\(LibboxFormatBytes(status!.uplink + status!.downlink))/s")
|
||||||
}
|
}
|
||||||
|
updateStatusItemLength()
|
||||||
|
}
|
||||||
|
private func updateStatusItemLength() {
|
||||||
|
guard let statusItem, let statusItemView else { return }
|
||||||
|
let width = statusItemView.fittingWidth()
|
||||||
|
statusItem.length = max(width, NSStatusBar.system.thickness)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func showAlert(error: Error) {
|
private func showAlert(error: Error) {
|
||||||
@@ -603,6 +626,175 @@ public class StatusBarController: NSObject, NSMenuDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - StatusBarItemView
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
private class StatusBarItemView: NSView {
|
||||||
|
private enum Layout {
|
||||||
|
static let horizontalPadding: CGFloat = 8
|
||||||
|
static let verticalPadding: CGFloat = 2
|
||||||
|
static let spacing: CGFloat = 6
|
||||||
|
static let lineHeight: CGFloat = 9
|
||||||
|
static let fontSize: CGFloat = 8.75
|
||||||
|
static let textWidth: CGFloat = 42
|
||||||
|
}
|
||||||
|
|
||||||
|
private let imageView: NSImageView
|
||||||
|
private let textField: NSTextField
|
||||||
|
private let stackView: NSStackView
|
||||||
|
private let textAttributes: [NSAttributedString.Key: Any]
|
||||||
|
private var currentTitle = ""
|
||||||
|
private var isHighlighted = false
|
||||||
|
private weak var statusButton: NSStatusBarButton?
|
||||||
|
private var leadingConstraint: NSLayoutConstraint?
|
||||||
|
private var trailingConstraint: NSLayoutConstraint?
|
||||||
|
private var topConstraint: NSLayoutConstraint?
|
||||||
|
private var bottomConstraint: NSLayoutConstraint?
|
||||||
|
private var textWidthConstraint: NSLayoutConstraint?
|
||||||
|
private var currentHorizontalPadding = Layout.horizontalPadding
|
||||||
|
|
||||||
|
override init(frame frameRect: NSRect) {
|
||||||
|
imageView = NSImageView()
|
||||||
|
textField = NSTextField(labelWithString: "")
|
||||||
|
stackView = NSStackView()
|
||||||
|
|
||||||
|
let paragraphStyle = NSMutableParagraphStyle()
|
||||||
|
paragraphStyle.maximumLineHeight = Layout.lineHeight
|
||||||
|
paragraphStyle.minimumLineHeight = Layout.lineHeight
|
||||||
|
paragraphStyle.alignment = .right
|
||||||
|
paragraphStyle.lineBreakMode = .byClipping
|
||||||
|
textAttributes = [
|
||||||
|
.paragraphStyle: paragraphStyle,
|
||||||
|
.font: NSFont.systemFont(ofSize: Layout.fontSize),
|
||||||
|
.foregroundColor: NSColor.labelColor,
|
||||||
|
]
|
||||||
|
|
||||||
|
super.init(frame: frameRect)
|
||||||
|
setupView()
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder _: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupView() {
|
||||||
|
imageView.imageScaling = .scaleProportionallyDown
|
||||||
|
imageView.isHidden = true
|
||||||
|
imageView.setContentHuggingPriority(.required, for: .horizontal)
|
||||||
|
imageView.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||||
|
|
||||||
|
textField.isEditable = false
|
||||||
|
textField.isBezeled = false
|
||||||
|
textField.drawsBackground = false
|
||||||
|
textField.isHidden = true
|
||||||
|
textField.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
textField.lineBreakMode = .byClipping
|
||||||
|
textField.maximumNumberOfLines = 2
|
||||||
|
textField.usesSingleLineMode = false
|
||||||
|
textField.alignment = .right
|
||||||
|
textField.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
||||||
|
|
||||||
|
stackView.orientation = .horizontal
|
||||||
|
stackView.distribution = .equalSpacing
|
||||||
|
stackView.alignment = .centerY
|
||||||
|
stackView.spacing = Layout.spacing
|
||||||
|
stackView.detachesHiddenViews = true
|
||||||
|
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
stackView.addArrangedSubview(imageView)
|
||||||
|
stackView.addArrangedSubview(textField)
|
||||||
|
addSubview(stackView)
|
||||||
|
|
||||||
|
leadingConstraint = stackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: Layout.horizontalPadding)
|
||||||
|
trailingConstraint = stackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -Layout.horizontalPadding)
|
||||||
|
topConstraint = stackView.topAnchor.constraint(equalTo: topAnchor, constant: Layout.verticalPadding)
|
||||||
|
bottomConstraint = stackView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -Layout.verticalPadding)
|
||||||
|
textWidthConstraint = textField.widthAnchor.constraint(equalToConstant: Layout.textWidth)
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
leadingConstraint!,
|
||||||
|
trailingConstraint!,
|
||||||
|
topConstraint!,
|
||||||
|
bottomConstraint!,
|
||||||
|
textWidthConstraint!,
|
||||||
|
])
|
||||||
|
|
||||||
|
updateLayout(hasTitle: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setIcon(_ image: NSImage) {
|
||||||
|
imageView.image = image
|
||||||
|
imageView.isHidden = false
|
||||||
|
updateTintColor()
|
||||||
|
}
|
||||||
|
|
||||||
|
func setTitle(_ title: String) {
|
||||||
|
currentTitle = title
|
||||||
|
updateTitle()
|
||||||
|
}
|
||||||
|
|
||||||
|
func attach(to button: NSStatusBarButton) {
|
||||||
|
statusButton = button
|
||||||
|
updateHighlight(button.isHighlighted)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fittingWidth() -> CGFloat {
|
||||||
|
layoutSubtreeIfNeeded()
|
||||||
|
let width = stackView.fittingSize.width + currentHorizontalPadding * 2
|
||||||
|
return ceil(width)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateTitle() {
|
||||||
|
let hasTitle = !currentTitle.isEmpty
|
||||||
|
updateLayout(hasTitle: hasTitle)
|
||||||
|
if !hasTitle {
|
||||||
|
textField.stringValue = ""
|
||||||
|
textField.isHidden = true
|
||||||
|
} else {
|
||||||
|
var attributes = textAttributes
|
||||||
|
attributes[.foregroundColor] = isHighlighted
|
||||||
|
? NSColor.selectedMenuItemTextColor
|
||||||
|
: NSColor.labelColor
|
||||||
|
textField.attributedStringValue = NSAttributedString(string: currentTitle, attributes: attributes)
|
||||||
|
textField.isHidden = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateLayout(hasTitle: Bool) {
|
||||||
|
let horizontalPadding = hasTitle ? Layout.horizontalPadding : 0
|
||||||
|
let verticalPadding = hasTitle ? Layout.verticalPadding : 0
|
||||||
|
currentHorizontalPadding = horizontalPadding
|
||||||
|
leadingConstraint?.constant = horizontalPadding
|
||||||
|
trailingConstraint?.constant = -horizontalPadding
|
||||||
|
topConstraint?.constant = verticalPadding
|
||||||
|
bottomConstraint?.constant = -verticalPadding
|
||||||
|
textWidthConstraint?.constant = hasTitle ? Layout.textWidth : 0
|
||||||
|
stackView.spacing = hasTitle ? Layout.spacing : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateTintColor() {
|
||||||
|
imageView.contentTintColor = isHighlighted
|
||||||
|
? NSColor.selectedMenuItemTextColor
|
||||||
|
: NSColor.labelColor
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateHighlight(_ highlighted: Bool) {
|
||||||
|
guard isHighlighted != highlighted else { return }
|
||||||
|
isHighlighted = highlighted
|
||||||
|
updateTitle()
|
||||||
|
updateTintColor()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillDraw() {
|
||||||
|
super.viewWillDraw()
|
||||||
|
updateHighlight(statusButton?.isHighlighted == true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func hitTest(_ point: NSPoint) -> NSView? {
|
||||||
|
nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - StatusBarURLTestView
|
// MARK: - StatusBarURLTestView
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
|
|||||||
Reference in New Issue
Block a user