diff --git a/docs/SMOKE_TEST.md b/docs/SMOKE_TEST.md index a3fe144..0a32606 100644 --- a/docs/SMOKE_TEST.md +++ b/docs/SMOKE_TEST.md @@ -55,6 +55,7 @@ Use this checklist before a release or after changes to panel, pasteboard, setti 22. Drag an unassigned card onto the renamed collection chip and confirm the chip count increases and the card appears when that collection is selected. 23. Resize or test on a narrow display and confirm the bottom shelf switches to compact cards that still show two recent clips cleanly. 24. Select a file, rich text, or URL card and confirm the selected-card rail exposes `Paste Plain Text`, the corner source/kind badge remains visible, and on a narrow shelf secondary actions collapse behind `More` instead of overflowing the card. +25. Confirm card footers do not show `Unknown` for clips without a source app, and confirm used clips show their usage count beside the source app. ## Copy And Paste diff --git a/sources/clipbored/views/ClipboardPanelView.swift b/sources/clipbored/views/ClipboardPanelView.swift index 25ea115..70b2094 100644 --- a/sources/clipbored/views/ClipboardPanelView.swift +++ b/sources/clipbored/views/ClipboardPanelView.swift @@ -1370,6 +1370,14 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate { cardViews.first?.debugFooterDetailText ?? "" } + var debugFirstCardFooterSourceText: String { + cardViews.first?.debugFooterSourceText ?? "" + } + + var debugFirstCardFooterSourceIsHidden: Bool { + cardViews.first?.debugFooterSourceIsHidden ?? true + } + var debugQuickPasteBadgeTexts: [String] { cardViews.compactMap(\.debugQuickPasteBadgeText) } @@ -2401,6 +2409,7 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource { private let activeCollectionColor: NSColor? private let collectionNames: [String] private let contentView = NSView() + private let footerSourceLabel = NSTextField(labelWithString: "") private let footerDetailLabel = NSTextField(labelWithString: "") private let actionRail = NSStackView() private var actionRailButtons: [NSButton] = [] @@ -2697,6 +2706,14 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource { footerDetailLabel.stringValue } + var debugFooterSourceText: String { + footerSourceLabel.stringValue + } + + var debugFooterSourceIsHidden: Bool { + footerSourceLabel.isHidden + } + var debugItemID: UUID { itemID } @@ -3786,12 +3803,14 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource { footer.layer?.backgroundColor = Palette.footerBackground footer.heightAnchor.constraint(equalToConstant: layout.footerHeight).isActive = true - let source = NSTextField(labelWithString: sourceText(for: item)) - source.font = .systemFont(ofSize: NSFont.smallSystemFontSize, weight: .medium) - source.textColor = .secondaryLabelColor - source.lineBreakMode = .byTruncatingTail - source.maximumNumberOfLines = 1 - source.toolTip = source.stringValue + let sourceText = footerSourceText(for: item) + footerSourceLabel.stringValue = sourceText ?? "" + footerSourceLabel.font = .systemFont(ofSize: NSFont.smallSystemFontSize, weight: .medium) + footerSourceLabel.textColor = .secondaryLabelColor + footerSourceLabel.lineBreakMode = .byTruncatingTail + footerSourceLabel.maximumNumberOfLines = 1 + footerSourceLabel.toolTip = footerSourceLabel.stringValue + footerSourceLabel.isHidden = sourceText == nil let detailText = detailMetricText(for: item) if activeCollectionName == nil, @@ -3814,12 +3833,12 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource { divider.wantsLayer = true divider.layer?.backgroundColor = Palette.divider divider.translatesAutoresizingMaskIntoConstraints = false - let stack = row([source, footerDetailLabel]) + let stack = row([footerSourceLabel, footerDetailLabel]) stack.distribution = .fill stack.alignment = .centerY stack.translatesAutoresizingMaskIntoConstraints = false - source.setContentHuggingPriority(.defaultLow, for: .horizontal) - source.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + footerSourceLabel.setContentHuggingPriority(.defaultLow, for: .horizontal) + footerSourceLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) footerDetailLabel.setContentHuggingPriority(.required, for: .horizontal) footerDetailLabel.setContentCompressionResistancePriority(.required, for: .horizontal) footer.addSubview(divider) @@ -3989,8 +4008,21 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource { } } - private func sourceText(for item: ClipboardItem) -> String { - item.sourceApp?.clipboardTrimmed.isEmpty == false ? item.sourceApp! : "Unknown" + private func footerSourceText(for item: ClipboardItem) -> String? { + let source = item.sourceApp?.clipboardTrimmed + let usage = usageText(for: item.useCount) + if let source, !source.isEmpty, let usage { + return "\(source) - \(usage)" + } + if let source, !source.isEmpty { + return source + } + return usage + } + + private func usageText(for useCount: Int) -> String? { + guard useCount > 0 else { return nil } + return useCount == 1 ? "Used once" : "Used \(useCount) times" } private func linkTitle(for item: ClipboardItem) -> String { diff --git a/tests/clipboredtests/ClipboardPanelViewTests.swift b/tests/clipboredtests/ClipboardPanelViewTests.swift index 7e2edba..04c2053 100644 --- a/tests/clipboredtests/ClipboardPanelViewTests.swift +++ b/tests/clipboredtests/ClipboardPanelViewTests.swift @@ -106,6 +106,33 @@ final class ClipboardPanelViewTests: XCTestCase { XCTAssertFalse(fixture.view.debugStatusText.contains("Enter paste")) } + func testCardFooterHidesMissingSourceInsteadOfShowingUnknown() { + let fixture = makePanelFixture() + var item = makeTextItem("No source noise", store: fixture.store) + item.sourceApp = nil + + fixture.store.upsert(item) + drainMainQueue() + fixture.window.contentView?.layoutSubtreeIfNeeded() + + XCTAssertTrue(fixture.view.debugFirstCardFooterSourceIsHidden) + XCTAssertEqual(fixture.view.debugFirstCardFooterSourceText, "") + XCTAssertEqual(fixture.view.debugFirstCardFooterDetailText, "15 characters") + } + + func testCardFooterShowsSourceAndUsageWhenUsed() { + let fixture = makePanelFixture() + var item = makeTextItem("Frequently pasted", store: fixture.store) + item.useCount = 3 + + fixture.store.upsert(item) + drainMainQueue() + fixture.window.contentView?.layoutSubtreeIfNeeded() + + XCTAssertFalse(fixture.view.debugFirstCardFooterSourceIsHidden) + XCTAssertEqual(fixture.view.debugFirstCardFooterSourceText, "Ghostty - Used 3 times") + } + func testEditedTextStatusUsesActionTone() { let fixture = makePanelFixture() fixture.store.upsert(makeTextItem("Editable footer item", store: fixture.store))