mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-11-08 18:04:51 +00:00
App/ 根目录原有 73 个散落源文件,已无法从目录读出任何分组信息。 按「目录即分层」重组为 Composition / Application / Presentation / UI / PlatformEffects / Resources 六个目录,UI 层再按子视图分目录(Workspace、 Home、Servers(含 Plugin)、Subscriptions、Rules、Settings、Diagnostics、 StatusMenu)。纯移动:类型名、模块名与 import 均未改动,App/ 根层不再有 散落文件。 配套改动: - project.yml:Core 测试镜像的 excludes 由裸文件名改为 `**/` 递归形式。 XcodeGen 的 exclude 不跨 `/`,裸名只匹配 App 根层;MainApp.swift 下移后 旧写法会静默失效,把 @main 组合根编进测试镜像。info.path 与 entitlements.path 指向 App/Resources/;并排除 `**/*.md`——App/ 下任何 非源码文件都会被 XcodeGen 收进 Resources 阶段并封进签名后的 bundle。 - SkeletonTests 的架构守卫由「默认扫描全部 App/ + 7 个豁免文件 + 1 个豁免 前缀 + 4 个豁免目录」改为正面圈定 App/UI/,豁免登记全部删除;新增运行时 适配器放进 Application/ 即自动落在扫描范围外,无需再登记。 - README 工程结构说明同步;根 .gitignore 的生成物路径随 Resources/ 归位。 验证:clean build 0 error;task test 1022 用例全通过(含两个架构守卫); task format 无改动(证明纯移动);task lint 与移动前同为 21 条内容型 warning;packaging-gate.sh ALL CHECKS PASSED,确认 plist/entitlements 归位 后签名与 entitlements 语义不变。 注意:迁移时须先删旧的生成物再 xcodegen generate,否则遗留的 App/Info.plist 会被当作普通资源扫进 Resources 阶段。
92 lines
3.7 KiB
Swift
92 lines
3.7 KiB
Swift
import AppKit
|
|
import SwiftUI
|
|
|
|
/// 关掉系统给密码输入框的「密码自动填充」入口。
|
|
///
|
|
/// SwiftUI 的 `SecureField` 在 macOS 上由 `NSSecureTextField` 承载;系统对安全
|
|
/// 输入框默认提供钥匙串/密码 App 的自动填充入口。`textContentType(nil)` 只清掉
|
|
/// 字段语义(实测底层字段 `contentType` 已是 nil),关不掉入口;AppKit 没有公开
|
|
/// 开关,唯一开关是字段级的私有 `_setPasswordAutofillEnabled:`(默认开)。这里在
|
|
/// 探针认领到的文本框上按需调用它:方法不存在(系统换实现)时保持系统默认
|
|
/// 行为,不做替代实现,也不让 KVC 抛异常。
|
|
///
|
|
/// 用法:贴在密码控件(`Group { TextField / SecureField }`)的 `.background(...)`
|
|
/// 上。探针与控件同帧,据此在祖先里认领同帧的文本框,因此同一表单里的其他
|
|
/// 输入框不会被误伤。
|
|
struct PasswordAutofillOptOut: NSViewRepresentable {
|
|
func makeNSView(context: Context) -> ProbeView {
|
|
ProbeView()
|
|
}
|
|
|
|
func updateNSView(_ view: ProbeView, context: Context) {
|
|
view.disablePasswordAutofill()
|
|
}
|
|
|
|
/// 背景探针:由 `.background` 撑满密码控件,据此认领承载控件的文本框。
|
|
final class ProbeView: NSView {
|
|
/// 已处理过的文本框:明文/密文切换会换成新实例,换实例即重新处理。
|
|
private weak var claimedField: NSTextField?
|
|
|
|
private static let setterSelector = NSSelectorFromString("_setPasswordAutofillEnabled:")
|
|
|
|
override func viewDidMoveToWindow() {
|
|
super.viewDidMoveToWindow()
|
|
disablePasswordAutofill()
|
|
}
|
|
|
|
override func setFrameSize(_ newSize: NSSize) {
|
|
super.setFrameSize(newSize)
|
|
disablePasswordAutofill()
|
|
}
|
|
|
|
override func setFrameOrigin(_ newOrigin: NSPoint) {
|
|
super.setFrameOrigin(newOrigin)
|
|
disablePasswordAutofill()
|
|
}
|
|
|
|
func disablePasswordAutofill() {
|
|
let target = convert(bounds, to: nil)
|
|
guard window != nil, !target.isEmpty else { return }
|
|
guard let field = Self.field(overlapping: target, from: superview) else { return }
|
|
guard field !== claimedField else { return }
|
|
claimedField = field
|
|
guard field.responds(to: Self.setterSelector) else { return }
|
|
field.setValue(false, forKey: "passwordAutofillEnabled")
|
|
}
|
|
|
|
/// 逐层向上找第一个含候选文本框的祖先视图。按交叠面积认领而不是逐边
|
|
/// 相等:探针与控件同帧,取整差异不应该让认领落空。
|
|
private static func field(overlapping target: NSRect, from view: NSView?) -> NSTextField? {
|
|
var ancestor = view
|
|
while let current = ancestor {
|
|
if let match = bestMatch(in: current, target: target) { return match.field }
|
|
ancestor = current.superview
|
|
}
|
|
return nil
|
|
}
|
|
|
|
private static func bestMatch(
|
|
in view: NSView, target: NSRect
|
|
) -> (field: NSTextField, area: CGFloat)? {
|
|
var best: (field: NSTextField, area: CGFloat)?
|
|
if let field = view as? NSTextField, field.window != nil,
|
|
let area = intersectionArea(of: field, with: target), area > 0
|
|
{
|
|
best = (field, area)
|
|
}
|
|
for subview in view.subviews {
|
|
guard let candidate = bestMatch(in: subview, target: target) else { continue }
|
|
if candidate.area > (best?.area ?? 0) { best = candidate }
|
|
}
|
|
return best
|
|
}
|
|
|
|
private static func intersectionArea(of field: NSTextField, with target: NSRect) -> CGFloat? {
|
|
let rect = field.convert(field.bounds, to: nil)
|
|
let intersection = rect.intersection(target)
|
|
guard !intersection.isNull else { return nil }
|
|
return intersection.width * intersection.height
|
|
}
|
|
}
|
|
}
|