mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-11-12 22:35:05 +00:00
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.
80 lines
2.6 KiB
Swift
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
|
|
}
|
|
}
|
|
}
|