Files
ShadowsocksX-NG/ShadowsocksX-NG2/App/ServerTreeBlankClickObserver.swift
T
Qiu Yuzhou 535d62a28a fix(servers): support root creation and clearing selection
Add explicit current-group and root creation menus. Clear the management
tree selection on blank clicks or Escape while focused. Capture creation
parents, retain failed group drafts, and select/reveal created nodes.

Validation: 870 hostless tests (921 executions), 21 Python checks, unsigned
app build, format/lint/diff checks. Standards and spec reviews passed.
Native interaction and visual verification were not performed per request.
2026-10-04 21:36:51 +08:00

80 lines
2.6 KiB
Swift

import SwiftUI
/// List 没有空白点击回调;用原生表格的行命中判断,不拦截行选择、按钮或滚动条。
struct ServerTreeBlankClickObserver: NSViewRepresentable {
let onBlankClick: () -> Void
func makeNSView(context: Context) -> ObserverView {
let view = ObserverView()
view.onBlankClick = onBlankClick
return view
}
func updateNSView(_ view: ObserverView, context: Context) {
view.onBlankClick = onBlankClick
}
static func dismantleNSView(_ view: ObserverView, coordinator: ()) {
view.stopObserving()
}
final class ObserverView: NSView {
var onBlankClick: (() -> Void)?
private var monitor: Any?
override func hitTest(_ point: NSPoint) -> NSView? { nil }
override func viewDidMoveToWindow() {
super.viewDidMoveToWindow()
stopObserving()
guard window != nil else { return }
monitor = NSEvent.addLocalMonitorForEvents(matching: .leftMouseDown) { [weak self] event in
self?.handleClick(event)
return event
}
}
func stopObserving() {
if let monitor { NSEvent.removeMonitor(monitor) }
monitor = nil
}
private func handleClick(_ event: NSEvent) {
guard let window, event.window === window,
visibleRect.contains(convert(event.locationInWindow, from: nil))
else { return }
// 背景探针与 List 的原生滚动视图是兄弟;向上寻找共同容器,
// 只接受点击点处的表格,排除滚动条和其他列表。
var container = superview
while let current = container {
if let table = table(at: event.locationInWindow, in: current) {
// 覆盖式滚动条可能落在表格几何范围内,实际命中必须属于表格。
guard let content = window.contentView else { return }
let hitPoint =
content.superview?.convert(event.locationInWindow, from: nil)
?? event.locationInWindow
var hit = content.hitTest(hitPoint)
while let view = hit, view !== table { hit = view.superview }
guard hit === table else { return }
let point = table.convert(event.locationInWindow, from: nil)
if table.row(at: point) == -1 { onBlankClick?() }
return
}
container = current.superview
}
}
private func table(at point: NSPoint, in view: NSView) -> NSTableView? {
if let table = view as? NSTableView,
table.visibleRect.contains(table.convert(point, from: nil))
{
return table
}
for child in view.subviews {
if let table = table(at: point, in: child) { return table }
}
return nil
}
}
}