Files
Pulse/Sources/PulseUI/Features/Console/ConsoleTableView-macos.swift
2022-08-12 18:28:33 -04:00

179 lines
5.9 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// The MIT License (MIT)
//
// Copyright (c) 20202022 Alexander Grebenyuk (github.com/kean).
#if os(macOS)
import SwiftUI
import Pulse
import CoreData
import Combine
import AppKit
final class ConsoleTableViewModel: ObservableObject {
let searchCriteriaViewModel: ConsoleSearchCriteriaViewModel?
var diff: CollectionDifference<NSManagedObjectID>?
@Published var entities: [NSManagedObject] = []
init(searchCriteriaViewModel: ConsoleSearchCriteriaViewModel?) {
self.searchCriteriaViewModel = searchCriteriaViewModel
}
}
/// Using this because of the following List issues:
/// - Reload performance issues
/// - NavigationLink popped when cell disappears
/// - List doesn't keep scroll position when reloaded
struct ConsoleTableView: NSViewRepresentable {
let viewModel: ConsoleTableViewModel
let onSelected: (NSManagedObject?) -> Void
final class Coordinator: NSObject, NSTableViewDelegate, NSTableViewDataSource {
private let viewModel: ConsoleTableViewModel
private var colorPrimary = NSColor.labelColor
private var colorSecondary = NSColor.secondaryLabelColor
private var colorOrange = NSColor.systemOrange
private var colorRed = Palette.red
var entities: [NSManagedObject] = []
var cancellables: [AnyCancellable] = []
func color(for level: LoggerStore.Level) -> NSColor {
switch level {
case .trace: return colorSecondary
case .debug, .info: return colorPrimary
case .notice, .warning: return colorOrange
case .error, .critical: return colorRed
}
}
init(viewModel: ConsoleTableViewModel) {
self.viewModel = viewModel
}
func numberOfRows(in tableView: NSTableView) -> Int {
entities.count
}
func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat {
30
}
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
let cell = HostingTableCell.make(in: tableView)
switch entities[row] {
case let message as LoggerMessageEntity:
if let task = message.task {
cell.hostingView.rootView = AnyView(ConsoleNetworkRequestView(viewModel: .init(task: task)))
} else {
cell.hostingView.rootView = AnyView(ConsoleMessageView(viewModel: .init(message: message)))
}
case let task as NetworkTaskEntity:
cell.hostingView.rootView = AnyView(ConsoleNetworkRequestView(viewModel: .init(task: task)))
default:
fatalError("Invalid entity: \(entities[row])")
}
cell.hostingView.invalidateIntrinsicContentSize()
return cell
}
}
func makeCoordinator() -> Coordinator {
Coordinator(viewModel: viewModel)
}
func makeNSView(context: Context) -> NSScrollView {
let tableView = NSTableView()
let column = NSTableColumn(identifier: .init(rawValue: "first"))
tableView.addTableColumn(column)
tableView.headerView = nil
let coordinator = context.coordinator
tableView.delegate = coordinator
tableView.dataSource = coordinator
tableView.target = coordinator
tableView.style = .sourceList
tableView.usesAutomaticRowHeights = true
let scrollView = NSScrollView()
scrollView.hasVerticalScroller = true
scrollView.hasHorizontalRuler = true
scrollView.autohidesScrollers = true
scrollView.documentView = tableView
var isFirstReload = true
viewModel.$entities.sink { [viewModel, tableView] in
coordinator.entities = $0
if let diff = viewModel.diff, !isFirstReload {
viewModel.diff = nil
tableView.apply(diff: diff)
} else {
tableView.reloadData()
}
isFirstReload = false
}.store(in: &context.coordinator.cancellables)
NotificationCenter.default.publisher(for: NSTableView.selectionDidChangeNotification, object: tableView).sink { [viewModel] in
guard let table = $0.object as? NSTableView else { return }
if viewModel.entities.indices.contains(tableView.selectedRow) {
self.onSelected(viewModel.entities[table.selectedRow])
} else {
self.onSelected(nil)
}
}.store(in: &context.coordinator.cancellables)
return scrollView
}
func updateNSView(_ nsView: NSScrollView, context: Context) {
// Do nothing
}
}
final class HostingTableCell: NSTableCellView {
let hostingView = NSHostingView(rootView: AnyView(EmptyView()))
private let label = NSTextField.label()
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
addSubview(hostingView)
hostingView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
hostingView.leadingAnchor.constraint(equalTo: leadingAnchor),
hostingView.trailingAnchor.constraint(equalTo: trailingAnchor),
hostingView.topAnchor.constraint(equalTo: topAnchor, constant: 3),
hostingView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -3)
])
}
override func layout() {
super.layout()
label.sizeToFit()
label.frame.origin = CGPoint(x: 2, y: 2)
}
required init?(coder: NSCoder) {
fatalError("Not implemented")
}
static func make(in tableView: NSTableView) -> HostingTableCell {
let id = NSUserInterfaceItemIdentifier(rawValue: "HostingTableCell")
if let view = tableView.makeView(withIdentifier: id, owner: nil) as? HostingTableCell {
return view
}
let view = HostingTableCell()
view.identifier = id
return view
}
}
#endif