diff --git a/.swiftlint.yml b/.swiftlint.yml index 31ed88e..718cb44 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -4,6 +4,9 @@ excluded: identifier_name: allowed_symbols: "_" + +large_tuple: + warning: 3 disabled_rules: - function_body_length diff --git a/HeliPort.xcodeproj/project.pbxproj b/HeliPort.xcodeproj/project.pbxproj index 7d53897..5465726 100644 --- a/HeliPort.xcodeproj/project.pbxproj +++ b/HeliPort.xcodeproj/project.pbxproj @@ -13,10 +13,19 @@ 138D3CC824CE635800793AC1 /* Commands.swift in Sources */ = {isa = PBXBuildFile; fileRef = 138D3CC724CE635800793AC1 /* Commands.swift */; }; 138D3CCA24CE663B00793AC1 /* BugReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 138D3CC924CE663B00793AC1 /* BugReporter.swift */; }; 13AB3CA824DE47D10093D283 /* WiFiConfigWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13AB3CA724DE47D10093D283 /* WiFiConfigWindow.swift */; }; - 13AF73B624B25E170015867C /* StatusMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCA4DFAC243A307B002A862A /* StatusMenu.swift */; }; 13C20DFA24D8B6D100B1E713 /* PrefsGeneralView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13C20DF924D8B6D100B1E713 /* PrefsGeneralView.swift */; }; 5013B8FB2C5C8C8D002C5006 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 5013B8FA2C5C8C8D002C5006 /* Localizable.xcstrings */; }; 5013B8FD2C5DF244002C5006 /* NSApp+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5013B8FC2C5DF244002C5006 /* NSApp+Extensions.swift */; }; + 5013B9002C5E1E00002C5006 /* StatusMenuBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5013B8FF2C5E1E00002C5006 /* StatusMenuBase.swift */; }; + 5013B9022C5E1E36002C5006 /* StatusMenuLegacy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5013B9012C5E1E36002C5006 /* StatusMenuLegacy.swift */; }; + 5013B9042C5E1EA9002C5006 /* StatusMenuModern.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5013B9032C5E1EA9002C5006 /* StatusMenuModern.swift */; }; + 5013B9072C5E1EF2002C5006 /* KeyValueMenuItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5013B9062C5E1EF2002C5006 /* KeyValueMenuItemView.swift */; }; + 5013B9092C5E1FF4002C5006 /* SectionMenuItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5013B9082C5E1FF4002C5006 /* SectionMenuItemView.swift */; }; + 5013B90B2C5E2029002C5006 /* SelectableMenuItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5013B90A2C5E2029002C5006 /* SelectableMenuItemView.swift */; }; + 5013B90E2C5E21F5002C5006 /* StateSwitchMenuItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5013B90D2C5E21F5002C5006 /* StateSwitchMenuItemView.swift */; }; + 5013B9102C5E2265002C5006 /* WiFiMenuItemViewLegacy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5013B90F2C5E2265002C5006 /* WiFiMenuItemViewLegacy.swift */; }; + 5013B9122C5E22AB002C5006 /* WiFiMenuItemViewModern.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5013B9112C5E22AB002C5006 /* WiFiMenuItemViewModern.swift */; }; + 5013B9142C5E22F2002C5006 /* NSImage+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5013B9132C5E22F2002C5006 /* NSImage+Extensions.swift */; }; 505EC11D2C5BD89400F4E4EA /* StatusBarIconManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 505EC11C2C5BD89400F4E4EA /* StatusBarIconManager.swift */; }; 505EC11F2C5BD8ED00F4E4EA /* StatusBarIconLegacy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 505EC11E2C5BD8ED00F4E4EA /* StatusBarIconLegacy.swift */; }; 505EC1212C5BD95700F4E4EA /* StatusBarIconModern.swift in Sources */ = {isa = PBXBuildFile; fileRef = 505EC1202C5BD95700F4E4EA /* StatusBarIconModern.swift */; }; @@ -70,6 +79,17 @@ 13C20DF924D8B6D100B1E713 /* PrefsGeneralView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefsGeneralView.swift; sourceTree = ""; }; 5013B8FA2C5C8C8D002C5006 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; 5013B8FC2C5DF244002C5006 /* NSApp+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSApp+Extensions.swift"; sourceTree = ""; }; + 5013B8FF2C5E1E00002C5006 /* StatusMenuBase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusMenuBase.swift; sourceTree = ""; }; + 5013B9012C5E1E36002C5006 /* StatusMenuLegacy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusMenuLegacy.swift; sourceTree = ""; }; + 5013B9032C5E1EA9002C5006 /* StatusMenuModern.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusMenuModern.swift; sourceTree = ""; }; + 5013B9062C5E1EF2002C5006 /* KeyValueMenuItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyValueMenuItemView.swift; sourceTree = ""; }; + 5013B9082C5E1FF4002C5006 /* SectionMenuItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SectionMenuItemView.swift; sourceTree = ""; }; + 5013B90A2C5E2029002C5006 /* SelectableMenuItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableMenuItemView.swift; sourceTree = ""; }; + 5013B90C2C5E20E1002C5006 /* Bridge.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Bridge.h; sourceTree = ""; }; + 5013B90D2C5E21F5002C5006 /* StateSwitchMenuItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateSwitchMenuItemView.swift; sourceTree = ""; }; + 5013B90F2C5E2265002C5006 /* WiFiMenuItemViewLegacy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WiFiMenuItemViewLegacy.swift; sourceTree = ""; }; + 5013B9112C5E22AB002C5006 /* WiFiMenuItemViewModern.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WiFiMenuItemViewModern.swift; sourceTree = ""; }; + 5013B9132C5E22F2002C5006 /* NSImage+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSImage+Extensions.swift"; sourceTree = ""; }; 505EC11C2C5BD89400F4E4EA /* StatusBarIconManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusBarIconManager.swift; sourceTree = ""; }; 505EC11E2C5BD8ED00F4E4EA /* StatusBarIconLegacy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusBarIconLegacy.swift; sourceTree = ""; }; 505EC1202C5BD95700F4E4EA /* StatusBarIconModern.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusBarIconModern.swift; sourceTree = ""; }; @@ -83,7 +103,6 @@ 7557CDF324935260000F0F71 /* Credits.rtf */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; path = Credits.rtf; sourceTree = ""; }; 7562BBE324826102008A9BD2 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 75FDF38A2481D22000B2A601 /* NetworkManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkManager.swift; sourceTree = ""; }; - BCA4DFAC243A307B002A862A /* StatusMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusMenu.swift; sourceTree = ""; }; BCCB2AA3243708090005BB82 /* WiFiMenuItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WiFiMenuItemView.swift; sourceTree = ""; wrapsLines = 0; }; BCFA32E72424D2BE00E23603 /* HeliPort.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = HeliPort.app; sourceTree = BUILT_PRODUCTS_DIR; }; BCFA32EA2424D2BE00E23603 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -136,6 +155,31 @@ path = Preferences; sourceTree = ""; }; + 5013B8FE2C5E1B56002C5006 /* StatusMenu */ = { + isa = PBXGroup; + children = ( + 5013B9052C5E1ED5002C5006 /* MenuItemView */, + 5013B8FF2C5E1E00002C5006 /* StatusMenuBase.swift */, + 5013B9012C5E1E36002C5006 /* StatusMenuLegacy.swift */, + 5013B9032C5E1EA9002C5006 /* StatusMenuModern.swift */, + ); + path = StatusMenu; + sourceTree = ""; + }; + 5013B9052C5E1ED5002C5006 /* MenuItemView */ = { + isa = PBXGroup; + children = ( + 5013B9062C5E1EF2002C5006 /* KeyValueMenuItemView.swift */, + 5013B9082C5E1FF4002C5006 /* SectionMenuItemView.swift */, + 5013B90A2C5E2029002C5006 /* SelectableMenuItemView.swift */, + 5013B90D2C5E21F5002C5006 /* StateSwitchMenuItemView.swift */, + BCCB2AA3243708090005BB82 /* WiFiMenuItemView.swift */, + 5013B90F2C5E2265002C5006 /* WiFiMenuItemViewLegacy.swift */, + 5013B9112C5E22AB002C5006 /* WiFiMenuItemViewModern.swift */, + ); + path = MenuItemView; + sourceTree = ""; + }; 505EC11B2C5BD85300F4E4EA /* StatusBarIcon */ = { isa = PBXGroup; children = ( @@ -159,13 +203,12 @@ BCF712FA243C9EB100BE3C05 /* Appearance */ = { isa = PBXGroup; children = ( + 5013B8FE2C5E1B56002C5006 /* StatusMenu */, 505EC11B2C5BD85300F4E4EA /* StatusBarIcon */, + 13C20DFD24D92E3800B1E713 /* Preferences */, + 13AB3CA724DE47D10093D283 /* WiFiConfigWindow.swift */, 5013B8FA2C5C8C8D002C5006 /* Localizable.xcstrings */, BCFA32EE2424D2BF00E23603 /* MainMenu.xib */, - BCA4DFAC243A307B002A862A /* StatusMenu.swift */, - BCCB2AA3243708090005BB82 /* WiFiMenuItemView.swift */, - 13AB3CA724DE47D10093D283 /* WiFiConfigWindow.swift */, - 13C20DFD24D92E3800B1E713 /* Preferences */, ); path = Appearance; sourceTree = ""; @@ -193,14 +236,15 @@ BCFA32E92424D2BE00E23603 /* HeliPort */ = { isa = PBXGroup; children = ( - BCFA32EA2424D2BE00E23603 /* AppDelegate.swift */, - BCFA32EC2424D2BF00E23603 /* Assets.xcassets */, - BCFA32F12424D2BF00E23603 /* Info.plist */, BCF712FA243C9EB100BE3C05 /* Appearance */, + BCFA32EA2424D2BE00E23603 /* AppDelegate.swift */, F379276E24A0A4A50087FF2B /* CredentialsManager.swift */, 50F4959724BDD26D00AE4C08 /* LoginItemManager.swift */, 75FDF38A2481D22000B2A601 /* NetworkManager.swift */, 50E7700E2C5A7CD600DB1160 /* UpdateManager.swift */, + 5013B90C2C5E20E1002C5006 /* Bridge.h */, + BCFA32EC2424D2BF00E23603 /* Assets.xcassets */, + BCFA32F12424D2BF00E23603 /* Info.plist */, BCFA32F22424D2BF00E23603 /* HeliPort.entitlements */, 7557CDF324935260000F0F71 /* Credits.rtf */, F34B2B8B24AA4C08009AB1BB /* Supporting files */, @@ -224,6 +268,7 @@ F33A1F3E24C8347F008ED2BD /* NSLocalizedString+Extensions.swift */, F34B2B8C24AA4C1E009AB1BB /* NSMenuItem+Extensions.swift */, 1317990A256986E5006957D8 /* String+Extensions.swift */, + 5013B9132C5E22F2002C5006 /* NSImage+Extensions.swift */, ); path = "Supporting files"; sourceTree = ""; @@ -402,21 +447,28 @@ 75FDF38C2481D25A00B2A601 /* Api.c in Sources */, F379276F24A0A4A50087FF2B /* CredentialsManager.swift in Sources */, F336D63C24B497B6004C98C4 /* NetworkManager+Data.swift in Sources */, + 5013B9102C5E2265002C5006 /* WiFiMenuItemViewLegacy.swift in Sources */, F33A1F3F24C8347F008ED2BD /* NSLocalizedString+Extensions.swift in Sources */, F379277124A0A52E0087FF2B /* Log.swift in Sources */, + 5013B9122C5E22AB002C5006 /* WiFiMenuItemViewModern.swift in Sources */, 505EC11F2C5BD8ED00F4E4EA /* StatusBarIconLegacy.swift in Sources */, F336D63E24B4986C004C98C4 /* itl_phy_mode+Description.swift in Sources */, F336D64024B7B7D8004C98C4 /* itl80211_security+Description.swift in Sources */, + 5013B9092C5E1FF4002C5006 /* SectionMenuItemView.swift in Sources */, F3915F0724AB1A1B00E6614D /* itl_80211_state+Extensions.swift in Sources */, 5088F70826BEA46F009E3A15 /* KextInfo.swift in Sources */, F33A1F3B24C83016008ED2BD /* Alert.swift in Sources */, + 5013B90E2C5E21F5002C5006 /* StateSwitchMenuItemView.swift in Sources */, + 5013B9002C5E1E00002C5006 /* StatusMenuBase.swift in Sources */, 505EC1212C5BD95700F4E4EA /* StatusBarIconModern.swift in Sources */, BCFA32EB2424D2BE00E23603 /* AppDelegate.swift in Sources */, + 5013B9072C5E1EF2002C5006 /* KeyValueMenuItemView.swift in Sources */, BCCB2AA4243708090005BB82 /* WiFiMenuItemView.swift in Sources */, 1380C36124D54BFD00A448CF /* PrefsSavedNetworksView.swift in Sources */, 138D3CCA24CE663B00793AC1 /* BugReporter.swift in Sources */, + 5013B9042C5E1EA9002C5006 /* StatusMenuModern.swift in Sources */, 75FDF38B2481D22000B2A601 /* NetworkManager.swift in Sources */, - 13AF73B624B25E170015867C /* StatusMenu.swift in Sources */, + 5013B9022C5E1E36002C5006 /* StatusMenuLegacy.swift in Sources */, 138D3CC824CE635800793AC1 /* Commands.swift in Sources */, 13C20DFA24D8B6D100B1E713 /* PrefsGeneralView.swift in Sources */, 13AB3CA824DE47D10093D283 /* WiFiConfigWindow.swift in Sources */, @@ -424,8 +476,10 @@ 1380C36524D5580200A448CF /* PrefsWindow.swift in Sources */, 50F4959824BDD26D00AE4C08 /* LoginItemManager.swift in Sources */, F34B2B8D24AA4C1E009AB1BB /* NSMenuItem+Extensions.swift in Sources */, + 5013B90B2C5E2029002C5006 /* SelectableMenuItemView.swift in Sources */, 50E7700F2C5A7CE100DB1160 /* UpdateManager.swift in Sources */, 5013B8FD2C5DF244002C5006 /* NSApp+Extensions.swift in Sources */, + 5013B9142C5E22F2002C5006 /* NSImage+Extensions.swift in Sources */, 1317990B256986E5006957D8 /* String+Extensions.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -623,7 +677,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.OpenIntelWireless.HeliPort; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_OBJC_BRIDGING_HEADER = ClientKit/Api.h; + SWIFT_OBJC_BRIDGING_HEADER = HeliPort/Bridge.h; SWIFT_VERSION = 5.0; }; name = Debug; @@ -645,7 +699,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.OpenIntelWireless.HeliPort; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_OBJC_BRIDGING_HEADER = ClientKit/Api.h; + SWIFT_OBJC_BRIDGING_HEADER = HeliPort/Bridge.h; SWIFT_VERSION = 5.0; }; name = Release; diff --git a/HeliPort/AppDelegate.swift b/HeliPort/AppDelegate.swift index 9ddbb60..731a362 100644 --- a/HeliPort/AppDelegate.swift +++ b/HeliPort/AppDelegate.swift @@ -41,7 +41,11 @@ class AppDelegate: NSObject, NSApplicationDelegate { }() _ = StatusBarIcon.shared(statusBar: statusBar, icons: iconProvider) - statusBar.menu = StatusMenu() + if #available(macOS 11, *), !legacyUIEnabled { + statusBar.menu = StatusMenuModern() + } else { + statusBar.menu = StatusMenuLegacy() + } } private var drv_info = ioctl_driver_info() diff --git a/HeliPort/Appearance/Localizable.xcstrings b/HeliPort/Appearance/Localizable.xcstrings index b9b1aee..bf37201 100644 --- a/HeliPort/Appearance/Localizable.xcstrings +++ b/HeliPort/Appearance/Localizable.xcstrings @@ -4688,6 +4688,28 @@ } } }, + "Known Network" : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "已知网络" + } + } + } + }, + "Known Networks" : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "已知网络" + } + } + } + }, "Later" : { "extractionState" : "manual", "localizations" : { @@ -5838,6 +5860,28 @@ } } }, + "Other Networks" : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "其他网络" + } + } + } + }, + "Other..." : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "其他..." + } + } + } + }, "Password" : { "extractionState" : "manual", "localizations" : { @@ -8330,6 +8374,119 @@ } } }, + "Wi-Fi" : { + "extractionState" : "manual", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wi-Fi" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wi-Fi" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wi-Fi" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wi-Fi" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wi-Fi" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wi-Fi" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wi-Fi" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wi-Fi" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wi-Fi" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wi-Fi" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wi-Fi" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wi-Fi" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wi-Fi" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wi-Fi" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wi-Fi" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wi-Fi" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wi-Fi" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wi-Fi" + } + } + } + }, "Wi-Fi Network \"" : { "extractionState" : "manual", "localizations" : { @@ -8443,6 +8600,17 @@ } } }, + "Wi-Fi Settings..." : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wi-Fi设置..." + } + } + } + }, "Wi-Fi: Connected" : { "extractionState" : "manual", "localizations" : { diff --git a/HeliPort/Appearance/StatusMenu.swift b/HeliPort/Appearance/StatusMenu.swift deleted file mode 100644 index 75f7cd1..0000000 --- a/HeliPort/Appearance/StatusMenu.swift +++ /dev/null @@ -1,666 +0,0 @@ -// -// StatusMenuView.swift -// HeliPort -// -// Created by 梁怀宇 on 2020/4/5. -// Copyright © 2020 OpenIntelWireless. All rights reserved. -// - -/* - * This program and the accompanying materials are licensed and made available - * under the terms and conditions of the The 3-Clause BSD License - * which accompanies this distribution. The full text of the license may be found at - * https://opensource.org/licenses/BSD-3-Clause - */ - -import Foundation -import Cocoa -import Sparkle - -final class StatusMenu: NSMenu, NSMenuDelegate { - - // - MARK: Properties - - private let networkListUpdatePeriod: Double = 5 - private let statusUpdatePeriod: Double = 2 - - private var headerLength: Int = 0 - private var networkListUpdateTimer: Timer? - private var statusUpdateTimer: Timer? - - // One instance at a time - private lazy var preferenceWindow = PrefsWindow() - - private var status: itl_80211_state = ITL80211_S_INIT { - didSet { - /* Only allow if network card is enabled or if the network card does not load - either due to itlwm not loaded or just not able to receive info - This prevents cards that are working but are "off" to not change the - Status from "WiFi off" to another status. i.e "WiFi: on". */ - guard isNetworkCardEnabled || !isNetworkCardAvailable else { return } - - statusItem.title = NSLocalizedString(status.description) - - switch status { - case ITL80211_S_INIT: - StatusBarIcon.shared().disconnected() - case ITL80211_S_AUTH, ITL80211_S_ASSOC: - StatusBarIcon.shared().connecting() - case ITL80211_S_RUN: - DispatchQueue.global(qos: .background).async { - let isReachable = NetworkManager.isReachable() - var staInfo = station_info_t() - get_station_info(&staInfo) - DispatchQueue.main.async { - guard isReachable else { StatusBarIcon.shared().warning(); return } - StatusBarIcon.shared().signalStrength(rssi: staInfo.rssi) - } - } - case ITL80211_S_SCAN: - /* - * API does not report bgscan to HeliPort. During ITL80211_S_RUN the status - * will never change to ITL80211_S_SCAN unless users manually disassociate. - * Set the icon to disconnected here so it displays correctly when users manually disassociate. - */ - StatusBarIcon.shared().disconnected() - default: - StatusBarIcon.shared().error() - } - } - } - - private var showAllOptions: Bool = false { - willSet(visible) { - let hiddenItems: [NSMenuItem] = [ - bsdItem, - macItem, - itlwmVerItem, - enableLoggingItem, - createReportItem, - diagnoseItem, - hardwareInfoSeparator, - - toggleLaunchItem, - checkUpdateItem, - quitSeparator, - aboutItem, - quitItem - ] - - let connectedNetworkInfoItems: [NSMenuItem] = [ - disconnectItem, - ipAddresssItem, - routerItem, - internetItem, - securityItem, - bssidItem, - channelItem, - countryCodeItem, - rssiItem, - noiseItem, - txRateItem, - phyModeItem, - mcsIndexItem, - nssItem - ] - - let enabledNetworkCardItems: [NSMenuItem] = [ - createNetworkItem, - manuallyJoinItem - ] - - let notImplementedItems: [NSMenuItem] = [ - enableLoggingItem, - diagnoseItem, - - securityItem, - countryCodeItem, - nssItem, - - createNetworkItem - ] - - hiddenItems.forEach { $0.isHidden = !visible } - enabledNetworkCardItems.forEach { $0.isHidden = !isNetworkCardAvailable } - connectedNetworkInfoItems.forEach { $0.isHidden = !(visible && - self.isNetworkConnected && - self.isNetworkCardEnabled) } - notImplementedItems.forEach { $0.isHidden = true } - } - } - - private var isNetworkConnected: Bool = false - - private var isNetworkListEmpty: Bool = true { - willSet(empty) { - networkItemListSeparator.isHidden = empty - guard empty else { return } - - for item in self.networkItemList { - (item.view as? WifiMenuItemView)?.visible = false - } - - for index in (0 ..< self.items.count).reversed() where self.items[index].view is WifiMenuItemView { - if index >= self.headerLength { - self.removeItem(at: index) - } - } - } - } - - private var isNetworkCardAvailable: Bool = true { - willSet(newState) { - if !newState { self.isNetworkCardEnabled = false } - } - } - - private var isNetworkCardEnabled: Bool = false { - willSet(newState) { - statusItem.title = newState ? .wifiOn : .wifiOff - switchItem.title = newState ? .turnWiFiOff : .turnWiFiOn - if newState != isNetworkCardEnabled { - newState ? StatusBarIcon.shared().on() : StatusBarIcon.shared().off() - self.isNetworkListEmpty = true - } - } - } - - private var isAutoLaunch: Bool = false { - willSet(newState) { - toggleLaunchItem.state = newState ? .on : .off - } - } - - // - MARK: Menu items - - private let statusItem = NSMenuItem(title: .statusUnavailable) - private let switchItem = NSMenuItem( - title: .turnWiFiOn, - action: #selector(clickMenuItem(_:)) - ) - private let bsdItem = NSMenuItem(title: .interfaceName + "(null)") - private let macItem = NSMenuItem(title: .macAddress + "(null)") - private let itlwmVerItem = NSMenuItem(title: .itlwmVer + "(null)") - - private let enableLoggingItem = NSMenuItem(title: .enableWiFiLog) - private let createReportItem = NSMenuItem(title: .createReport) - private let diagnoseItem = NSMenuItem(title: .openDiagnostics) - private let hardwareInfoSeparator = NSMenuItem.separator() - - private var networkItemList = [NSMenuItem]() - private let maxNetworkListLength = MAX_NETWORK_LIST_LENGTH - private let networkItemListSeparator: NSMenuItem = { - let networkItemListSeparator = NSMenuItem.separator() - networkItemListSeparator.isHidden = true - return networkItemListSeparator - }() - - private let manuallyJoinItem = NSMenuItem(title: .joinNetworks) - private let createNetworkItem = NSMenuItem(title: .createNetwork) - private let networkPanelItem = NSMenuItem(title: .openNetworkPrefs) - - private let aboutItem = NSMenuItem(title: .aboutHeliport) - private let checkUpdateItem = { - let item = NSMenuItem(title: .checkUpdates) - item.target = UpdateManager.sharedController - item.action = #selector(SPUStandardUpdaterController.checkForUpdates(_:)) - return item - }() - private let quitSeparator = NSMenuItem.separator() - private let quitItem = NSMenuItem(title: .quitHeliport, - action: #selector(clickMenuItem(_:)), keyEquivalent: "q") - - private let toggleLaunchItem = NSMenuItem( - title: .launchLogin, - action: #selector(clickMenuItem(_:)) - ) - - // MARK: - WiFi connected items - - let disconnectItem = NSMenuItem( - title: .disconnectNet + "(null)", - action: #selector(disassociateSSID(_:))) - private let ipAddresssItem = NSMenuItem(title: .ipAddr + "(null)") - private let routerItem = NSMenuItem(title: .routerStr + "(null)") - private let internetItem = NSMenuItem(title: .internetStr + "(null)") - private let securityItem = NSMenuItem(title: .securityStr + "(null)") - private let bssidItem = NSMenuItem(title: .bssidStr + "(null)") - private let channelItem = NSMenuItem(title: .channelStr + "(null)") - private let countryCodeItem = NSMenuItem(title: .countryCodeStr + "(null)") - private let rssiItem = NSMenuItem(title: .rssiStr + "(null)") - private let noiseItem = NSMenuItem(title: .noiseStr + "(null)") - private let txRateItem = NSMenuItem(title: .txRateStr + "(null)") - private let phyModeItem = NSMenuItem(title: .phyModeStr + "(null)") - private let mcsIndexItem = NSMenuItem(title: .mcsStr + "(null)") - private let nssItem = NSMenuItem(title: .nssStr + "(null)") - - // - MARK: Init - - init() { - super.init(title: "") - minimumWidth = CGFloat(286.0) - delegate = self - setupMenuHeaderAndFooter() - getDeviceInfo() - - DispatchQueue.global(qos: .default).async { - self.updateStatus() - self.updateNetworkList() - - self.isAutoLaunch = LoginItemManager.isEnabled() - - self.statusUpdateTimer = Timer.scheduledTimer( - timeInterval: self.statusUpdatePeriod, - target: self, - selector: #selector(self.updateStatus), - userInfo: nil, - repeats: true - ) - let currentRunLoop = RunLoop.current - currentRunLoop.add(self.statusUpdateTimer!, forMode: .common) - currentRunLoop.run() - } - - NSApp.servicesProvider = self - } - - required init(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // - MARK: Setup - - private func setupMenuHeaderAndFooter() { - addItem(bsdItem) - addItem(macItem) - addItem(itlwmVerItem) - - addClickItem(enableLoggingItem) - addClickItem(createReportItem) - addClickItem(diagnoseItem) - - addItem(hardwareInfoSeparator) - - addItem(statusItem) - addItem(switchItem) - switchItem.target = self - addItem(NSMenuItem.separator()) - - headerLength = items.count - networkItemList.append(addNetworkItemPlaceholder()) - - addItem(disconnectItem) - disconnectItem.target = self - addItem(ipAddresssItem) - addItem(routerItem) - addItem(internetItem) - addItem(securityItem) - addItem(bssidItem) - addItem(channelItem) - addItem(countryCodeItem) - addItem(rssiItem) - addItem(noiseItem) - addItem(txRateItem) - addItem(phyModeItem) - addItem(mcsIndexItem) - addItem(nssItem) - - headerLength = items.count - addItem(networkItemListSeparator) - - addClickItem(manuallyJoinItem) - addClickItem(createNetworkItem) - addClickItem(networkPanelItem) - - addItem(NSMenuItem.separator()) - - addClickItem(toggleLaunchItem) - addItem(checkUpdateItem) - addClickItem(aboutItem) - - addItem(quitSeparator) - addClickItem(quitItem) - } - - // - MARK: Overrides - - func menu(_ menu: NSMenu, willHighlight item: NSMenuItem?) { - networkItemList.forEach { ($0.view as? WifiMenuItemView)?.checkHighlight() } - } - - func menuWillOpen(_ menu: NSMenu) { - - showAllOptions = (NSApp.currentEvent?.modifierFlags.contains(.option))! - - DispatchQueue.global(qos: .default).async { - self.updateNetworkInfo() - self.updateNetworkList() - self.networkListUpdateTimer = Timer.scheduledTimer( - timeInterval: self.networkListUpdatePeriod, - target: self, - selector: #selector(self.updateNetworkList), - userInfo: nil, - repeats: true - ) - let currentRunLoop = RunLoop.current - currentRunLoop.add(self.networkListUpdateTimer!, forMode: .common) - currentRunLoop.run() - } - } - - func menuDidClose(_ menu: NSMenu) { - networkListUpdateTimer?.invalidate() - } - - // - MARK: Actions - - private func addClickItem(_ item: NSMenuItem) { - item.target = self - item.action = #selector(clickMenuItem(_:)) - addItem(item) - } - - private func getDeviceInfo() { - DispatchQueue.global(qos: .background).async { - var bsdName: String = .unavailable - var macAddr: String = .unavailable - var itlwmVer: String = .unavailable - var platformInfo = platform_info_t() - - if is_power_on() { - Log.debug("Wi-Fi powered on") - } else { - Log.debug("Wi-Fi powered off") - } - - if get_platform_info(&platformInfo) { - bsdName = String(cCharArray: platformInfo.device_info_str) - macAddr = NetworkManager.getMACAddressFromBSD(bsd: bsdName) ?? macAddr - itlwmVer = String(cCharArray: platformInfo.driver_info_str) - } - - DispatchQueue.main.async { - self.bsdItem.title = .interfaceName + bsdName - self.macItem.title = .macAddress + macAddr - self.itlwmVerItem.title = .itlwmVer + itlwmVer - } - - // If not connected, try to connect saved networks - var stationInfo = station_info_t() - var state: UInt32 = 0 - var power: Bool = false - get_power_state(&power) - if get_80211_state(&state) && power && - (state != ITL80211_S_RUN.rawValue || get_station_info(&stationInfo) != KERN_SUCCESS) { - NetworkManager.scanSavedNetworks() - } - } - } - - private func addNetworkItemPlaceholder() -> NSMenuItem { - let item = insertItem( - withTitle: "placeholder", - action: #selector(clickMenuItem(_:)), - keyEquivalent: "", - at: headerLength - ) - item.view = WifiMenuItemView( - networkInfo: NetworkInfo(ssid: "placeholder") - ) - guard let view = item.view as? WifiMenuItemView else { - return item - } - view.translatesAutoresizingMaskIntoConstraints = false - guard let supView = view.superview else { - return item - } - view.leadingAnchor.constraint(equalTo: supView.leadingAnchor).isActive = true - view.topAnchor.constraint(equalTo: supView.topAnchor).isActive = true - view.trailingAnchor.constraint(greaterThanOrEqualTo: supView.trailingAnchor).isActive = true - view.visible = false - return item - } - - // - MARK: Action handlers - - @objc private func clickMenuItem(_ sender: NSMenuItem) { - Log.debug("Clicked \(sender.title)") - switch sender.title { - case .createReport: - // Disable while bug report is being genetated, if autoenable == true, NSMenu ignores isEnable - createReportItem.action = nil - DispatchQueue.global(qos: .background).async { - BugReporter.generateBugReport() - DispatchQueue.main.async { - // Enable after generating report is finished - self.createReportItem.action = #selector(self.clickMenuItem(_:)) - } - } - case .turnWiFiOn: - power_on() - case .turnWiFiOff: - power_off() - case .joinNetworks: - let joinPop = WiFiConfigWindow() - joinPop.show() - case .createNetwork: - let alert = Alert(text: .notImplemented) - alert.show() - case .openNetworkPrefs: - preferenceWindow.close() - preferenceWindow.show() - case .launchLogin: - LoginItemManager.setStatus(enabled: LoginItemManager.isEnabled() ? false : true) - isAutoLaunch = LoginItemManager.isEnabled() - case .aboutHeliport: - NSApplication.shared.orderFrontStandardAboutPanel() - NSApplication.shared.activate(ignoringOtherApps: true) - case .quitHeliport: - NSApp.terminate(nil) - default: - Log.error("Invalid menu item clicked") - } - } - - @objc private func updateStatus() { - DispatchQueue.global(qos: .background).async { - var powerState: Bool = false - let get_power_ret = get_power_state(&powerState) - var status: UInt32 = 0xFF - let get_state_ret = get_80211_state(&status) - - DispatchQueue.main.async { - if get_power_ret && get_state_ret { - self.isNetworkCardEnabled = powerState - } else { - Log.error("Failed get card state") - } - self.isNetworkCardAvailable = get_power_ret - self.status = itl_80211_state(rawValue: status) - self.updateNetworkInfo() - } - } - } - - @objc private func updateNetworkInfo() { - guard isNetworkCardEnabled else { return } - - DispatchQueue.global(qos: .background).async { - var disconnectName: String = .unavailable - var ipAddr: String = .unavailable - var routerAddr: String = .unavailable - var internet: String = .unavailable - var security: String = .unavailable - var bssid: String = .unavailable - var channel: String = .unavailable - var countryCode: String = .unavailable - var rssi: String = .unavailable - var noise: String = .unavailable - var txRate: String = .unavailable - var phyMode: String = .unavailable - var mcsIndex: String = .unavailable - var nss: String = .unavailable - self.isNetworkConnected = false - var staInfo = station_info_t() - var entity: NetworkInfoStorageEntity? - var hideDisconnect = true - if self.status == ITL80211_S_RUN && get_station_info(&staInfo) == KERN_SUCCESS { - #if !DEBUG - entity = CredentialsManager.instance.getStorageFromSsid(String(cCharArray: staInfo.ssid)) - #endif - hideDisconnect = entity?.autoJoin ?? false - self.isNetworkConnected = true - let bsd = String(self.bsdItem.title).replacingOccurrences(of: String.interfaceName, with: "", - options: .regularExpression, range: nil) - let ipAddress = NetworkManager.getLocalAddress(bsd: bsd) - let routerAddress = NetworkManager.getRouterAddress(bsd: bsd) - let isReachable = NetworkManager.isReachable() - disconnectName = String.getSSIDFromCString(cString: &staInfo.ssid.0) - ipAddr = ipAddress ?? .unknown - routerAddr = routerAddress ?? .unknown - internet = isReachable ? .reachable : .unreachable - security = .unknown - bssid = String(format: "%02x:%02x:%02x:%02x:%02x:%02x", - staInfo.bssid.0, - staInfo.bssid.1, - staInfo.bssid.2, - staInfo.bssid.3, - staInfo.bssid.4, - staInfo.bssid.5 - ) - channel = "\(staInfo.channel) (\(staInfo.channel <= 14 ? 2.4 : 5) GHz, \(staInfo.band_width) MHz)" - countryCode = .unknown - rssi = "\(staInfo.rssi) dBm" - noise = "\(staInfo.noise) dBm" - txRate = "\(staInfo.rate) Mbps" - phyMode = staInfo.op_mode.description - mcsIndex = "\(staInfo.cur_mcs)" - nss = .unknown - } - - if self.showAllOptions && self.isNetworkConnected { - hideDisconnect = false - } - - DispatchQueue.main.async { - self.disconnectItem.title = .disconnectNet + disconnectName - self.ipAddresssItem.title = .ipAddr + ipAddr - self.routerItem.title = .routerStr + routerAddr - self.internetItem.title = .internetStr + internet - self.securityItem.title = .securityStr + security - self.bssidItem.title = .bssidStr + bssid - self.channelItem.title = .channelStr + channel - self.countryCodeItem.title = .countryCodeStr + countryCode - self.rssiItem.title = .rssiStr + rssi - self.noiseItem.title = .noiseStr + noise - self.txRateItem.title = .txRateStr + txRate - self.phyModeItem.title = .phyModeStr + phyMode - self.mcsIndexItem.title = .mcsStr + mcsIndex - self.nssItem.title = .nssStr + nss - self.disconnectItem.isHidden = hideDisconnect - guard self.isNetworkCardEnabled, - let wifiItemView = self.networkItemList.first?.view as? WifiMenuItemView else { return } - wifiItemView.visible = self.isNetworkConnected - wifiItemView.connected = self.isNetworkConnected - if self.isNetworkConnected { - self.isNetworkListEmpty = false - wifiItemView.networkInfo = NetworkInfo( - ssid: String.getSSIDFromCString(cString: &staInfo.ssid.0), - rssi: Int(staInfo.rssi) - ) - } - } - } - } - - @objc private func updateNetworkList() { - guard isNetworkCardEnabled else { return } - - NetworkManager.scanNetwork { networkList in - self.isNetworkListEmpty = networkList.count == 0 && !self.isNetworkConnected - var networkList = networkList - if networkList.count > self.maxNetworkListLength { - Log.error("Number of scanned networks (\(networkList.count))" + - " exceeds maximum (\(self.networkItemList.count))") - } - for index in (self.headerLength ..< self.items.count).reversed() - where self.items[index].view is WifiMenuItemView { - self.removeItem(at: index) - } - for _ in (1 ..< self.networkItemList.count) { - self.networkItemList.removeLast() - } - for _ in 0 ..< networkList.count { - self.networkItemList.append(self.addNetworkItemPlaceholder()) - } - for index in (self.headerLength ..< self.items.count) { - if let view = self.items[index].view as? WifiMenuItemView { - if networkList.count > 0 { - view.networkInfo = networkList.removeFirst() - view.visible = true - } - } - } - } - } - - @objc func disassociateSSID(_ sender: NSMenuItem) { - let ssid = String(sender.title).replacingOccurrences(of: String.disconnectNet, with: "", - options: .regularExpression, - range: nil) - DispatchQueue.global().async { - CredentialsManager.instance.setAutoJoin(ssid, false) - dis_associate_ssid(ssid) - Log.debug("Disconnected from \(ssid)") - } - } - - @objc func toggleWiFiServiceHandler(_ pboard: NSPasteboard, userData: String, error: NSErrorPointer) { - Log.debug("Handle Toggle WiFi service") - DispatchQueue.main.async { - self.clickMenuItem(self.switchItem) - } - } -} - -// MARK: Localized Strings - -private extension String { - static let notImplemented = NSLocalizedString("FUNCTION NOT IMPLEMENTED") - static let unknown = NSLocalizedString("Unknown") - static let unavailable = NSLocalizedString("Unavailable") - static let statusUnavailable = NSLocalizedString("Wi-Fi: Status unavailable") - static let turnWiFiOn = NSLocalizedString("Turn Wi-Fi On") - static let turnWiFiOff = NSLocalizedString("Turn Wi-Fi Off") - static let wifiOn = NSLocalizedString("Wi-Fi: On") - static let wifiOff = NSLocalizedString("Wi-Fi: Off") - static let interfaceName = NSLocalizedString("Interface Name: ") - static let macAddress = NSLocalizedString("Address: ") - static let itlwmVer = NSLocalizedString("Version: ") - static let enableWiFiLog = NSLocalizedString("Enable Wi-Fi Logging") - static let createReport = NSLocalizedString("Create Diagnostics Report...") - static let openDiagnostics = NSLocalizedString("Open Wireless Diagnostics...") - static let joinNetworks = NSLocalizedString("Join Other Network...") - static let createNetwork = NSLocalizedString("Create Network...") - static let openNetworkPrefs = NSLocalizedString("Open Network Preferences...") - static let checkUpdates = NSLocalizedString("Check for Updates...") - static let aboutHeliport = NSLocalizedString("About HeliPort") - static let quitHeliport = NSLocalizedString("Quit HeliPort") - static let launchLogin = NSLocalizedString("Launch At Login") - static let disconnectNet = NSLocalizedString("Disconnect from ") - static let ipAddr = NSLocalizedString(" IP Address: ") - static let routerStr = NSLocalizedString(" Router: ") - static let internetStr = NSLocalizedString(" Internet: ") - static let reachable = NSLocalizedString("Reachable") - static let unreachable = NSLocalizedString("Unreachable") - static let securityStr = NSLocalizedString(" Security: ") - static let bssidStr = NSLocalizedString(" BSSID: ") - static let channelStr = NSLocalizedString(" Channel: ") - static let countryCodeStr = NSLocalizedString(" Country Code: ") - static let rssiStr = NSLocalizedString(" RSSI: ") - static let noiseStr = NSLocalizedString(" Noise: ") - static let txRateStr = NSLocalizedString(" Tx Rate: ") - static let phyModeStr = NSLocalizedString(" PHY Mode: ") - static let mcsStr = NSLocalizedString(" MCS Index: ") - static let nssStr = NSLocalizedString(" NSS: ") -} diff --git a/HeliPort/Appearance/StatusMenu/MenuItemView/KeyValueMenuItemView.swift b/HeliPort/Appearance/StatusMenu/MenuItemView/KeyValueMenuItemView.swift new file mode 100644 index 0000000..b27e3d8 --- /dev/null +++ b/HeliPort/Appearance/StatusMenu/MenuItemView/KeyValueMenuItemView.swift @@ -0,0 +1,76 @@ +// +// KeyValueMenuItemView.swift +// HeliPort +// +// Created by Bat.bat on 24/6/2024. +// Copyright © 2024 OpenIntelWireless. All rights reserved. +// + +/* + * This program and the accompanying materials are licensed and made available + * under the terms and conditions of the The 3-Clause BSD License + * which accompanies this distribution. The full text of the license may be found at + * https://opensource.org/licenses/BSD-3-Clause + */ + +import Cocoa + +class KeyValueMenuItemView: NSView { + + enum Inset: CGFloat { + case standard = 14 + case staInfo = 34 + } + + private let keyLabel: NSTextField + private let valueLabel: NSTextField + + init(key: String, value: String? = nil, inset: Inset) { + keyLabel = NSTextField(labelWithString: key) + keyLabel.font = NSFont.systemFont(ofSize: 12, weight: .medium) + keyLabel.textColor = .secondaryLabelColor + + valueLabel = NSTextField(labelWithString: value ?? "(null)") + valueLabel.font = NSFont.systemFont(ofSize: 12, weight: .regular) + valueLabel.textColor = .secondaryLabelColor + + super.init(frame: .zero) + addSubview(keyLabel) + addSubview(valueLabel) + + translatesAutoresizingMaskIntoConstraints = false + keyLabel.translatesAutoresizingMaskIntoConstraints = false + valueLabel.translatesAutoresizingMaskIntoConstraints = false + + keyLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal) + valueLabel.setContentHuggingPriority(.defaultLow, for: .horizontal) + + let height = { + if #available(macOS 11, *) { + return NSMenuItem.ItemHeight.textModern + } + return NSMenuItem.ItemHeight.textLegacy + }() + + NSLayoutConstraint.activate([ + heightAnchor.constraint(equalToConstant: height.rawValue), + keyLabel.centerYAnchor.constraint(equalTo: centerYAnchor), + keyLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: inset.rawValue), + valueLabel.centerYAnchor.constraint(equalTo: centerYAnchor), + valueLabel.leadingAnchor.constraint(equalTo: keyLabel.trailingAnchor), + valueLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -14) + ]) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public var value: String? { + didSet { + if value != oldValue { + valueLabel.stringValue = value ?? "(null)" + } + } + } +} diff --git a/HeliPort/Appearance/StatusMenu/MenuItemView/SectionMenuItemView.swift b/HeliPort/Appearance/StatusMenu/MenuItemView/SectionMenuItemView.swift new file mode 100644 index 0000000..c433899 --- /dev/null +++ b/HeliPort/Appearance/StatusMenu/MenuItemView/SectionMenuItemView.swift @@ -0,0 +1,121 @@ +// +// SectionMenuItemView.swift +// HeliPort +// +// Created by Bat.bat on 27/6/2024. +// Copyright © 2024 OpenIntelWireless. All rights reserved. +// + +/* + * This program and the accompanying materials are licensed and made available + * under the terms and conditions of the The 3-Clause BSD License + * which accompanies this distribution. The full text of the license may be found at + * https://opensource.org/licenses/BSD-3-Clause + */ + +import Foundation +import Cocoa + +@available(macOS 11, *) +class SectionMenuItemView: SelectableMenuItemView { + + // MARK: Initializers + + private let label: NSTextField = { + let label = NSTextField(labelWithString: "") + label.font = NSFont.systemFont(ofSize: 12, weight: .semibold) + label.textColor = .secondaryLabelColor + return label + }() + + private static let chevronDown = NSImage(systemSymbolName: "chevron.down")? + .withSymbolConfiguration(NSImage.SymbolConfiguration(pointSize: 12, weight: .regular)) + private static let chevronRight = NSImage(systemSymbolName: "chevron.right")? + .withSymbolConfiguration(NSImage.SymbolConfiguration(pointSize: 12, weight: .regular)) + + private let chevronImage: NSImageView = { + let imageView = NSImageView() + imageView.setContentHuggingPriority(.defaultHigh, for: .horizontal) + imageView.image = chevronRight + imageView.wantsLayer = true + imageView.image?.isTemplate = true + imageView.alphaValue = 0.9 + return imageView + }() + + let expandAction: ((Bool) -> Void)? + + var title: String { + willSet { + label.stringValue = newValue + } + } + + var isExpanded: Bool = false { + willSet { + guard newValue != isExpanded else { return } + self.expandAction?(newValue) + animateImageTransition(imageView: chevronImage, + toImage: newValue ? SectionMenuItemView.chevronDown + : SectionMenuItemView.chevronRight, + onComplete: nil) + } + } + + init(title: String, expandAction: ((Bool) -> Void)? = nil) { + self.title = title + self.expandAction = expandAction + super.init(height: .textModern, hoverStyle: expandAction == nil ? .none : .greytint) + + chevronImage.isHidden = expandAction == nil + label.stringValue = title + + addSubview(label) + addSubview(chevronImage) + setupLayout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: Private + + private func animateImageTransition(imageView: NSImageView, toImage: NSImage?, onComplete: (() -> Void)? = nil) { + // Fade out + NSAnimationContext.runAnimationGroup({ context in + context.duration = 0.25 + imageView.animator().alphaValue = 0.0 + }, completionHandler: { + imageView.image = toImage + + // Fade in + NSAnimationContext.runAnimationGroup({ context in + context.duration = 0.25 + imageView.animator().alphaValue = 0.9 + }, completionHandler: onComplete) + }) + } + + // MARK: Overrides + + override func mouseUp(with event: NSEvent) { + super.mouseUp(with: event) + isExpanded = !isExpanded + } + + internal override func setupLayout() { + super.setupLayout() + translatesAutoresizingMaskIntoConstraints = false + self.subviews.forEach { $0.translatesAutoresizingMaskIntoConstraints = false } + + NSLayoutConstraint.activate([ + label.centerYAnchor.constraint(equalTo: centerYAnchor), + label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 14), + label.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -14), + + chevronImage.firstBaselineAnchor.constraint(equalTo: label.firstBaselineAnchor), + chevronImage.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -13) + ]) + } +} diff --git a/HeliPort/Appearance/StatusMenu/MenuItemView/SelectableMenuItemView.swift b/HeliPort/Appearance/StatusMenu/MenuItemView/SelectableMenuItemView.swift new file mode 100644 index 0000000..3288bf9 --- /dev/null +++ b/HeliPort/Appearance/StatusMenu/MenuItemView/SelectableMenuItemView.swift @@ -0,0 +1,175 @@ +// +// SelectableMenuItemView.swift +// HeliPort +// +// Created by Bat.bat on 20/6/2024. +// Copyright © 2024 OpenIntelWireless. All rights reserved. +// + +/* + * This program and the accompanying materials are licensed and made available + * under the terms and conditions of the The 3-Clause BSD License + * which accompanies this distribution. The full text of the license may be found at + * https://opensource.org/licenses/BSD-3-Clause + */ + +import Cocoa + +class SelectableMenuItem: NSMenuItem { + override func _canBeHighlighted() -> Bool { + return true + } +} + +class SelectableMenuItemView: NSView { + + private class HoverView: NSView { + override func draw(_ dirtyRect: NSRect) { + super.draw(dirtyRect) + NSColor(named: "HoverColor")?.setFill() + dirtyRect.fill() + } + } + + // MARK: Initializers + + private var currentWindow: NSWindow? + private var heightConstraint: NSLayoutConstraint! + + private let effectView: NSView? + + private let effectPadding: CGFloat = { + if #available(macOS 11, *) { + return 5 + } + return 0 + }() + + private let height: CGFloat + + init(height: NSMenuItem.ItemHeight, hoverStyle: HoverStyle) { + self.height = height.rawValue + switch hoverStyle { + case .none: + effectView = nil + case .greytint: + effectView = HoverView() + effectView?.isHidden = true + case .selection: + let view = NSVisualEffectView() + view.material = .popover + view.state = .active + view.isEmphasized = true + view.blendingMode = .behindWindow + effectView = view + } + + super.init(frame: .zero) + if let view = effectView { self.addSubview(view) } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: Public + + public enum HoverStyle { + case none + case selection + case greytint + } + + public func checkHighlight() { + if effectView != nil, let position = currentWindow?.mouseLocationOutsideOfEventStream { + isMouseOver = bounds.contains(convert(position, from: nil)) + } + } + + // MARK: Internal + + var isMouseOver: Bool = false { + willSet(hover) { + if let view = effectView as? NSVisualEffectView { + view.material = hover ? .selection : .popover + } else { + effectView?.isHidden = !hover + } + } + } + + func setupLayout() { + translatesAutoresizingMaskIntoConstraints = false + subviews.forEach { $0.translatesAutoresizingMaskIntoConstraints = false } + + if #available(macOS 11, *) { + effectView?.wantsLayer = true + effectView?.layer?.cornerRadius = 4 + effectView?.layer?.masksToBounds = true + } + + heightConstraint = heightAnchor.constraint(equalToConstant: self.height) + heightConstraint.priority = NSLayoutConstraint.Priority(rawValue: 1000) + heightConstraint.isActive = true + + effectView?.translatesAutoresizingMaskIntoConstraints = false + effectView?.subviews.forEach { $0.translatesAutoresizingMaskIntoConstraints = false } + effectView?.leftAnchor.constraint(equalTo: self.leftAnchor, constant: effectPadding).isActive = true + effectView?.rightAnchor.constraint(equalTo: self.rightAnchor, constant: -effectPadding).isActive = true + effectView?.topAnchor.constraint(equalTo: topAnchor).isActive = true + effectView?.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true + } + + func performMenuItemAction() { + guard let menuItem = enclosingMenuItem, let menu = menuItem.menu, + menuItem.isEnabled else { + return + } + + isMouseOver = false // NSWindow pop up could escape mouseExit + menu.cancelTracking() + menu.performActionForItem(at: menu.index(of: menuItem)) + } + + // MARK: Overrides + + override func mouseUp(with event: NSEvent) { + guard let view = effectView else { + performMenuItemAction() + return + } + + // Simulate original click flash animtion + NSAnimationContext.runAnimationGroup({ context in + context.duration = 0.06 + view.animator().alphaValue = 0 + }, completionHandler: { + self.checkHighlight() + NSAnimationContext.runAnimationGroup({ context in + context.duration = 0.06 + view.animator().alphaValue = 1 + }, completionHandler: { + self.performMenuItemAction() + }) + }) + } + + override func viewWillMove(toWindow newWindow: NSWindow?) { + // Fix mouseUp event after losing focus + // https://stackoverflow.com/questions/15075033/weird-issue-with-nsmenuitem-custom-view-and-mouseup + super.viewWillMove(toWindow: newWindow) + newWindow?.becomeKey() + currentWindow = newWindow + } + + override func layout() { + super.layout() + if #available(macOS 11, *) { + effectView?.frame = CGRect(x: effectPadding, y: 0, + width: bounds.width - effectPadding * 2, + height: bounds.height) + } else { + effectView?.frame = bounds + } + } +} diff --git a/HeliPort/Appearance/StatusMenu/MenuItemView/StateSwitchMenuItemView.swift b/HeliPort/Appearance/StatusMenu/MenuItemView/StateSwitchMenuItemView.swift new file mode 100644 index 0000000..635c720 --- /dev/null +++ b/HeliPort/Appearance/StatusMenu/MenuItemView/StateSwitchMenuItemView.swift @@ -0,0 +1,90 @@ +// +// StateSwitchMenuItemView.swift +// HeliPort +// +// Created by Bat.bat on 22/6/2024. +// Copyright © 2024 OpenIntelWireless. All rights reserved. +// + +/* + * This program and the accompanying materials are licensed and made available + * under the terms and conditions of the The 3-Clause BSD License + * which accompanies this distribution. The full text of the license may be found at + * https://opensource.org/licenses/BSD-3-Clause + */ + +import Cocoa + +@available(macOS 11, *) +class StateSwitchMenuItemView: NSView { + + // MARK: Initializers + + private let label: NSTextField = { + let label = NSTextField(labelWithString: "") + label.font = NSFont.systemFont(ofSize: NSFont.menuFont(ofSize: 0).pointSize, weight: .semibold) + label.textColor = .controlTextColor + return label + }() + + private let stateSwitch = NSSwitch() + private let actionClosure: ((NSSwitch) -> Void) + + init(title: String, action: @escaping (NSSwitch) -> Void) { + actionClosure = action + super.init(frame: .zero) + translatesAutoresizingMaskIntoConstraints = false + + addSubview(label) + addSubview(stateSwitch) + + label.stringValue = title + stateSwitch.action = #selector(switchValueDidChange) + stateSwitch.target = self + + setupLayout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: Public + + public var state: Bool = false { + willSet(state) { + stateSwitch.state = state ? .on : .off + } + } + + public var isEnabled: Bool = false { + willSet { + stateSwitch.isEnabled = newValue + } + } + + public func toggle() { + stateSwitch.performClick(self) + } + + // MARK: Private + + private func setupLayout() { + self.subviews.forEach { $0.translatesAutoresizingMaskIntoConstraints = false } + + let heightConstraint = heightAnchor.constraint(equalToConstant: NSMenuItem.ItemHeight.networkModern.rawValue) + heightConstraint.priority = NSLayoutConstraint.Priority(1000) + heightConstraint.isActive = true + + label.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true + label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 14).isActive = true + + stateSwitch.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true + stateSwitch.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -14).isActive = true + stateSwitch.heightAnchor.constraint(equalToConstant: 30).isActive = true + } + + @objc private func switchValueDidChange(sender: NSSwitch) { + actionClosure(sender) + } +} diff --git a/HeliPort/Appearance/StatusMenu/MenuItemView/WiFiMenuItemView.swift b/HeliPort/Appearance/StatusMenu/MenuItemView/WiFiMenuItemView.swift new file mode 100644 index 0000000..04fca04 --- /dev/null +++ b/HeliPort/Appearance/StatusMenu/MenuItemView/WiFiMenuItemView.swift @@ -0,0 +1,22 @@ +// +// WiFiMenuItemView.swift +// HeliPort +// +// Created by 梁怀宇 on 2020/4/3. +// Copyright © 2020 OpenIntelWireless. All rights reserved. +// + +/* + * This program and the accompanying materials are licensed and made available + * under the terms and conditions of the The 3-Clause BSD License + * which accompanies this distribution. The full text of the license may be found at + * https://opensource.org/licenses/BSD-3-Clause + */ + +import Cocoa + +protocol WifiMenuItemView: SelectableMenuItemView { + var networkInfo: NetworkInfo { get set } + var connected: Bool { get set } + func updateImages() +} diff --git a/HeliPort/Appearance/WiFiMenuItemView.swift b/HeliPort/Appearance/StatusMenu/MenuItemView/WiFiMenuItemViewLegacy.swift similarity index 54% rename from HeliPort/Appearance/WiFiMenuItemView.swift rename to HeliPort/Appearance/StatusMenu/MenuItemView/WiFiMenuItemViewLegacy.swift index 7f36c85..c86ab6b 100644 --- a/HeliPort/Appearance/WiFiMenuItemView.swift +++ b/HeliPort/Appearance/StatusMenu/MenuItemView/WiFiMenuItemViewLegacy.swift @@ -1,5 +1,5 @@ // -// WiFiMenuItemView.swift +// WiFiMenuItemViewLegacy.swift // HeliPort // // Created by 梁怀宇 on 2020/4/3. @@ -13,33 +13,12 @@ * https://opensource.org/licenses/BSD-3-Clause */ -import Foundation import Cocoa -class WifiMenuItemView: NSView { +class WifiMenuItemViewLegacy: SelectableMenuItemView, WifiMenuItemView { // MARK: Initializers - private var currentWindow: NSWindow? - private var heightConstraint: NSLayoutConstraint! - - private let menuBarHeight: CGFloat = { - if #available(macOS 11, *) { - return 22 - } else { - return 19 - } - }() - - private let effectView: NSVisualEffectView = { - let effectView = NSVisualEffectView() - effectView.material = .popover - effectView.state = .active - effectView.isEmphasized = true - effectView.blendingMode = .behindWindow - return effectView - }() - private let statusImage: NSImageView = { let statusImage = NSImageView() statusImage.setContentHuggingPriority(.defaultHigh, for: .horizontal) @@ -91,18 +70,27 @@ class WifiMenuItemView: NSView { return ssidLabel }() - public init(networkInfo: NetworkInfo) { + init(networkInfo: NetworkInfo) { self.networkInfo = networkInfo - super.init(frame: .zero) - translatesAutoresizingMaskIntoConstraints = false - - self.addSubview(effectView) - effectView.addSubview(statusImage) - effectView.addSubview(ssidLabel) - effectView.addSubview(lockImage) - effectView.addSubview(signalImage) + let height: NSMenuItem.ItemHeight = { + if #available(macOS 11, *) { + return .textModern + } + return .textLegacy + }() + super.init(height: height, hoverStyle: .selection) + + addSubview(statusImage) + addSubview(ssidLabel) + addSubview(lockImage) + addSubview(signalImage) setupLayout() + + // willSet/didSet will not be called during initialization + defer { + self.networkInfo = networkInfo + } } required init?(coder: NSCoder) { @@ -114,17 +102,10 @@ class WifiMenuItemView: NSView { public var networkInfo: NetworkInfo { willSet(networkInfo) { ssidLabel.stringValue = networkInfo.ssid - lockImage.isHidden = networkInfo.auth.security == ITL80211_SECURITY_NONE - signalImage.image = StatusBarIcon.shared().getRssiImage(rssi: Int16(networkInfo.rssi)) layoutSubtreeIfNeeded() } - } - - public var visible: Bool = true { - willSet(visible) { - isHidden = !visible - heightConstraint.constant = visible ? menuBarHeight : 0 - layoutSubtreeIfNeeded() + didSet { + updateImages() } } @@ -134,18 +115,16 @@ class WifiMenuItemView: NSView { } } - public func checkHighlight() { - if visible, let position = currentWindow?.mouseLocationOutsideOfEventStream { - isMouseOver = bounds.contains(convert(position, from: nil)) - } + public func updateImages() { + signalImage.image = StatusBarIcon.shared().getRssiImage(rssi: Int16(networkInfo.rssi)) + lockImage.isHidden = networkInfo.auth.security == ITL80211_SECURITY_NONE } - // MARK: Private + // MARK: Overrides - private var isMouseOver: Bool = false { + override var isMouseOver: Bool { willSet(hover) { - effectView.material = hover ? .selection : .popover - effectView.isEmphasized = hover + super.isMouseOver = hover ssidLabel.textColor = hover ? .selectedMenuItemTextColor : .controlTextColor @@ -155,30 +134,16 @@ class WifiMenuItemView: NSView { } } - private func setupLayout() { + override func setupLayout() { + super.setupLayout() - let effectPadding: CGFloat - let statusPadding: CGFloat - let statusWidth: CGFloat - let lockWidth: CGFloat - if #available(macOS 11, *) { - effectView.wantsLayer = true - effectView.layer?.cornerRadius = 4 - effectView.layer?.masksToBounds = true - effectPadding = 5 - statusPadding = 10 - statusWidth = 15 - lockWidth = 16 - } else { - effectPadding = 0 - statusPadding = 6 - statusWidth = 12 - lockWidth = 10 - } - - heightConstraint = heightAnchor.constraint(equalToConstant: menuBarHeight) - heightConstraint.priority = NSLayoutConstraint.Priority(rawValue: 1000) - heightConstraint.isActive = true + let (statusPadding, statusWidth, lockWidth): (CGFloat, CGFloat, CGFloat) = { + if #available(macOS 11, *) { + return (10, 15, 16) + } else { + return (6, 12, 10) + } + }() statusImage.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true statusImage.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: statusPadding).isActive = true @@ -195,46 +160,17 @@ class WifiMenuItemView: NSView { signalImage.leadingAnchor.constraint(equalTo: lockImage.trailingAnchor, constant: 12).isActive = true signalImage.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -12).isActive = true signalImage.widthAnchor.constraint(equalToConstant: 18).isActive = true - - effectView.translatesAutoresizingMaskIntoConstraints = false - effectView.subviews.forEach { $0.translatesAutoresizingMaskIntoConstraints = false } - effectView.leftAnchor.constraint(equalTo: self.leftAnchor, constant: effectPadding).isActive = true - effectView.rightAnchor.constraint(equalTo: self.rightAnchor, constant: -effectPadding).isActive = true - effectView.topAnchor.constraint(equalTo: topAnchor).isActive = true - effectView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true } - // MARK: Overrides - - override func mouseUp(with event: NSEvent) { - isMouseOver = false // NSWindow pop up could escape mouseExit - enclosingMenuItem?.menu?.cancelTracking() + override func performMenuItemAction() { if !connected { NetworkManager.connect(networkInfo: networkInfo, saveNetwork: true) } - } - - override func viewWillMove(toWindow newWindow: NSWindow?) { - // Fix mouseUp event after losing focus - // https://stackoverflow.com/questions/15075033/weird-issue-with-nsmenuitem-custom-view-and-mouseup - super.viewWillMove(toWindow: newWindow) - newWindow?.becomeKey() - currentWindow = newWindow - } - override func draw(_ rect: NSRect) { - checkHighlight() - } + isMouseOver = false // NSWindow pop up could escape mouseExit - override func layout() { - super.layout() - if #available(macOS 11, *) { - effectView.frame = CGRect(x: 5, // effectPadding - y: 0, - width: bounds.width - 10, // effectPadding * 2 - height: bounds.height) - } else { - effectView.frame = bounds - } + guard let menuItem = enclosingMenuItem, let menu = menuItem.menu else { return } + menu.cancelTracking() + menu.performActionForItem(at: menu.index(of: menuItem)) } } diff --git a/HeliPort/Appearance/StatusMenu/MenuItemView/WiFiMenuItemViewModern.swift b/HeliPort/Appearance/StatusMenu/MenuItemView/WiFiMenuItemViewModern.swift new file mode 100644 index 0000000..5f1c46a --- /dev/null +++ b/HeliPort/Appearance/StatusMenu/MenuItemView/WiFiMenuItemViewModern.swift @@ -0,0 +1,181 @@ +// +// WiFiMenuItemViewModern.swift +// HeliPort +// +// Created by Bat.bat on 19/6/2024. +// Copyright © 2024 OpenIntelWireless. All rights reserved. +// + +/* + * This program and the accompanying materials are licensed and made available + * under the terms and conditions of the The 3-Clause BSD License + * which accompanies this distribution. The full text of the license may be found at + * https://opensource.org/licenses/BSD-3-Clause + */ + +import Cocoa + +@available(macOS 11, *) +private class CircleSignalView: NSView { + var image: NSImage? { + willSet { + signalView.image = newValue + } + } + + var active: Bool = false { + willSet { + fillColor = newValue ? .controlAccentColor : CircleSignalView.inactiveColor + signalView.contentTintColor = newValue ? .white : .controlTextColor + // Force redraw to apply the new fillColor + setNeedsDisplay(bounds) + } + } + + private static let inactiveColor = NSColor(named: "SignalBackgroundColor")! + + private let signalView = NSImageView() + private var fillColor: NSColor = inactiveColor + + init() { + super.init(frame: .zero) + addSubview(signalView) + + let signalSize: CGFloat = 17 + + translatesAutoresizingMaskIntoConstraints = false + signalView.translatesAutoresizingMaskIntoConstraints = false + signalView.imageScaling = .scaleProportionallyUpOrDown + NSLayoutConstraint.activate([ + signalView.centerXAnchor.constraint(equalTo: self.centerXAnchor), + signalView.centerYAnchor.constraint(equalTo: self.centerYAnchor), + signalView.widthAnchor.constraint(equalToConstant: signalSize), + signalView.heightAnchor.constraint(equalToConstant: signalSize) + ]) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func draw(_ dirtyRect: NSRect) { + super.draw(dirtyRect) + let path = NSBezierPath(ovalIn: NSRect(x: 1, y: 1, width: 26, height: 26)) + fillColor.setFill() + path.fill() + } +} + +@available(macOS 11, *) +class WifiMenuItemViewModern: SelectableMenuItemView, WifiMenuItemView { + + // MARK: Initializers + + private let lockImage: NSImageView = { + let lockImage = NSImageView() + lockImage.setContentHuggingPriority(.defaultHigh, for: .horizontal) + lockImage.image = NSImage(systemSymbolName: "lock.fill")? + .withSymbolConfiguration(NSImage.SymbolConfiguration(pointSize: 13, weight: .medium)) + lockImage.image?.isTemplate = true + lockImage.alphaValue = 0.5 + return lockImage + }() + + private let signalCircle: CircleSignalView = { + let signalImage = CircleSignalView() + signalImage.setContentHuggingPriority(.defaultHigh, for: .horizontal) + return signalImage + }() + + private let ssidLabel: NSTextField = { + let ssidLabel = NSTextField(labelWithString: "") + ssidLabel.font = NSFont.menuFont(ofSize: 0) + return ssidLabel + }() + + init(networkInfo: NetworkInfo) { + self.networkInfo = networkInfo + super.init(height: .networkModern, hoverStyle: .greytint) + + addSubview(ssidLabel) + addSubview(lockImage) + addSubview(signalCircle) + + setupLayout() + + // willSet/didSet will not be called during initialization + defer { + self.networkInfo = networkInfo + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: Public + + public var networkInfo: NetworkInfo { + willSet(info) { + ssidLabel.stringValue = info.ssid + layoutSubtreeIfNeeded() + } + didSet { + updateImages() + } + } + + public var connected: Bool = false { + didSet { + guard oldValue != connected else { return } + signalCircle.active = connected + } + } + + public func updateImages() { + signalCircle.image = StatusBarIcon.shared().getRssiImage(rssi: Int16(networkInfo.rssi)) + lockImage.isHidden = networkInfo.auth.security == ITL80211_SECURITY_NONE + } + + // MARK: Internal + + internal override func setupLayout() { + super.setupLayout() + + let signalCircleSize: CGFloat = 28 + let lockWidth: CGFloat = 16 + + NSLayoutConstraint.activate([ + signalCircle.centerYAnchor.constraint(equalTo: self.centerYAnchor), + signalCircle.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 12), + signalCircle.widthAnchor.constraint(equalToConstant: signalCircleSize), + signalCircle.heightAnchor.constraint(equalToConstant: signalCircleSize), + + ssidLabel.centerYAnchor.constraint(equalTo: self.centerYAnchor), + ssidLabel.leadingAnchor.constraint(equalTo: signalCircle.trailingAnchor, constant: 6), + + lockImage.centerYAnchor.constraint(equalTo: self.centerYAnchor), + lockImage.leadingAnchor.constraint(equalTo: ssidLabel.trailingAnchor, constant: 6), + lockImage.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -12), + lockImage.widthAnchor.constraint(equalToConstant: lockWidth) + ]) + } + + // MARK: Overrides + + override func mouseUp(with event: NSEvent) { + // Do not close the menu if the user clicked on a connected item + if connected { + connected = false + updateImages() + DispatchQueue.global().async { + dis_associate_ssid(self.networkInfo.ssid) + Log.debug("Disconnected from \(self.networkInfo.ssid)") + } + } else { + isMouseOver = false + enclosingMenuItem?.menu?.cancelTracking() + NetworkManager.connect(networkInfo: networkInfo, saveNetwork: true) + } + } +} diff --git a/HeliPort/Appearance/StatusMenu/StatusMenuBase.swift b/HeliPort/Appearance/StatusMenu/StatusMenuBase.swift new file mode 100644 index 0000000..98f651f --- /dev/null +++ b/HeliPort/Appearance/StatusMenu/StatusMenuBase.swift @@ -0,0 +1,587 @@ +// +// StatusMenuBase.swift +// HeliPort +// +// Created by 梁怀宇 on 2020/4/5. +// Copyright © 2020 OpenIntelWireless. All rights reserved. +// + +/* + * This program and the accompanying materials are licensed and made available + * under the terms and conditions of the The 3-Clause BSD License + * which accompanies this distribution. The full text of the license may be found at + * https://opensource.org/licenses/BSD-3-Clause + */ + +import Cocoa +import Sparkle + +protocol StatusMenuItems { + var enabledNetworkCardItems: [NSMenuItem] { get } + var stationInfoItems: [NSMenuItem] { get } + var hiddenItems: [NSMenuItem] { get } + var notImplementedItems: [NSMenuItem] { get } + + func setupMenu() + func setValueForItem(_ item: NSMenuItem, value: String) + func updateNetworkList() + func toggleWIFI() +} + +class StatusMenuBase: NSMenu, NSMenuDelegate { + + // - MARK: Properties + + private let networkListUpdatePeriod: Double = 5 + private let statusUpdatePeriod: Double = 2 + + var headerLength: Int = 0 + private var networkListUpdateTimer: Timer? + private var statusUpdateTimer: Timer? + + // One instance at a time + private lazy var preferenceWindow = PrefsWindow() + + private var driverState: itl_80211_state = ITL80211_S_INIT { + didSet { + /** Only allow if network card is enabled or if the network card does not load + either due to itlwm not loaded or just not able to receive info + This prevents cards that are working but are "off" to not change the + Status from "WiFi off" to another status. i.e "WiFi: on". */ + guard isNetworkCardEnabled || !isNetworkCardAvailable else { return } + + switch driverState { + case ITL80211_S_INIT: + StatusBarIcon.shared().disconnected() + case ITL80211_S_AUTH, ITL80211_S_ASSOC: + StatusBarIcon.shared().connecting() + case ITL80211_S_RUN: + DispatchQueue.global(qos: .background).async { + let isReachable = NetworkManager.isReachable() + var staInfo = station_info_t() + get_station_info(&staInfo) + DispatchQueue.main.async { + guard isReachable else { StatusBarIcon.shared().warning(); return } + StatusBarIcon.shared().signalStrength(rssi: staInfo.rssi) + } + } + case ITL80211_S_SCAN: + /** API does not report bgscan to HeliPort. During `ITL80211_S_RUN` the status + will never change to `ITL80211_S_SCAN` unless users manually disassociate. + Set the icon to disconnected here so it displays correctly when users manually disassociate. */ + StatusBarIcon.shared().disconnected() + default: + StatusBarIcon.shared().error() + } + } + } + + var showAllOptions: Bool = false { + willSet(visible) { + guard let items = self as? StatusMenuItems else { return } + + items.hiddenItems.forEach { $0.isHidden = !visible } + items.enabledNetworkCardItems.forEach { $0.isHidden = !isNetworkCardAvailable } + items.stationInfoItems.forEach { $0.isHidden = !(visible && + self.isNetworkConnected && + self.isNetworkCardEnabled) } + items.notImplementedItems.forEach { $0.isHidden = true } + } + } + + var isNetworkConnected: Bool = false { + willSet { + guard isNetworkConnected != newValue, let items = self as? StatusMenuItems else { return } + items.stationInfoItems.forEach { $0.isHidden = !newValue || !showAllOptions } + } + } + + var isNetworkListEmpty: Bool = true { + willSet(empty) { + guard empty else { return } + currentNetworkItem.isHidden = true + } + } + + var isNetworkCardAvailable: Bool = true { + willSet(newState) { + if !newState && newState != isNetworkCardAvailable { + self.isNetworkCardEnabled = false + } + } + } + + var isNetworkCardEnabled: Bool = true { + willSet(newState) { + guard newState != isNetworkCardEnabled else { return } + + newState ? StatusBarIcon.shared().on() : StatusBarIcon.shared().off() + + if !newState { + self.isNetworkListEmpty = true + self.isNetworkConnected = false + } + } + } + + private var isAutoLaunch: Bool = false { + willSet(newState) { + toggleLaunchItem.state = newState ? .on : .off + } + } + + // - MARK: Common Menu items + + let bsdItem = NSMenuItem(title: .interfaceName) + let macItem = NSMenuItem(title: .macAddress) + let itlwmVerItem = NSMenuItem(title: .itlwmVer) + + let enableLoggingItem = NSMenuItem(title: .enableWiFiLog) + let createReportItem = NSMenuItem(title: .createReport) + let diagnoseItem = NSMenuItem(title: .openDiagnostics) + let hardwareInfoSeparator = NSMenuItem.separator() + + let networkItemListSeparator = NSMenuItem.separator() + + let aboutItem = NSMenuItem(title: .aboutHeliport) + let checkUpdateItem = { + let item = NSMenuItem(title: .checkUpdates) + item.target = UpdateManager.sharedController + item.action = #selector(SPUStandardUpdaterController.checkForUpdates(_:)) + return item + }() + let quitSeparator = NSMenuItem.separator() + let quitItem = NSMenuItem(title: .quitHeliport, + action: #selector(clickMenuItem(_:)), keyEquivalent: "q") + + let toggleLaunchItem = NSMenuItem(title: .launchLogin, + action: #selector(clickMenuItem(_:))) + + // MARK: - WiFi connected items + + let currentNetworkItem = SelectableMenuItem() + let ipAddresssItem = NSMenuItem(title: .ipAddr) + let routerItem = NSMenuItem(title: .routerStr) + let internetItem = NSMenuItem(title: .internetStr) + let securityItem = NSMenuItem(title: .securityStr) + let bssidItem = NSMenuItem(title: .bssidStr) + let channelItem = NSMenuItem(title: .channelStr) + let countryCodeItem = NSMenuItem(title: .countryCodeStr) + let rssiItem = NSMenuItem(title: .rssiStr) + let noiseItem = NSMenuItem(title: .noiseStr) + let txRateItem = NSMenuItem(title: .txRateStr) + let phyModeItem = NSMenuItem(title: .phyModeStr) + let mcsIndexItem = NSMenuItem(title: .mcsStr) + let nssItem = NSMenuItem(title: .nssStr) + + // - MARK: Init + + init() { + super.init(title: "") + delegate = self + isAutoLaunch = LoginItemManager.isEnabled() + + (self as? StatusMenuItems)?.setupMenu() + getDeviceInfo() + + DispatchQueue.global(qos: .default).async { + self.statusUpdateTimer = Timer.scheduledTimer( + timeInterval: self.statusUpdatePeriod, + target: self, + selector: #selector(self.updateStatus), + userInfo: nil, + repeats: true + ) + + self.statusUpdateTimer?.fire() + let currentRunLoop = RunLoop.current + currentRunLoop.add(self.statusUpdateTimer!, forMode: .common) + currentRunLoop.run() + } + + NSApp.servicesProvider = self + } + + required init(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // - MARK: NSMenuDelegate + + func menu(_ menu: NSMenu, willHighlight item: NSMenuItem?) { + (menu.highlightedItem?.view as? SelectableMenuItemView)?.isMouseOver = false + (item?.view as? SelectableMenuItemView)?.isMouseOver = true + } + + func menuWillOpen(_ menu: NSMenu) { + showAllOptions = (NSApp.currentEvent?.modifierFlags.contains(.option)) ?? false + + DispatchQueue.global(qos: .default).async { + self.updateStationItems() + self.networkListUpdateTimer = Timer.scheduledTimer( + timeInterval: self.networkListUpdatePeriod, + target: self, + selector: #selector(self.updateNetworkList), + userInfo: nil, + repeats: true + ) + self.networkListUpdateTimer?.fire() + let currentRunLoop = RunLoop.current + currentRunLoop.add(self.networkListUpdateTimer!, forMode: .common) + currentRunLoop.run() + } + } + + func menuDidClose(_ menu: NSMenu) { + networkListUpdateTimer?.invalidate() + (menu.highlightedItem?.view as? SelectableMenuItemView)?.isMouseOver = false + } + + // - MARK: Actions + + func addClickItem(_ item: NSMenuItem, target: AnyObject? = nil, action: Selector? = nil) { + item.target = target ?? self + item.action = action ?? #selector(clickMenuItem(_:)) + addItem(item) + } + + func addNetworkItem(_ item: NSMenuItem = SelectableMenuItem(), insertAt: Int? = nil, hidden: Bool = false, + networkInfo: NetworkInfo = NetworkInfo(ssid: "placeholder")) -> NSMenuItem { + item.isHidden = hidden + + if let insertAt { + insertItem(item, at: insertAt) + } else { + addItem(item) + } + + return item + } + + private func getDeviceInfo() { + DispatchQueue.global(qos: .background).async { + var bsdName: String = .unavailable + var macAddr: String = .unavailable + var itlwmVer: String = .unavailable + var platformInfo = platform_info_t() + + if is_power_on() { + Log.debug("Wi-Fi powered on") + } else { + Log.debug("Wi-Fi powered off") + } + + if get_platform_info(&platformInfo) { + bsdName = String(cCharArray: platformInfo.device_info_str) + macAddr = NetworkManager.getMACAddressFromBSD(bsd: bsdName) ?? macAddr + itlwmVer = String(cCharArray: platformInfo.driver_info_str) + } + + DispatchQueue.main.async { + if let items = self as? StatusMenuItems { + items.setValueForItem(self.bsdItem, value: bsdName) + items.setValueForItem(self.macItem, value: macAddr) + items.setValueForItem(self.itlwmVerItem, value: itlwmVer) + } + } + + // If not connected, try to connect saved networks + var stationInfo = station_info_t() + var state: UInt32 = 0 + var power: Bool = false + get_power_state(&power) + if get_80211_state(&state) && power && + (state != ITL80211_S_RUN.rawValue || get_station_info(&stationInfo) != KERN_SUCCESS) { + NetworkManager.scanSavedNetworks() + } + } + } + + // - MARK: Action handlers + + @objc func clickMenuItem(_ sender: NSMenuItem) { + Log.debug("Clicked \(sender.title)") + switch sender.title { + case .createReport: + // Disable while bug report is being genetated, if autoenable == true, NSMenu ignores isEnable + createReportItem.action = nil + DispatchQueue.global(qos: .background).async { + BugReporter.generateBugReport() + DispatchQueue.main.async { + // Enable after generating report is finished + self.createReportItem.action = #selector(self.clickMenuItem(_:)) + } + } + case .Legacy.turnWiFiOn: + power_on() + case .Legacy.turnWiFiOff: + power_off() + case .Legacy.joinNetworks, .Modern.joinNetworks: + let joinPop = WiFiConfigWindow() + joinPop.show() + case .Legacy.createNetwork: + let alert = Alert(text: .notImplemented) + alert.show() + case .Legacy.openNetworkPrefs, .Modern.wifiSettings: + preferenceWindow.close() + preferenceWindow.show() + case .launchLogin: + LoginItemManager.setStatus(enabled: !LoginItemManager.isEnabled()) + isAutoLaunch = LoginItemManager.isEnabled() + case .aboutHeliport: + NSApplication.shared.orderFrontStandardAboutPanel() + NSApplication.shared.activate(ignoringOtherApps: true) + case .quitHeliport: + NSApp.terminate(nil) + default: + Log.error("Invalid menu item clicked") + } + } + + @objc private func updateStatus() { + DispatchQueue.global(qos: .background).async { + var powerState: Bool = false + let get_power_ret = get_power_state(&powerState) + var status: UInt32 = 0xFF + let get_state_ret = get_80211_state(&status) + + DispatchQueue.main.async { + if get_power_ret && get_state_ret { + self.isNetworkCardEnabled = powerState + } else { + Log.error("Failed get card state") + } + self.isNetworkCardAvailable = get_power_ret + self.driverState = itl_80211_state(rawValue: status) + self.updateStationItems() + } + } + } + + struct StationInfo { + var ssid: String? + var rssiValue: Int = 0 + var ipAddr: String = .unavailable + var routerAddr: String = .unavailable + var internet: String = .unavailable + var security: String = .unavailable + var bssid: String = .unavailable + var channel: String = .unavailable + var countryCode: String = .unavailable + var rssi: String = .unavailable + var noise: String = .unavailable + var txRate: String = .unavailable + var phyMode: String = .unavailable + var mcsIndex: String = .unavailable + var nss: String = .unavailable + var isNetworkConnected = false + } + + func updateStationItems() { + guard isNetworkCardEnabled else { return } + + DispatchQueue.global(qos: .background).async { + let info = self.getStationInfo() + + DispatchQueue.main.async { + self.setCurrentNetworkItem(with: info) + self.setStationItems(with: info) + } + } + } + + private func getStationInfo() -> StationInfo { + var infoOut = StationInfo() + var infoIn = station_info_t() + + guard driverState == ITL80211_S_RUN, + get_station_info(&infoIn) == KERN_SUCCESS else { + return infoOut + } + + infoOut.isNetworkConnected = true + infoOut.ssid = String(ssid: infoIn.ssid) + infoOut.rssiValue = Int(infoIn.rssi) + + guard showAllOptions else { return infoOut } + + var platformInfo = platform_info_t() + if get_platform_info(&platformInfo) { + let bsd = String(cCharArray: platformInfo.device_info_str) + infoOut.ipAddr = NetworkManager.getLocalAddress(bsd: bsd) ?? .unknown + infoOut.routerAddr = NetworkManager.getRouterAddress(bsd: bsd) ?? .unknown + } + + infoOut.internet = NetworkManager.isReachable() ? .reachable : .unreachable + infoOut.security = .unknown + infoOut.bssid = String(format: "%02x:%02x:%02x:%02x:%02x:%02x", + infoIn.bssid.0, + infoIn.bssid.1, + infoIn.bssid.2, + infoIn.bssid.3, + infoIn.bssid.4, + infoIn.bssid.5) + infoOut.channel = "\(infoIn.channel) (\(infoIn.channel <= 14 ? 2.4 : 5) GHz, \(infoIn.band_width) MHz)" + infoOut.countryCode = .unknown + infoOut.rssi = "\(infoIn.rssi) dBm" + infoOut.noise = "\(infoIn.noise) dBm" + infoOut.txRate = "\(infoIn.rate) Mbps" + infoOut.phyMode = infoIn.op_mode.description + infoOut.mcsIndex = "\(infoIn.cur_mcs)" + infoOut.nss = .unknown + + return infoOut + } + + func setStationItems(with info: StationInfo) { + guard showAllOptions, let items = self as? StatusMenuItems else { return } + + items.setValueForItem(self.ipAddresssItem, value: info.ipAddr) + items.setValueForItem(self.routerItem, value: info.routerAddr) + items.setValueForItem(self.internetItem, value: info.internet) + items.setValueForItem(self.securityItem, value: info.security) + items.setValueForItem(self.bssidItem, value: info.bssid) + items.setValueForItem(self.channelItem, value: info.channel) + items.setValueForItem(self.countryCodeItem, value: info.countryCode) + items.setValueForItem(self.rssiItem, value: info.rssi) + items.setValueForItem(self.noiseItem, value: info.noise) + items.setValueForItem(self.txRateItem, value: info.txRate) + items.setValueForItem(self.phyModeItem, value: info.phyMode) + items.setValueForItem(self.mcsIndexItem, value: info.mcsIndex) + items.setValueForItem(self.nssItem, value: info.nss) + } + + func setCurrentNetworkItem(with info: StationInfo) { + isNetworkConnected = info.isNetworkConnected + + guard isNetworkCardEnabled, + let wifiItemView = currentNetworkItem.view as? WifiMenuItemView else { return } + + // disconnected -> connected + if !wifiItemView.connected && info.isNetworkConnected { + for index in self.headerLength ..< self.items.count { + if let view = self.items[index].view as? WifiMenuItemView, + view.networkInfo.ssid == info.ssid { + self.items[index].isHidden = true + self.items[index].isEnabled = false + break + } + } + } + + currentNetworkItem.isHidden = !isNetworkConnected + wifiItemView.connected = isNetworkConnected + + guard isNetworkConnected else { return } + + isNetworkListEmpty = false + if let staSSID = info.ssid, wifiItemView.networkInfo.ssid != staSSID { + wifiItemView.networkInfo = NetworkInfo(ssid: staSSID, rssi: info.rssiValue) + } else { + wifiItemView.networkInfo.rssi = info.rssiValue + wifiItemView.updateImages() + } + } + + func processNetworkList(from infoList: [NetworkInfo], to itemList: inout [NSMenuItem], + insertAt: Int, _ staInfo: NetworkInfo?, hidden: Bool = false) { + var index = 0 + + for info in infoList { + var enabled = true + + if let staInfo, staInfo.ssid == info.ssid { + staInfo.auth.security = info.auth.security + (self.currentNetworkItem.view as? WifiMenuItemView)?.updateImages() + enabled = false + } + + if index < itemList.endIndex, let wifiMenuItemView = itemList[index].view as? WifiMenuItemView { + // Reuse existing item + itemList[index].isHidden = hidden || !enabled + itemList[index].isEnabled = enabled + wifiMenuItemView.networkInfo = info + } else { + // Add new item if not enough existing ones + let item = self.addNetworkItem(insertAt: insertAt + index, + hidden: hidden || !enabled, + networkInfo: info) + item.isEnabled = enabled + itemList.append(item) + } + + index += 1 + } + + // Hide extra items + for hideIndex in (index.. MAX_NETWORK_LIST_LENGTH { + Log.error("Number of scanned networks (\(networkList.count))" + + " exceeds maximum (\(MAX_NETWORK_LIST_LENGTH))") + } + + let staInfo: NetworkInfo? = (self.isNetworkConnected + ? (self.currentNetworkItem.view as? WifiMenuItemView)?.networkInfo + : nil) + + self.processNetworkList(from: networkList, to: &self.networkItemList, + insertAt: self.headerLength, staInfo) + } + } + + func toggleWIFI() { + DispatchQueue.main.async { + self.clickMenuItem(self.switchItem) + } + } + + @objc func disassociateSSID(_ sender: NSMenuItem) { + guard let ssid = sender.representedObject as? String else { return } + DispatchQueue.global().async { + CredentialsManager.instance.setAutoJoin(ssid, false) + dis_associate_ssid(ssid) + Log.debug("Disconnected from \(ssid)") + } + } + + // - MARK: Overrides + + override func addNetworkItem(_ item: NSMenuItem = SelectableMenuItem(), insertAt: Int? = nil, hidden: Bool = false, + networkInfo: NetworkInfo = NetworkInfo(ssid: "placeholder")) -> NSMenuItem { + item.view = WifiMenuItemViewLegacy(networkInfo: networkInfo) + + if let view = item.view as? WifiMenuItemView, let supView = view.superview { + NSLayoutConstraint.activate([ + view.leadingAnchor.constraint(equalTo: supView.leadingAnchor), + view.topAnchor.constraint(equalTo: supView.topAnchor), + view.trailingAnchor.constraint(greaterThanOrEqualTo: supView.trailingAnchor) + ]) + } + + return super.addNetworkItem(item, insertAt: insertAt, hidden: hidden, networkInfo: networkInfo) + } + + override func setCurrentNetworkItem(with info: StatusMenuBase.StationInfo) { + super.setCurrentNetworkItem(with: info) + guard isNetworkConnected, let ssid = info.ssid else { return } + + DispatchQueue.global(qos: .background).async { +#if !DEBUG + let autoJoin = CredentialsManager.instance.getStorageFromSsid(ssid)?.autoJoin ?? false +#else + let autoJoin = false +#endif + let hidden = autoJoin && !self.showAllOptions + DispatchQueue.main.async { + if !hidden { + self.disconnectItem.representedObject = ssid + self.disconnectItem.title = .Legacy.disconnectNet + ssid + } + self.disconnectItem.isHidden = hidden + } + } + } +} diff --git a/HeliPort/Appearance/StatusMenu/StatusMenuModern.swift b/HeliPort/Appearance/StatusMenu/StatusMenuModern.swift new file mode 100644 index 0000000..f427355 --- /dev/null +++ b/HeliPort/Appearance/StatusMenu/StatusMenuModern.swift @@ -0,0 +1,280 @@ +// +// StatusMenuModern.swift +// HeliPort +// +// Created by Bat.bat on 23/6/2024. +// Copyright © 2024 OpenIntelWireless. All rights reserved. +// + +/* + * This program and the accompanying materials are licensed and made available + * under the terms and conditions of the The 3-Clause BSD License + * which accompanies this distribution. The full text of the license may be found at + * https://opensource.org/licenses/BSD-3-Clause + */ + +import Cocoa + +@available(macOS 11, *) +final class StatusMenuModern: StatusMenuBase, StatusMenuItems { + + // - MARK: Menu items + + private lazy var statusItem: NSMenuItem = { + let item = NSMenuItem() + let view = StateSwitchMenuItemView(title: .Modern.wifi) { sender in + _ = sender.state == .on ? power_on() : power_off() + } + item.view = view + return item + }() + + private let knownSectionItem: NSMenuItem = { + let item = NSMenuItem() + item.view = SectionMenuItemView(title: .Modern.knownNetwork) + return item + }() + + private lazy var otherSectionItem: NSMenuItem = { + let item = SelectableMenuItem() + item.isHidden = true + item.view = SectionMenuItemView(title: .Modern.otherNetworks) { expand in + self.otherNetworkItemList.filter { $0.isEnabled } + .forEach { $0.isHidden = !expand } + self.manuallyJoinItem.isHidden = !expand + } + return item + }() + + private let manuallyJoinItem = NSMenuItem(title: .Modern.joinNetworks) + private let networkPanelItem = NSMenuItem(title: .Modern.wifiSettings) + + lazy var enabledNetworkCardItems: [NSMenuItem] = [] + + lazy var stationInfoItems: [NSMenuItem] = [ + ipAddresssItem, + routerItem, + internetItem, + securityItem, + bssidItem, + channelItem, + countryCodeItem, + rssiItem, + noiseItem, + txRateItem, + phyModeItem, + mcsIndexItem, + nssItem + ] + + lazy var hiddenItems: [NSMenuItem] = [ + bsdItem, + macItem, + itlwmVerItem, + enableLoggingItem, + createReportItem, + diagnoseItem, + hardwareInfoSeparator, + + toggleLaunchItem, + checkUpdateItem, + quitSeparator, + aboutItem, + quitItem + ] + + lazy var notImplementedItems: [NSMenuItem] = [ + enableLoggingItem, + diagnoseItem, + + securityItem, + countryCodeItem, + nssItem + ] + + override var isNetworkListEmpty: Bool { + willSet(empty) { + super.isNetworkListEmpty = empty + knownSectionItem.isHidden = empty + + guard empty else { return } + + otherSectionItem.isHidden = true + manuallyJoinItem.isHidden = true + + knownNetworkItemList.forEach { $0.isHidden = true } + otherNetworkItemList.forEach { $0.isHidden = true } + } + } + + override var isNetworkCardAvailable: Bool { + willSet(newState) { + super.isNetworkCardAvailable = newState + if !newState { (statusItem.view as? StateSwitchMenuItemView)?.isEnabled = false } + } + } + + override var isNetworkCardEnabled: Bool { + willSet(newState) { + (statusItem.view as? StateSwitchMenuItemView)?.state = newState + super.isNetworkCardEnabled = newState + } + } + + private var knownNetworkItemList = [NSMenuItem]() + private var otherNetworkItemList = [NSMenuItem]() + + // - MARK: Init + + override init() { + super.init() + minimumWidth = 300 + } + + required init(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // - MARK: Menu Setup + + func setupMenu() { + addItem(statusItem) + + [bsdItem, macItem, itlwmVerItem].forEach { + $0.view = KeyValueMenuItemView(key: $0.title, inset: .standard) + addItem($0) + } + + addItem(hardwareInfoSeparator) + + addClickItem(enableLoggingItem) + addClickItem(createReportItem) + addClickItem(diagnoseItem) + + addItem(.separator()) + addItem(knownSectionItem) + + _ = addNetworkItem(currentNetworkItem) + + stationInfoItems.forEach { + $0.view = KeyValueMenuItemView(key: $0.title, inset: .staInfo) + addItem($0) + } + + headerLength = items.count + + addItem(.separator()) + addItem(otherSectionItem) + + addClickItem(manuallyJoinItem) + addItem(networkItemListSeparator) + + addClickItem(networkPanelItem) + + addItem(.separator()) + + addClickItem(toggleLaunchItem) + addClickItem(checkUpdateItem) + addClickItem(aboutItem) + + addItem(quitSeparator) + addClickItem(quitItem) + } + + // - MARK: Menu Updates + + func setValueForItem(_ item: NSMenuItem, value: String) { + (item.view as? KeyValueMenuItemView)?.value = value + } + + func updateNetworkList() { + guard isNetworkCardEnabled else { return } + + NetworkManager.scanNetwork { knownList, otherList in + let networkListSize = knownList.count + otherList.count + if networkListSize > MAX_NETWORK_LIST_LENGTH { + Log.error("Number of scanned networks (\(networkListSize))" + + " exceeds maximum (\(MAX_NETWORK_LIST_LENGTH))") + } + + self.isNetworkListEmpty = networkListSize == 0 && !self.isNetworkConnected + self.knownSectionItem.isHidden = knownList.isEmpty && !self.isNetworkConnected + (self.knownSectionItem.view as? SectionMenuItemView)? + .title = (knownList.count > 1 ? .Modern.knownNetworks : .Modern.knownNetwork) + + if otherList.isEmpty { + self.manuallyJoinItem.isHidden = false + } else { + self.manuallyJoinItem.isHidden = !(self.otherSectionItem.view as? SectionMenuItemView)!.isExpanded + } + self.otherSectionItem.isHidden = otherList.isEmpty + + let staInfo: NetworkInfo? = (self.isNetworkConnected + ? (self.currentNetworkItem.view as? WifiMenuItemView)?.networkInfo + : nil) + + self.processNetworkList(from: knownList, to: &self.knownNetworkItemList, + insertAt: self.headerLength, staInfo) + self.processNetworkList(from: otherList, to: &self.otherNetworkItemList, + insertAt: (self.headerLength + self.knownNetworkItemList.count + + 2 /* separator + section header */), + staInfo, hidden: !(self.otherSectionItem.view as? SectionMenuItemView)!.isExpanded) + } + } + + func toggleWIFI() { + DispatchQueue.main.async { + (self.statusItem.view as? StateSwitchMenuItemView)?.toggle() + } + } + + // - MARK: Overrides + + override func menuWillOpen(_ menu: NSMenu) { + super.menuWillOpen(menu) + + guard isNetworkCardEnabled else { return } + (otherSectionItem.view as? SectionMenuItemView)? + .isExpanded = (!self.isNetworkConnected && self.knownNetworkItemList.isEmpty) + } + + override func addClickItem(_ item: NSMenuItem, target: AnyObject? = nil, action: Selector? = nil) { + let view = SelectableMenuItemView(height: .textModern, hoverStyle: .greytint) + let label: NSTextField = { + let label = NSTextField(labelWithString: item.title) + label.font = NSFont.menuFont(ofSize: 0) + label.textColor = .controlTextColor + return label + }() + + view.addSubview(label) + view.setupLayout() + label.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true + label.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 14).isActive = true + + item.view = view + + super.addClickItem(item, target: item.target, action: item.action) + } + + override func addNetworkItem(_ item: NSMenuItem = SelectableMenuItem(), insertAt: Int? = nil, hidden: Bool = false, + networkInfo: NetworkInfo = NetworkInfo(ssid: "placeholder")) -> NSMenuItem { + item.view = WifiMenuItemViewModern(networkInfo: networkInfo) + return super.addNetworkItem(item, insertAt: insertAt, hidden: hidden, networkInfo: networkInfo) + } + + override func setCurrentNetworkItem(with info: StatusMenuBase.StationInfo) { + // connected -> disconnected + if !currentNetworkItem.isHidden && !info.isNetworkConnected { + for index in self.headerLength ..< + min(self.items.count, + self.headerLength + self.knownNetworkItemList.count) + where self.items[index].view is WifiMenuItemView { + self.items[index].isHidden = false + self.items[index].isEnabled = true + } + } + + super.setCurrentNetworkItem(with: info) + } +} diff --git a/HeliPort/Bridge.h b/HeliPort/Bridge.h new file mode 100644 index 0000000..c5c3f54 --- /dev/null +++ b/HeliPort/Bridge.h @@ -0,0 +1,20 @@ +// +// Bridge.h +// HeliPort +// +// Created by Bat.bat on 8/7/2024. +// Copyright © 2024 OpenIntelWireless. All rights reserved. +// + +#pragma once + +// MARK: NSMenuItem Private API +#import + +@interface NSMenuItem () +- (BOOL)_canBeHighlighted; +@end + + +// MARK: itlwm API +#include "../ClientKit/Api.h" diff --git a/HeliPort/CredentialsManager.swift b/HeliPort/CredentialsManager.swift index 98ba258..66ad34d 100644 --- a/HeliPort/CredentialsManager.swift +++ b/HeliPort/CredentialsManager.swift @@ -20,8 +20,10 @@ final class CredentialsManager { static let instance: CredentialsManager = CredentialsManager() private let keychain: Keychain + private let ssidCache: NSCache = NSCache() + private let ssidCacheKey = NSString("savedSSIDs") - init() { + private init() { keychain = Keychain(service: Bundle.main.bundleIdentifier!) } @@ -35,6 +37,8 @@ final class CredentialsManager { return } + ssidCache.removeObject(forKey: ssidCacheKey) + Log.debug("Saving password for network \(network.ssid)") try? keychain.comment(entityJson).set(networkAuthJson, key: network.keychainKey) } @@ -119,6 +123,15 @@ final class CredentialsManager { } } + func getSavedNetworkSSIDs() -> Set { + if let cached = ssidCache.object(forKey: ssidCacheKey) as? Set { + return cached + } + let savedSSIDs = Set(keychain.allKeys()) + ssidCache.setObject(savedSSIDs as NSSet, forKey: ssidCacheKey) + return savedSSIDs + } + func getSavedNetworksEntity() -> [NetworkInfoStorageEntity] { return (keychain.allKeys().compactMap { ssid in return getStorageFromSsid(ssid) diff --git a/HeliPort/NetworkManager.swift b/HeliPort/NetworkManager.swift index 90384a4..0ae631a 100644 --- a/HeliPort/NetworkManager.swift +++ b/HeliPort/NetworkManager.swift @@ -1,5 +1,5 @@ // -// NetworkInfo.swift +// NetworkManager.swift // HeliPort // // Created by 梁怀宇 on 2020/3/23. @@ -14,7 +14,6 @@ */ import Foundation -import Cocoa import SystemConfiguration final class NetworkManager { @@ -78,7 +77,33 @@ final class NetworkManager { } } - static func scanNetwork(callback: @escaping (_ networkInfoList: [NetworkInfo]) -> Void) { + static func scanNetwork(sortBy areInIncreasingOrder: @escaping (NetworkInfo, NetworkInfo) -> Bool + = { $0.ssid < $1.ssid }, + callback: @escaping (_ sortedNetworkInfoList: [NetworkInfo]) -> Void) { + scanNetwork { result in + callback(result.sorted(by: areInIncreasingOrder)) + } + } + + static func scanNetwork(sortBy areInIncreasingOrder: @escaping (NetworkInfo, NetworkInfo) -> Bool + = { $0.ssid < $1.ssid }, + callback: @escaping (_ knownNetworks: [NetworkInfo], + _ otherNetworks: [NetworkInfo]) -> Void) { + DispatchQueue.global(qos: .background).async { + let savedSSIDs = CredentialsManager.instance.getSavedNetworkSSIDs() + scanNetwork { result in + let known = result.filter { savedSSIDs.contains($0.ssid) } + let other = result.subtracting(known) + + DispatchQueue.main.async { + callback(known.sorted(by: areInIncreasingOrder), + other.sorted(by: areInIncreasingOrder)) + } + } + } + } + + private static func scanNetwork(callback: @escaping (_ networkInfoList: Set) -> Void) { DispatchQueue.global(qos: .background).async { var list = network_info_list_t() get_network_list(&list) @@ -87,10 +112,10 @@ final class NetworkManager { let networks = Mirror(reflecting: list.networks).children.map({ $0.value }).prefix(Int(list.count)) for element in networks { - guard var network = element as? ioctl_network_info else { + guard let network = element as? ioctl_network_info else { continue } - let ssid = String.getSSIDFromCString(cString: &network.ssid.0) + let ssid = String(ssid: network.ssid) guard !ssid.isEmpty else { continue } @@ -104,7 +129,7 @@ final class NetworkManager { } DispatchQueue.main.async { - callback(Array(result).sorted { $0.ssid < $1.ssid }) + callback(result) } } } @@ -116,19 +141,15 @@ final class NetworkManager { Log.debug("No network saved for auto join") return } - var targetNetworks: [NetworkInfo]? - let dispatchSemaphore = DispatchSemaphore(value: 0) let scanTimer: Timer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true) { timer in NetworkManager.scanNetwork { networkList in - targetNetworks = savedNetworks.filter { networkList.contains($0) } - dispatchSemaphore.signal() - } - dispatchSemaphore.wait() - if targetNetworks != nil, targetNetworks!.count > 0 { - // This will stop the timer completely - timer.invalidate() - Log.debug("Auto join timer stopped") - connectSavedNetworks(networks: targetNetworks!) + let targetNetworks = savedNetworks.filter { networkList.contains($0) } + if targetNetworks.count > 0 { + // This will stop the timer completely + timer.invalidate() + Log.debug("Auto join timer stopped") + connectSavedNetworks(networks: targetNetworks) + } } } // Start executing code inside the timer immediately diff --git a/HeliPort/Supporting files/NSImage+Extensions.swift b/HeliPort/Supporting files/NSImage+Extensions.swift new file mode 100644 index 0000000..db8d4cb --- /dev/null +++ b/HeliPort/Supporting files/NSImage+Extensions.swift @@ -0,0 +1,23 @@ +// +// NSImage+Extensions.swift +// HeliPort +// +// Created by Bat.bat on 19/6/2024. +// Copyright © 2024 OpenIntelWireless. All rights reserved. +// + +/* + * This program and the accompanying materials are licensed and made available + * under the terms and conditions of the The 3-Clause BSD License + * which accompanies this distribution. The full text of the license may be found at + * https://opensource.org/licenses/BSD-3-Clause + */ + +import Cocoa + +extension NSImage { + @available(macOS 11.0, *) + public convenience init?(systemSymbolName name: String) { + self.init(systemSymbolName: name, accessibilityDescription: nil) + } +} diff --git a/HeliPort/Supporting files/NSMenuItem+Extensions.swift b/HeliPort/Supporting files/NSMenuItem+Extensions.swift index 7a39cf1..a2f33e9 100644 --- a/HeliPort/Supporting files/NSMenuItem+Extensions.swift +++ b/HeliPort/Supporting files/NSMenuItem+Extensions.swift @@ -17,6 +17,12 @@ import Foundation import Cocoa extension NSMenuItem { + enum ItemHeight: CGFloat { + case textLegacy = 19 + case textModern = 22 + case networkModern = 32 + } + convenience init(title: String) { self.init(title: title, action: nil, keyEquivalent: "") } diff --git a/HeliPort/Supporting files/String+Extensions.swift b/HeliPort/Supporting files/String+Extensions.swift index df6aa42..d5195c3 100644 --- a/HeliPort/Supporting files/String+Extensions.swift +++ b/HeliPort/Supporting files/String+Extensions.swift @@ -20,25 +20,12 @@ public extension String { static let legacyUI = "legacyUIEnabled" } - static func getSSIDFromCString(cString: UnsafePointer) -> String { - var string = String(cString: cString) - // Fixes memory leak, see https://stackoverflow.com/a/37584615 - autoreleasepool { - string = string.trimmingCharacters(in: .whitespacesAndNewlines) - .replacingOccurrences(of: "[\n,\r]*", - with: "", - options: .regularExpression) - if string.count > NWID_LEN { - let pointer = UnsafeRawPointer(cString) - let nsString = NSString(bytes: pointer, length: Int(NWID_LEN), encoding: Encoding.utf8.rawValue) - if let nsString = nsString { - string = nsString as String - } else { - string = "\(string.prefix(Int(NWID_LEN)))" - } - } - } - return string + init(ssid: T) { + self = withUnsafeBytes(of: ssid) { + String(decoding: $0.prefix(Int(NWID_LEN)), as: UTF8.self) + }.trimmingCharacters(in: .whitespaces) + .replacingOccurrences(of: "\0", with: "") + self.unicodeScalars.removeAll(where: { CharacterSet.newlines.contains($0) }) } init(cCharArray: T) {