From 9a0175e6697e23e9b7ac2c9b5e7d3f331d1fe752 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ng=C3=B4=20Qu=E1=BB=91c=20=C4=90=E1=BA=A1t?= Date: Fri, 22 May 2026 14:27:08 +0700 Subject: [PATCH] fix(datagrid): crash adding a row while a cell overlay is open (#1378) --- CHANGELOG.md | 4 ++ .../Views/Results/KeyHandlingTableView.swift | 15 +++--- .../KeyHandlingTableViewOverlayTests.swift | 51 +++++++++++++++++++ 3 files changed, 62 insertions(+), 8 deletions(-) create mode 100644 TableProTests/Views/Results/KeyHandlingTableViewOverlayTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index b43ba266d..fddd0eb4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- Fixed a crash when adding a row while a cell editor or value viewer was open (#1378) + ## [0.43.2] - 2026-05-22 ### Changed diff --git a/TablePro/Views/Results/KeyHandlingTableView.swift b/TablePro/Views/Results/KeyHandlingTableView.swift index c8a807f5b..54822f472 100644 --- a/TablePro/Views/Results/KeyHandlingTableView.swift +++ b/TablePro/Views/Results/KeyHandlingTableView.swift @@ -3,8 +3,7 @@ import AppKit final class KeyHandlingTableView: NSTableView { weak var coordinator: TableViewCoordinator? - private var isRaisingOverlayEditor = false - private var isRaisingOverlayViewer = false + private var isRaisingOverlay = false override var acceptsFirstResponder: Bool { true @@ -12,21 +11,21 @@ final class KeyHandlingTableView: NSTableView { override func didAddSubview(_ subview: NSView) { super.didAddSubview(subview) - raiseOverlayIfNeeded(coordinator?.overlayEditor, subview: subview, flag: &isRaisingOverlayEditor) - raiseOverlayIfNeeded(coordinator?.overlayViewer, subview: subview, flag: &isRaisingOverlayViewer) + guard !isRaisingOverlay else { return } + isRaisingOverlay = true + defer { isRaisingOverlay = false } + raiseOverlayIfNeeded(coordinator?.overlayEditor, subview: subview) + raiseOverlayIfNeeded(coordinator?.overlayViewer, subview: subview) } - private func raiseOverlayIfNeeded(_ overlay: CellOverlayBase?, subview: NSView, flag: inout Bool) { - guard !flag else { return } + private func raiseOverlayIfNeeded(_ overlay: CellOverlayBase?, subview: NSView) { guard let overlay, overlay.isActive, let container = overlay.containerView, container !== subview, container.superview === self, subviews.last !== container else { return } - flag = true overlay.raiseToFront() - flag = false } var selection = TableSelection() { diff --git a/TableProTests/Views/Results/KeyHandlingTableViewOverlayTests.swift b/TableProTests/Views/Results/KeyHandlingTableViewOverlayTests.swift new file mode 100644 index 000000000..273009ff5 --- /dev/null +++ b/TableProTests/Views/Results/KeyHandlingTableViewOverlayTests.swift @@ -0,0 +1,51 @@ +// +// KeyHandlingTableViewOverlayTests.swift +// TableProTests +// + +import AppKit +import Foundation +import SwiftUI +import TableProPluginKit +import Testing + +@testable import TablePro + +@MainActor +private final class StubColumnLayoutPersister: ColumnLayoutPersisting { + func load(for tableName: String, connectionId: UUID) -> ColumnLayoutState? { nil } + func save(_ layout: ColumnLayoutState, for tableName: String, connectionId: UUID) {} + func clear(for tableName: String, connectionId: UUID) {} +} + +@Suite("KeyHandlingTableView overlay raise") +@MainActor +struct KeyHandlingTableViewOverlayTests { + private func makeCoordinator() -> TableViewCoordinator { + TableViewCoordinator( + changeManager: AnyChangeManager(DataChangeManager()), + isEditable: true, + selectedRowIndices: .constant([]), + delegate: nil, + layoutPersister: StubColumnLayoutPersister() + ) + } + + @Test("adding a subview while an overlay is active raises it to front without trapping") + func addingSubviewRaisesActiveOverlay() { + let tableView = KeyHandlingTableView() + let coordinator = makeCoordinator() + tableView.coordinator = coordinator + + let editor = CellOverlayEditor() + coordinator.overlayEditor = editor + let container = CellOverlayContainerView(frame: NSRect(x: 0, y: 0, width: 80, height: 24)) + editor.install(in: tableView, row: 0, column: 0, columnIndex: 0, container: container) + + tableView.addSubview(NSView(frame: NSRect(x: 0, y: 0, width: 10, height: 10))) + + #expect(tableView.subviews.last === container) + + editor.removeOverlay() + } +}