From f7d7abc69496820dae2da2cd08d07e0ba8b0d297 Mon Sep 17 00:00:00 2001 From: uniplanck <198168437+uniplanck@users.noreply.github.com> Date: Tue, 4 Aug 2026 02:58:20 +0900 Subject: [PATCH 1/2] Fix Board-Man panel UI regressions --- Clipy/Sources/Managers/MenuManager.swift | 101 +++++---- .../CPYUpdatesPreferenceViewController.xib | 4 +- ClipyTests/EntitlementGateTests.swift | 199 ++++++++++++++++++ 3 files changed, 249 insertions(+), 55 deletions(-) diff --git a/Clipy/Sources/Managers/MenuManager.swift b/Clipy/Sources/Managers/MenuManager.swift index 451eb64..f7f0bc7 100644 --- a/Clipy/Sources/Managers/MenuManager.swift +++ b/Clipy/Sources/Managers/MenuManager.swift @@ -3004,11 +3004,27 @@ final class BoardManHeaderSegmentedControl: NSSegmentedControl { setHoveredSegment(-1) return } - let segmentWidth = bounds.width / CGFloat(segmentCount) - let index = min(segmentCount - 1, max(0, Int(point.x / segmentWidth))) + let index = (0.. NSRect? { + guard let segmentRect = segmentFrame(for: segment) else { return nil } + return NSIntegralRect(segmentRect.insetBy(dx: 2, dy: 3)) + } + + private func segmentFrame(for segment: Int) -> NSRect? { + guard segment >= 0, segment < segmentCount, bounds.width > 0 else { return nil } + let configuredWidths = (0.. 0 + ? configuredWidths.map { bounds.width * ($0 / configuredTotal) } + : Array(repeating: bounds.width / CGFloat(segmentCount), count: segmentCount) + let minX = bounds.minX + widths.prefix(segment).reduce(0, +) + let maxX = segment == segmentCount - 1 ? bounds.maxX : minX + widths[segment] + return NSRect(x: minX, y: bounds.minY, width: max(0, maxX - minX), height: bounds.height) + } + private func setHoveredSegment(_ value: Int) { guard hoveredSegment != value else { return } hoveredSegment = value @@ -3017,28 +3033,14 @@ final class BoardManHeaderSegmentedControl: NSSegmentedControl { override func draw(_ dirtyRect: NSRect) { super.draw(dirtyRect) - guard hoveredSegment >= 0, hoveredSegment < segmentCount, segmentCount > 0 else { return } - - let segmentWidth = bounds.width / CGFloat(segmentCount) - let minX = floor(CGFloat(hoveredSegment) * segmentWidth) + 2 - let maxX = min(bounds.maxX - 2, floor(CGFloat(hoveredSegment + 1) * segmentWidth) - 2) - let hoverRect = NSRect( - x: minX, - y: 3, - width: max(0, maxX - minX), - height: max(0, bounds.height - 6) - ) + guard let hoverRect = hoverBackgroundRect(forSegment: hoveredSegment) else { return } + let isSelected = hoveredSegment == selectedSegment let path = NSBezierPath(roundedRect: hoverRect, xRadius: 8, yRadius: 8) (isSelected - ? NSColor.selectedControlTextColor.withAlphaComponent(0.12) - : NSColor.labelColor.withAlphaComponent(0.10)).setFill() + ? NSColor.selectedControlTextColor.withAlphaComponent(0.10) + : NSColor.labelColor.withAlphaComponent(0.08)).setFill() path.fill() - (isSelected - ? NSColor.selectedControlTextColor.withAlphaComponent(0.36) - : NSColor.labelColor.withAlphaComponent(0.28)).setStroke() - path.lineWidth = 1 - path.stroke() } } @@ -3118,31 +3120,6 @@ final class BoardManCenteredSearchFieldCell: NSSearchFieldCell { return verticallyCentered(original, height: original.height) } - override func edit(withFrame aRect: NSRect, - in controlView: NSView, - editor textObj: NSText, - delegate: Any?, - event: NSEvent?) { - super.edit(withFrame: searchTextRect(forBounds: aRect), - in: controlView, - editor: textObj, - delegate: delegate, - event: event) - } - - override func select(withFrame aRect: NSRect, - in controlView: NSView, - editor textObj: NSText, - delegate: Any?, - start selStart: Int, - length selLength: Int) { - super.select(withFrame: searchTextRect(forBounds: aRect), - in: controlView, - editor: textObj, - delegate: delegate, - start: selStart, - length: selLength) - } } final class BoardManHistoryCellView: NSTableCellView { @@ -5709,6 +5686,7 @@ class BoardManPanel: NSPanel { snippetAddButton = addSnippet let editSnippet = NSButton(title: boardManText("Edit Mode"), target: self, action: #selector(editSelectedSnippetFromPanel(_:))) + editSnippet.identifier = NSUserInterfaceItemIdentifier("BoardManSnippetEditButton") editSnippet.font = NSFont.systemFont(ofSize: 11) editSnippet.bezelStyle = .rounded editSnippet.isHidden = true @@ -5790,6 +5768,9 @@ class BoardManPanel: NSPanel { let editorTitle = NSTextField(frame: .zero) editorTitle.cell = BoardManCenteredTextFieldCell(textCell: "") editorTitle.font = NSFont.systemFont(ofSize: 12) + editorTitle.textColor = .textColor + editorTitle.backgroundColor = .textBackgroundColor + editorTitle.drawsBackground = true editorTitle.placeholderString = boardManText("Untitled snippet") editorTitle.identifier = NSUserInterfaceItemIdentifier("BoardManSnippetEditorTitleField") editorTitle.target = self @@ -5805,11 +5786,20 @@ class BoardManPanel: NSPanel { snippetEditorContentLabel = editorContentLabel let editorScroll = NSScrollView(frame: .zero) + editorScroll.identifier = NSUserInterfaceItemIdentifier("BoardManSnippetEditorScrollView") editorScroll.hasVerticalScroller = true editorScroll.borderType = .bezelBorder editorScroll.autohidesScrollers = true + editorScroll.drawsBackground = true + editorScroll.backgroundColor = .textBackgroundColor let editorText = NSTextView(frame: .zero) + editorText.identifier = NSUserInterfaceItemIdentifier("BoardManSnippetEditorTextView") editorText.font = NSFont.systemFont(ofSize: 12) + editorText.textColor = .textColor + editorText.backgroundColor = .textBackgroundColor + editorText.insertionPointColor = .textColor + editorText.drawsBackground = true + editorText.textContainerInset = NSSize(width: 8, height: 8) editorText.isRichText = false editorText.isAutomaticQuoteSubstitutionEnabled = false editorText.enabledTextCheckingTypes = 0 @@ -7225,6 +7215,14 @@ class BoardManPanel: NSPanel { } #if DEBUG + func loadItemsForTesting(_ items: [BoardManHistoryItem]) { + reloadHistoryItems(items) + } + + func selectItemForTesting(at index: Int) { + setSelectedIndex(index) + } + fileprivate func prepareReadmeScreenshot(scene: String, width: CGFloat?, height: CGFloat?) { var targetFrame = frame if let width { @@ -7886,11 +7884,11 @@ class BoardManPanel: NSPanel { snippetEnableButton?.state = snippet.enable ? .on : .off snippetFolderEnableButton?.state = (folder?.enable ?? false) ? .on : .off } - let canEditTitleDirectly = !isSnippetReorderMode - snippetEditorTitleField?.isEnabled = canEditTitleDirectly - snippetEditorTitleField?.isEditable = canEditTitleDirectly - snippetEditorTitleField?.isSelectable = canEditTitleDirectly - snippetEditorTextView?.isEditable = isEditingSelection + let canEditSelection = isEditingSelection && !isSnippetReorderMode + snippetEditorTitleField?.isEnabled = !isSnippetReorderMode + snippetEditorTitleField?.isEditable = canEditSelection + snippetEditorTitleField?.isSelectable = canEditSelection + snippetEditorTextView?.isEditable = canEditSelection snippetEditorTextView?.isSelectable = true if isEditingSelection { @@ -8128,8 +8126,6 @@ class BoardManPanel: NSPanel { editingSnippetIdentifier = item.dataHash NSHapticFeedbackManager.defaultPerformer.perform(.alignment, performanceTime: .now) updateSnippetActionButtons() - makeFirstResponder(snippetEditorTitleField) - snippetEditorTitleField?.selectText(nil) } @objc private func cancelSnippetEditing(_ sender: Any?) { @@ -8915,7 +8911,6 @@ class BoardManPanel: NSPanel { self.applyCurrentFilter() } DispatchQueue.main.async { - alert.window.level = .floating alert.window.makeFirstResponder(includedField) } } diff --git a/Clipy/Sources/Preferences/Panels/Base.lproj/CPYUpdatesPreferenceViewController.xib b/Clipy/Sources/Preferences/Panels/Base.lproj/CPYUpdatesPreferenceViewController.xib index a4e8990..fcf259c 100644 --- a/Clipy/Sources/Preferences/Panels/Base.lproj/CPYUpdatesPreferenceViewController.xib +++ b/Clipy/Sources/Preferences/Panels/Base.lproj/CPYUpdatesPreferenceViewController.xib @@ -25,7 +25,7 @@ - + @@ -75,7 +75,7 @@ - + diff --git a/ClipyTests/EntitlementGateTests.swift b/ClipyTests/EntitlementGateTests.swift index 6966635..23f0d9b 100644 --- a/ClipyTests/EntitlementGateTests.swift +++ b/ClipyTests/EntitlementGateTests.swift @@ -1383,6 +1383,205 @@ final class BoardManPanelLayoutTests { } } +} + +@MainActor @Suite(.serialized) +final class BoardManUIRegressionTests { + + @Test + func searchEditingHoverAndFilterPresentationStayStable() async throws { + let originalRealmConfiguration = Realm.Configuration.defaultConfiguration + Realm.Configuration.defaultConfiguration = Realm.Configuration(inMemoryIdentifier: UUID().uuidString) + defer { Realm.Configuration.defaultConfiguration = originalRealmConfiguration } + + let panel = BoardManPanel() + panel.setFrame(NSRect(x: 0, y: 0, width: 800, height: 760), display: false) + panel.makeKeyAndOrderFront(nil) + defer { panel.orderOut(nil) } + await settlePanelLayout(panel) + + let root = try #require(panel.contentView) + let descendants = allSubviews(of: root) + let tabs = try #require(descendants.compactMap { $0 as? BoardManHeaderSegmentedControl }.first) + let tabFrameBeforeHover = tabs.frame + let tabWidthsBeforeHover = (0..= tabs.bounds.minX) + #expect(tabs.frame == tabFrameBeforeHover, "Hover must not resize or move the tab control.") + #expect((0..= 0.35, + "Settings contains low-contrast dark text: \(field.stringValue), luminance=\(luminance)") + } + } + + private func settlePanelLayout(_ panel: BoardManPanel) async { + await withCheckedContinuation { continuation in + DispatchQueue.main.async { + continuation.resume() + } + } + panel.contentView?.layoutSubtreeIfNeeded() + } + + private func allSubviews(of view: NSView) -> [NSView] { + return view.subviews + view.subviews.flatMap(allSubviews(of:)) + } +} + +extension BoardManPanelLayoutTests { + @Test func responsiveTemplateTabLabelsStayReadableAcrossLanguages() async throws { let originalRealmConfiguration = Realm.Configuration.defaultConfiguration From b0dac1d94371456fb1a474712b3938348c32085c Mon Sep 17 00:00:00 2001 From: uniplanck <198168437+uniplanck@users.noreply.github.com> Date: Tue, 4 Aug 2026 04:08:11 +0900 Subject: [PATCH 2/2] Stabilize search layout regression test --- ClipyTests/EntitlementGateTests.swift | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/ClipyTests/EntitlementGateTests.swift b/ClipyTests/EntitlementGateTests.swift index 23f0d9b..6b5a247 100644 --- a/ClipyTests/EntitlementGateTests.swift +++ b/ClipyTests/EntitlementGateTests.swift @@ -1417,16 +1417,22 @@ final class BoardManUIRegressionTests { let search = try #require(descendants.compactMap { $0 as? NSSearchField }.first) let searchCell = try #require(search.cell as? BoardManCenteredSearchFieldCell) let searchFrameBeforeEditing = search.frame - search.stringValue = "layout" + search.stringValue = "" #expect(panel.makeFirstResponder(search)) search.selectText(nil) await settlePanelLayout(panel) let editor = try #require(search.currentEditor() as? NSTextView) - let expectedTextRect = searchCell.searchTextRect(forBounds: search.bounds) - #expect(abs(editor.frame.midY - expectedTextRect.midY) <= 1, - "Search field editor moved away from the non-editing text rect.") + let editorFrameAtInputStart = editor.frame + search.stringValue = "layout" + await settlePanelLayout(panel) + #expect(editor.frame == editorFrameAtInputStart, + "Entering search text must not move the field editor.") #expect(search.frame == searchFrameBeforeEditing, "Starting search input must not move or resize the search control.") + let textRect = searchCell.searchTextRect(forBounds: search.bounds) + let cancelRect = searchCell.cancelButtonRect(forBounds: search.bounds) + #expect(abs(cancelRect.midY - textRect.midY) <= 0.5, + "The search clear button must stay vertically aligned with the input text.") search.stringValue = "" search.abortEditing()