A per-cell context menu intercepts the primary click in a macOS Table, so rows rarely got selected. Make the cells passive and move the address menu to a table-level contextMenu(forSelectionType:), reusing a shared AddressMenuItems.
167 lines
4.9 KiB
Swift
167 lines
4.9 KiB
Swift
import LumaCore
|
|
import SwiftUI
|
|
|
|
struct PointerValueText: View {
|
|
let engine: Engine
|
|
let sessionID: UUID
|
|
let value: String
|
|
let address: UInt64
|
|
var context: AddressContext = AddressContext()
|
|
@Binding var selection: SidebarItemID?
|
|
|
|
var body: some View {
|
|
Text(value)
|
|
.font(.system(.body, design: .monospaced))
|
|
.pointerActions(
|
|
engine: engine,
|
|
sessionID: sessionID,
|
|
value: value,
|
|
address: address,
|
|
context: context,
|
|
selection: $selection
|
|
)
|
|
}
|
|
}
|
|
|
|
extension View {
|
|
func pointerActions<Extra: View>(
|
|
engine: Engine,
|
|
sessionID: UUID,
|
|
value: String,
|
|
address: UInt64,
|
|
context: AddressContext = AddressContext(),
|
|
copyTitle: String = "Copy",
|
|
selection: Binding<SidebarItemID?>,
|
|
@ViewBuilder extraItems: @escaping () -> Extra = { EmptyView() }
|
|
) -> some View {
|
|
modifier(
|
|
PointerActions(
|
|
engine: engine,
|
|
sessionID: sessionID,
|
|
value: value,
|
|
address: address,
|
|
context: context,
|
|
copyTitle: copyTitle,
|
|
selection: selection,
|
|
extraItems: extraItems
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
private struct PointerActions<Extra: View>: ViewModifier {
|
|
let engine: Engine
|
|
let sessionID: UUID
|
|
let value: String
|
|
let address: UInt64
|
|
let context: AddressContext
|
|
let copyTitle: String
|
|
@Binding var selection: SidebarItemID?
|
|
@ViewBuilder let extraItems: () -> Extra
|
|
|
|
@State private var facts: AddressFacts?
|
|
|
|
func body(content: Content) -> some View {
|
|
content
|
|
.textSelection(.disabled)
|
|
.onHover { hovering in
|
|
guard hovering, facts == nil else { return }
|
|
Task { facts = await engine.addressFacts(sessionID: sessionID, address: address, context: context) }
|
|
}
|
|
.contextMenu {
|
|
AddressMenuItems(
|
|
engine: engine,
|
|
sessionID: sessionID,
|
|
value: value,
|
|
address: address,
|
|
context: context,
|
|
copyTitle: copyTitle,
|
|
facts: facts,
|
|
selection: $selection,
|
|
extraItems: extraItems
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
struct AddressMenuItems<Extra: View>: View {
|
|
let engine: Engine
|
|
let sessionID: UUID
|
|
let value: String
|
|
let address: UInt64
|
|
let context: AddressContext
|
|
var copyTitle: String = "Copy"
|
|
let facts: AddressFacts?
|
|
@Binding var selection: SidebarItemID?
|
|
@ViewBuilder var extraItems: () -> Extra
|
|
|
|
var body: some View {
|
|
Button {
|
|
Platform.copyToClipboard(value)
|
|
} label: {
|
|
Label(copyTitle, systemImage: "doc.on.doc")
|
|
}
|
|
|
|
if let facts {
|
|
if facts.mapping == .executable {
|
|
inspectorButton("Open Disassembly", systemImage: "hammer", kind: .disassembly)
|
|
}
|
|
if facts.mapping != .unmapped {
|
|
inspectorButton("Open Memory", systemImage: "doc.text.magnifyingglass", kind: .memory)
|
|
}
|
|
|
|
let actions = engine.addressActions(sessionID: sessionID, address: address, context: context, facts: facts)
|
|
if !actions.isEmpty {
|
|
Divider()
|
|
ForEach(actions) { action in
|
|
actionButton(action)
|
|
}
|
|
}
|
|
} else {
|
|
Divider()
|
|
Button {} label: {
|
|
Label("Analyzing\u{2026}", systemImage: "hourglass")
|
|
}
|
|
.disabled(true)
|
|
}
|
|
|
|
extraItems()
|
|
}
|
|
|
|
private func inspectorButton(_ title: String, systemImage: String, kind: LumaCore.AddressInsight.Kind) -> some View {
|
|
Button {
|
|
openInsight(kind: kind)
|
|
} label: {
|
|
Label(title, systemImage: systemImage)
|
|
}
|
|
}
|
|
|
|
private func actionButton(_ action: AddressAction) -> some View {
|
|
Button(role: action.role == .destructive ? .destructive : nil) {
|
|
Task { @MainActor in
|
|
if let target = await action.perform() {
|
|
selection = SidebarItemID(navigationTarget: target)
|
|
}
|
|
}
|
|
} label: {
|
|
if let icon = action.systemImage {
|
|
Label(action.title, systemImage: icon)
|
|
} else {
|
|
Text(action.title)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func openInsight(kind: LumaCore.AddressInsight.Kind) {
|
|
Task { @MainActor in
|
|
guard let insight = try? engine.getOrCreateInsight(
|
|
sessionID: sessionID,
|
|
pointer: address,
|
|
kind: kind,
|
|
preferredAnchor: context.anchorHint
|
|
) else { return }
|
|
selection = .insight(sessionID, insight.id)
|
|
}
|
|
}
|
|
}
|