From ffcd3fdfaa84b4dc5b33e4d7ff7e8573fa1f2390 Mon Sep 17 00:00:00 2001 From: Lawrence Chen Date: Tue, 17 Mar 2026 01:51:57 -0700 Subject: [PATCH] Tighten browser import sheet UI --- Resources/Localizable.xcstrings | 60 ++--- Sources/Panels/BrowserPanel.swift | 208 +++++++++++++----- cmuxTests/BrowserImportMappingTests.swift | 17 ++ .../BrowserImportProfilesUITests.swift | 6 +- 4 files changed, 202 insertions(+), 89 deletions(-) diff --git a/Resources/Localizable.xcstrings b/Resources/Localizable.xcstrings index 68b84487..257a60d5 100644 --- a/Resources/Localizable.xcstrings +++ b/Resources/Localizable.xcstrings @@ -4740,13 +4740,13 @@ "en": { "stringUnit": { "state": "translated", - "value": "Bookmarks, settings, and extensions import are not available yet." + "value": "Bookmarks, settings, and extensions are not available yet." } }, "ja": { "stringUnit": { "state": "translated", - "value": "ブックマーク、設定、拡張機能のインポートにはまだ対応していません。" + "value": "ブックマーク、設定、拡張機能はまだ利用できません。" } } } @@ -5029,13 +5029,13 @@ "en": { "stringUnit": { "state": "translated", - "value": "cmux destination" + "value": "Destination" } }, "ja": { "stringUnit": { "state": "translated", - "value": "cmux の保存先" + "value": "保存先" } } } @@ -5080,13 +5080,13 @@ "en": { "stringUnit": { "state": "translated", - "value": "Imported cookies and history go into the selected cmux browser profile." + "value": "Imported data goes into the selected cmux profile." } }, "ja": { "stringUnit": { "state": "translated", - "value": "インポートしたCookieと履歴は、選択したcmuxブラウザープロファイルに保存されます。" + "value": "インポートしたデータは、選択した cmux プロファイルに保存されます。" } } } @@ -5097,13 +5097,13 @@ "en": { "stringUnit": { "state": "translated", - "value": "All selected source profiles will be merged into the chosen cmux browser profile." + "value": "All selected source profiles go into one cmux profile." } }, "ja": { "stringUnit": { "state": "translated", - "value": "選択した元プロファイルはすべて、選んだ cmux ブラウザープロファイルにまとめて取り込まれます。" + "value": "選択した元プロファイルは、1つの cmux プロファイルにまとめて取り込まれます。" } } } @@ -5114,13 +5114,13 @@ "en": { "stringUnit": { "state": "translated", - "value": "Missing cmux profiles are created when import starts." + "value": "Missing cmux profiles are created on import." } }, "ja": { "stringUnit": { "state": "translated", - "value": "不足している cmux プロファイルは、インポート開始時に作成されます。" + "value": "不足している cmux プロファイルは、インポート時に作成されます。" } } } @@ -5131,13 +5131,13 @@ "en": { "stringUnit": { "state": "translated", - "value": "Merge all into one cmux profile" + "value": "Merge into one" } }, "ja": { "stringUnit": { "state": "translated", - "value": "すべてを1つの cmux プロファイルにまとめる" + "value": "1つにまとめる" } } } @@ -5148,13 +5148,13 @@ "en": { "stringUnit": { "state": "translated", - "value": "Keep profiles separate" + "value": "Separate profiles" } }, "ja": { "stringUnit": { "state": "translated", - "value": "プロファイルを分けたまま取り込む" + "value": "分けて取り込む" } } } @@ -5233,13 +5233,13 @@ "en": { "stringUnit": { "state": "translated", - "value": "Limit to" + "value": "Domains" } }, "ja": { "stringUnit": { "state": "translated", - "value": "対象ドメイン" + "value": "ドメイン" } } } @@ -5250,13 +5250,13 @@ "en": { "stringUnit": { "state": "translated", - "value": "Optional domains only (e.g. github.com, openai.com)" + "value": "Optional domains, comma-separated" } }, "ja": { "stringUnit": { "state": "translated", - "value": "任意のドメインのみ(例: github.com, openai.com)" + "value": "任意のドメインをカンマ区切りで指定" } } } @@ -5505,13 +5505,13 @@ "en": { "stringUnit": { "state": "translated", - "value": "Source" + "value": "Browser" } }, "ja": { "stringUnit": { "state": "translated", - "value": "インポート元" + "value": "ブラウザー" } } } @@ -5539,13 +5539,13 @@ "en": { "stringUnit": { "state": "translated", - "value": "Source Profiles" + "value": "Profiles" } }, "ja": { "stringUnit": { "state": "translated", - "value": "元プロファイル" + "value": "プロファイル" } } } @@ -5556,13 +5556,13 @@ "en": { "stringUnit": { "state": "translated", - "value": "Choose one or more source profiles. Step 3 lets you keep them separate or merge them into one cmux profile." + "value": "Select one or more profiles." } }, "ja": { "stringUnit": { "state": "translated", - "value": "元プロファイルを1つ以上選択してください。3 / 3 で、分けたまま取り込むか、1つの cmux プロファイルにまとめるかを選べます。" + "value": "1つ以上のプロファイルを選択してください。" } } } @@ -5607,13 +5607,13 @@ "en": { "stringUnit": { "state": "translated", - "value": "Step 3 of 3: Choose what to import from %@ and where to put it." + "value": "Step 3 of 3" } }, "ja": { "stringUnit": { "state": "translated", - "value": "3 / 3: %@ から何をインポートし、どこに保存するかを選択します。" + "value": "3 / 3" } } } @@ -5624,13 +5624,13 @@ "en": { "stringUnit": { "state": "translated", - "value": "Step 1 of 3: Choose the browser to import from." + "value": "Step 1 of 3" } }, "ja": { "stringUnit": { "state": "translated", - "value": "1 / 3: インポート元のブラウザーを選択します。" + "value": "1 / 3" } } } @@ -5641,13 +5641,13 @@ "en": { "stringUnit": { "state": "translated", - "value": "Step 2 of 3: Choose source profiles from %@." + "value": "Step 2 of 3" } }, "ja": { "stringUnit": { "state": "translated", - "value": "2 / 3: %@ の元プロファイルを選択します。" + "value": "2 / 3" } } } diff --git a/Sources/Panels/BrowserPanel.swift b/Sources/Panels/BrowserPanel.swift index c7a17f4d..9e2e5504 100644 --- a/Sources/Panels/BrowserPanel.swift +++ b/Sources/Panels/BrowserPanel.swift @@ -6837,6 +6837,18 @@ struct BrowserImportStep3Presentation: Equatable { } } +struct BrowserImportSourceProfilesPresentation: Equatable { + let scrollHeight: CGFloat + let showsHelpText: Bool + + init(profileCount: Int) { + let visibleRows = min(max(profileCount, 1), 5) + let contentHeight = CGFloat(visibleRows * 26 + 14) + scrollHeight = max(76, contentHeight) + showsHelpText = profileCount > 1 + } +} + enum BrowserImportPlanResolver { @MainActor static func defaultPlan( @@ -8378,6 +8390,7 @@ final class BrowserDataImportCoordinator { private let sourceProfilesEmptyLabel = NSTextField(wrappingLabelWithString: "") private let sourceProfilesHelpLabel = NSTextField(labelWithString: "") private let sourceProfilesScrollView = NSScrollView() + private var sourceProfilesScrollHeightConstraint: NSLayoutConstraint? private let dataTypesContainer = NSStackView() private let validationLabel = NSTextField(labelWithString: "") private let destinationModeContainer = NSStackView() @@ -8387,6 +8400,7 @@ final class BrowserDataImportCoordinator { private let mergeDestinationRow = NSStackView() private let mergeDestinationPopup = NSPopUpButton(frame: .zero, pullsDown: false) private let destinationHelpLabel = NSTextField(wrappingLabelWithString: "") + private let additionalDataNoteLabel = NSTextField(wrappingLabelWithString: "") private let cookiesCheckbox = NSButton(checkboxWithTitle: "", target: nil, action: nil) private let historyCheckbox = NSButton(checkboxWithTitle: "", target: nil, action: nil) @@ -8412,7 +8426,7 @@ final class BrowserDataImportCoordinator { ?? fallbackDestinationProfileID self.mergeDestinationProfileID = self.initialDestinationProfileID self.panel = NSPanel( - contentRect: NSRect(x: 0, y: 0, width: 620, height: 420), + contentRect: NSRect(x: 0, y: 0, width: 560, height: 292), styleMask: [.titled, .closable], backing: .buffered, defer: false @@ -8538,6 +8552,7 @@ final class BrowserDataImportCoordinator { guard selectedSourceProfiles.count > 1 else { return } destinationMode = sender == separateProfilesRadio ? .separateProfiles : .mergeIntoOne rebuildStep3DestinationUI() + updatePanelSize() } @objc @@ -8560,6 +8575,13 @@ final class BrowserDataImportCoordinator { validationLabel.isHidden = true } + @objc + private func handleImportOptionChanged(_ sender: NSButton) { + validationLabel.isHidden = true + updateAdditionalDataNoteVisibility() + updatePanelSize() + } + private func setupUI() { panel.title = String( localized: "browser.import.title", @@ -8570,7 +8592,7 @@ final class BrowserDataImportCoordinator { panel.standardWindowButton(.miniaturizeButton)?.isHidden = true panel.standardWindowButton(.zoomButton)?.isHidden = true - let contentView = NSView(frame: NSRect(x: 0, y: 0, width: 620, height: 420)) + let contentView = NSView(frame: NSRect(x: 0, y: 0, width: 560, height: 292)) contentView.translatesAutoresizingMaskIntoConstraints = false panel.contentView = contentView @@ -8580,9 +8602,9 @@ final class BrowserDataImportCoordinator { defaultValue: "Import Browser Data" ) ) - titleLabel.font = NSFont.systemFont(ofSize: 24, weight: .semibold) + titleLabel.font = NSFont.systemFont(ofSize: 22, weight: .semibold) - stepLabel.font = NSFont.systemFont(ofSize: 15, weight: .medium) + stepLabel.font = NSFont.systemFont(ofSize: 13, weight: .semibold) stepLabel.textColor = .secondaryLabelColor setupSourceContainer() @@ -8594,6 +8616,7 @@ final class BrowserDataImportCoordinator { validationLabel.isHidden = true validationLabel.lineBreakMode = .byWordWrapping validationLabel.maximumNumberOfLines = 3 + validationLabel.translatesAutoresizingMaskIntoConstraints = false backButton.target = self backButton.action = #selector(handleBack) @@ -8631,23 +8654,32 @@ final class BrowserDataImportCoordinator { validationLabel, ]) contentStack.orientation = .vertical - contentStack.spacing = 10 + contentStack.spacing = 8 contentStack.alignment = .leading contentStack.translatesAutoresizingMaskIntoConstraints = false + sourceContainer.translatesAutoresizingMaskIntoConstraints = false + sourceProfilesContainer.translatesAutoresizingMaskIntoConstraints = false + dataTypesContainer.translatesAutoresizingMaskIntoConstraints = false + guard let panelContent = panel.contentView else { return } panelContent.addSubview(contentStack) panelContent.addSubview(buttonRow) NSLayoutConstraint.activate([ - contentStack.topAnchor.constraint(equalTo: panelContent.topAnchor, constant: 18), - contentStack.leadingAnchor.constraint(equalTo: panelContent.leadingAnchor, constant: 20), - contentStack.trailingAnchor.constraint(equalTo: panelContent.trailingAnchor, constant: -20), + contentStack.topAnchor.constraint(equalTo: panelContent.topAnchor, constant: 16), + contentStack.leadingAnchor.constraint(equalTo: panelContent.leadingAnchor, constant: 18), + contentStack.trailingAnchor.constraint(equalTo: panelContent.trailingAnchor, constant: -18), buttonRow.topAnchor.constraint(greaterThanOrEqualTo: contentStack.bottomAnchor, constant: 14), - buttonRow.leadingAnchor.constraint(equalTo: panelContent.leadingAnchor, constant: 20), - buttonRow.trailingAnchor.constraint(equalTo: panelContent.trailingAnchor, constant: -20), - buttonRow.bottomAnchor.constraint(equalTo: panelContent.bottomAnchor, constant: -16), + buttonRow.leadingAnchor.constraint(equalTo: panelContent.leadingAnchor, constant: 18), + buttonRow.trailingAnchor.constraint(equalTo: panelContent.trailingAnchor, constant: -18), + buttonRow.bottomAnchor.constraint(equalTo: panelContent.bottomAnchor, constant: -14), + + sourceContainer.widthAnchor.constraint(equalTo: contentStack.widthAnchor), + sourceProfilesContainer.widthAnchor.constraint(equalTo: contentStack.widthAnchor), + dataTypesContainer.widthAnchor.constraint(equalTo: contentStack.widthAnchor), + validationLabel.widthAnchor.constraint(equalTo: contentStack.widthAnchor), ]) } @@ -8663,23 +8695,27 @@ final class BrowserDataImportCoordinator { labelWithString: String(localized: "browser.import.source", defaultValue: "Source") ) sourceLabel.alignment = .right - sourceLabel.frame.size.width = 80 + sourceLabel.frame.size.width = 64 + + sourcePopup.setContentHuggingPriority(.defaultLow, for: .horizontal) + sourcePopup.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) let sourceRow = NSStackView(views: [sourceLabel, sourcePopup]) sourceRow.orientation = .horizontal sourceRow.spacing = 8 sourceRow.alignment = .centerY + sourceRow.distribution = .fill let detectedLabel = NSTextField( wrappingLabelWithString: InstalledBrowserDetector.summaryText(for: browsers) ) - detectedLabel.font = NSFont.systemFont(ofSize: 12) + detectedLabel.font = NSFont.systemFont(ofSize: 11) detectedLabel.textColor = .secondaryLabelColor detectedLabel.maximumNumberOfLines = 2 detectedLabel.preferredMaxLayoutWidth = 500 sourceContainer.orientation = .vertical - sourceContainer.spacing = 10 + sourceContainer.spacing = 8 sourceContainer.alignment = .leading sourceContainer.addArrangedSubview(sourceRow) sourceContainer.addArrangedSubview(detectedLabel) @@ -8692,17 +8728,17 @@ final class BrowserDataImportCoordinator { defaultValue: "Source Profiles" ) ) - sourceProfilesTitle.font = NSFont.systemFont(ofSize: 13, weight: .semibold) + sourceProfilesTitle.font = NSFont.systemFont(ofSize: 12, weight: .semibold) sourceProfilesList.orientation = .vertical sourceProfilesList.spacing = 6 sourceProfilesList.alignment = .leading sourceProfilesList.translatesAutoresizingMaskIntoConstraints = false - sourceProfilesEmptyLabel.font = NSFont.systemFont(ofSize: 13) + sourceProfilesEmptyLabel.font = NSFont.systemFont(ofSize: 12) sourceProfilesEmptyLabel.textColor = .secondaryLabelColor sourceProfilesEmptyLabel.maximumNumberOfLines = 0 - sourceProfilesEmptyLabel.preferredMaxLayoutWidth = 520 + sourceProfilesEmptyLabel.preferredMaxLayoutWidth = 500 sourceProfilesDocumentView.frame = NSRect(x: 0, y: 0, width: 1, height: 1) sourceProfilesDocumentView.translatesAutoresizingMaskIntoConstraints = false @@ -8721,19 +8757,22 @@ final class BrowserDataImportCoordinator { sourceProfilesScrollView.documentView = sourceProfilesDocumentView sourceProfilesScrollView.translatesAutoresizingMaskIntoConstraints = false sourceProfilesScrollView.contentView.postsBoundsChangedNotifications = true - sourceProfilesScrollView.heightAnchor.constraint(equalToConstant: 180).isActive = true + sourceProfilesScrollHeightConstraint = sourceProfilesScrollView.heightAnchor.constraint(equalToConstant: 76) + sourceProfilesScrollHeightConstraint?.isActive = true + sourceProfilesScrollView.widthAnchor.constraint(equalTo: sourceProfilesContainer.widthAnchor).isActive = true - sourceProfilesHelpLabel.font = NSFont.systemFont(ofSize: 12) + sourceProfilesHelpLabel.font = NSFont.systemFont(ofSize: 11) sourceProfilesHelpLabel.textColor = .secondaryLabelColor sourceProfilesHelpLabel.maximumNumberOfLines = 2 sourceProfilesHelpLabel.lineBreakMode = .byWordWrapping + sourceProfilesHelpLabel.preferredMaxLayoutWidth = 500 sourceProfilesHelpLabel.stringValue = String( localized: "browser.import.sourceProfiles.help", defaultValue: "Choose one or more source profiles. Step 3 lets you keep them separate or merge them into one cmux profile." ) sourceProfilesContainer.orientation = .vertical - sourceProfilesContainer.spacing = 10 + sourceProfilesContainer.spacing = 8 sourceProfilesContainer.alignment = .leading sourceProfilesContainer.addArrangedSubview(sourceProfilesTitle) sourceProfilesContainer.addArrangedSubview(sourceProfilesScrollView) @@ -8758,6 +8797,12 @@ final class BrowserDataImportCoordinator { localized: "browser.import.additionalData", defaultValue: "Additional data (bookmarks, settings, extensions)" ) + cookiesCheckbox.target = self + cookiesCheckbox.action = #selector(handleImportOptionChanged(_:)) + historyCheckbox.target = self + historyCheckbox.action = #selector(handleImportOptionChanged(_:)) + additionalDataCheckbox.target = self + additionalDataCheckbox.action = #selector(handleImportOptionChanged(_:)) cookiesCheckbox.setAccessibilityIdentifier("BrowserImportCookiesCheckbox") historyCheckbox.setAccessibilityIdentifier("BrowserImportHistoryCheckbox") additionalDataCheckbox.setAccessibilityIdentifier("BrowserImportAdditionalDataCheckbox") @@ -8782,25 +8827,29 @@ final class BrowserDataImportCoordinator { mergeDestinationPopup.target = self mergeDestinationPopup.action = #selector(handleMergeDestinationChanged(_:)) + mergeDestinationPopup.setContentHuggingPriority(.defaultLow, for: .horizontal) + mergeDestinationPopup.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) separateDestinationRows.orientation = .vertical - separateDestinationRows.spacing = 8 + separateDestinationRows.spacing = 6 separateDestinationRows.alignment = .leading mergeDestinationRow.orientation = .horizontal - mergeDestinationRow.spacing = 8 + mergeDestinationRow.spacing = 6 mergeDestinationRow.alignment = .centerY - destinationHelpLabel.font = NSFont.systemFont(ofSize: 12) + destinationHelpLabel.font = NSFont.systemFont(ofSize: 11) destinationHelpLabel.textColor = .secondaryLabelColor - destinationHelpLabel.maximumNumberOfLines = 3 - destinationHelpLabel.preferredMaxLayoutWidth = 540 + destinationHelpLabel.maximumNumberOfLines = 2 + destinationHelpLabel.preferredMaxLayoutWidth = 500 domainField.placeholderString = String( localized: "browser.import.domain.placeholder", defaultValue: "Optional domains only (e.g. github.com, openai.com)" ) domainField.stringValue = "" + domainField.setContentHuggingPriority(.defaultLow, for: .horizontal) + domainField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) let destinationTitleLabel = NSTextField( labelWithString: String( @@ -8808,32 +8857,32 @@ final class BrowserDataImportCoordinator { defaultValue: "cmux destination" ) ) - destinationTitleLabel.font = NSFont.systemFont(ofSize: 13, weight: .semibold) + destinationTitleLabel.font = NSFont.systemFont(ofSize: 12, weight: .semibold) let domainLabel = NSTextField( labelWithString: String(localized: "browser.import.domain", defaultValue: "Limit to") ) domainLabel.alignment = .right - domainLabel.frame.size.width = 80 + domainLabel.frame.size.width = 72 let domainRow = NSStackView(views: [domainLabel, domainField]) domainRow.orientation = .horizontal domainRow.spacing = 8 domainRow.alignment = .centerY + domainRow.distribution = .fill - let noteLabel = NSTextField( - wrappingLabelWithString: String( - localized: "browser.import.additionalData.note", - defaultValue: "Bookmarks, settings, and extensions import are not available yet." - ) + additionalDataNoteLabel.stringValue = String( + localized: "browser.import.additionalData.note", + defaultValue: "Bookmarks, settings, and extensions import are not available yet." ) - noteLabel.font = NSFont.systemFont(ofSize: 12) - noteLabel.textColor = .secondaryLabelColor - noteLabel.maximumNumberOfLines = 2 - noteLabel.preferredMaxLayoutWidth = 540 + additionalDataNoteLabel.font = NSFont.systemFont(ofSize: 11) + additionalDataNoteLabel.textColor = .secondaryLabelColor + additionalDataNoteLabel.maximumNumberOfLines = 2 + additionalDataNoteLabel.preferredMaxLayoutWidth = 500 + additionalDataNoteLabel.isHidden = true dataTypesContainer.orientation = .vertical - dataTypesContainer.spacing = 8 + dataTypesContainer.spacing = 6 dataTypesContainer.alignment = .leading dataTypesContainer.addArrangedSubview(destinationTitleLabel) dataTypesContainer.addArrangedSubview(destinationModeContainer) @@ -8843,13 +8892,14 @@ final class BrowserDataImportCoordinator { dataTypesContainer.addArrangedSubview(cookiesCheckbox) dataTypesContainer.addArrangedSubview(historyCheckbox) dataTypesContainer.addArrangedSubview(additionalDataCheckbox) + dataTypesContainer.addArrangedSubview(additionalDataNoteLabel) dataTypesContainer.addArrangedSubview(domainRow) - dataTypesContainer.addArrangedSubview(noteLabel) } private func configureInitialState() { step = .source refreshSourceProfilesList() + updateAdditionalDataNoteVisibility() updateStepUI() } @@ -8858,7 +8908,7 @@ final class BrowserDataImportCoordinator { case .source: stepLabel.stringValue = String( localized: "browser.import.step.source", - defaultValue: "Step 1 of 3: Choose the browser to import from." + defaultValue: "Step 1 of 3" ) sourceContainer.isHidden = false sourceProfilesContainer.isHidden = true @@ -8868,11 +8918,8 @@ final class BrowserDataImportCoordinator { primaryButton.title = String(localized: "browser.import.next", defaultValue: "Next") case .sourceProfiles: stepLabel.stringValue = String( - format: String( - localized: "browser.import.step.sourceProfiles", - defaultValue: "Step 2 of 3: Choose source profiles from %@." - ), - selectedBrowser().displayName + localized: "browser.import.step.sourceProfiles", + defaultValue: "Step 2 of 3" ) sourceContainer.isHidden = true sourceProfilesContainer.isHidden = false @@ -8883,11 +8930,8 @@ final class BrowserDataImportCoordinator { case .dataTypes: rebuildStep3DestinationUI() stepLabel.stringValue = String( - format: String( - localized: "browser.import.step.dataTypes", - defaultValue: "Step 3 of 3: Choose what to import from %@ and where to put it." - ), - selectedBrowser().displayName + localized: "browser.import.step.dataTypes", + defaultValue: "Step 3 of 3" ) sourceContainer.isHidden = true sourceProfilesContainer.isHidden = true @@ -8899,6 +8943,7 @@ final class BrowserDataImportCoordinator { defaultValue: "Start Import" ) } + updatePanelSize() } private func selectedBrowser() -> InstalledBrowserCandidate { @@ -8925,6 +8970,7 @@ final class BrowserDataImportCoordinator { browser.displayName ) sourceProfilesList.addArrangedSubview(sourceProfilesEmptyLabel) + updateSourceProfilesPresentation(for: browser) return } @@ -8940,6 +8986,8 @@ final class BrowserDataImportCoordinator { sourceProfilesList.addArrangedSubview(checkbox) sourceProfileCheckboxes.append(checkbox) } + + updateSourceProfilesPresentation(for: browser) } private func storedSelectedSourceProfileIDs(for browser: InstalledBrowserCandidate) -> Set { @@ -9055,16 +9103,16 @@ final class BrowserDataImportCoordinator { localized: "browser.import.destinationProfile.separateHelp", defaultValue: "Missing cmux profiles are created when import starts." ) + destinationHelpLabel.isHidden = false } else if plan.entries.count > 1 { destinationHelpLabel.stringValue = String( localized: "browser.import.destinationProfile.mergeHelp", defaultValue: "All selected source profiles will be merged into the chosen cmux browser profile." ) + destinationHelpLabel.isHidden = false } else { - destinationHelpLabel.stringValue = String( - localized: "browser.import.destinationProfile.help", - defaultValue: "Imported cookies and history go into the selected cmux browser profile." - ) + destinationHelpLabel.stringValue = "" + destinationHelpLabel.isHidden = true } } @@ -9081,7 +9129,7 @@ final class BrowserDataImportCoordinator { guard let sourceProfile = entry.sourceProfiles.first else { continue } let sourceLabel = NSTextField(labelWithString: sourceProfile.displayName) sourceLabel.alignment = .right - sourceLabel.frame.size.width = 140 + sourceLabel.frame.size.width = 110 let popup = NSPopUpButton(frame: .zero, pullsDown: false) popup.target = self @@ -9101,11 +9149,14 @@ final class BrowserDataImportCoordinator { } else { popup.selectItem(at: 0) } + popup.setContentHuggingPriority(.defaultLow, for: .horizontal) + popup.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) let row = NSStackView(views: [sourceLabel, popup]) row.orientation = .horizontal - row.spacing = 8 + row.spacing = 6 row.alignment = .centerY + row.distribution = .fill separateDestinationRows.addArrangedSubview(row) } } @@ -9137,7 +9188,7 @@ final class BrowserDataImportCoordinator { ) ) destinationLabel.alignment = .right - destinationLabel.frame.size.width = 140 + destinationLabel.frame.size.width = 110 mergeDestinationRow.addArrangedSubview(destinationLabel) mergeDestinationRow.addArrangedSubview(mergeDestinationPopup) @@ -9211,6 +9262,51 @@ final class BrowserDataImportCoordinator { return base.isEmpty ? "profile-\(index)" : base } + private func updateSourceProfilesPresentation(for browser: InstalledBrowserCandidate) { + let presentation = BrowserImportSourceProfilesPresentation(profileCount: browser.profiles.count) + sourceProfilesScrollHeightConstraint?.constant = presentation.scrollHeight + sourceProfilesHelpLabel.isHidden = !presentation.showsHelpText + } + + private func updateAdditionalDataNoteVisibility() { + additionalDataNoteLabel.isHidden = additionalDataCheckbox.state != .on + } + + private func updatePanelSize() { + let contentSize = preferredContentSize() + let targetFrame = panel.frameRect(forContentRect: NSRect(origin: .zero, size: contentSize)) + + guard panel.frame.size != targetFrame.size else { return } + if !panel.isVisible { + panel.setContentSize(contentSize) + return + } + + var frame = panel.frame + frame.origin.x -= (targetFrame.width - frame.width) / 2 + frame.origin.y -= (targetFrame.height - frame.height) / 2 + frame.size = targetFrame.size + panel.setFrame(frame, display: true) + } + + private func preferredContentSize() -> NSSize { + switch step { + case .source: + return NSSize(width: 560, height: 292) + case .sourceProfiles: + let presentation = BrowserImportSourceProfilesPresentation(profileCount: selectedBrowser().profiles.count) + let helpHeight: CGFloat = presentation.showsHelpText ? 24 : 0 + let height = 214 + presentation.scrollHeight + helpHeight + return NSSize(width: 560, height: min(max(height, 292), 360)) + case .dataTypes: + var height: CGFloat = currentExecutionPlan().mode == .separateProfiles ? 412 : 374 + if additionalDataCheckbox.state == .on { + height += 24 + } + return NSSize(width: 560, height: height) + } + } + private func finishModal(with response: NSApplication.ModalResponse) { guard !didFinishModal else { return } didFinishModal = true diff --git a/cmuxTests/BrowserImportMappingTests.swift b/cmuxTests/BrowserImportMappingTests.swift index 1f6c662c..2f122921 100644 --- a/cmuxTests/BrowserImportMappingTests.swift +++ b/cmuxTests/BrowserImportMappingTests.swift @@ -127,6 +127,23 @@ final class BrowserImportMappingTests: XCTestCase { XCTAssertTrue(presentation.showsSingleDestinationPicker) } + func testSourceProfilesPresentationShrinksListForSmallProfileCounts() { + let presentation = BrowserImportSourceProfilesPresentation(profileCount: 2) + + XCTAssertEqual(presentation.scrollHeight, 76) + XCTAssertTrue(presentation.showsHelpText) + } + + func testSourceProfilesPresentationCapsListHeightAndHidesHelpForSingleProfile() { + let singleProfilePresentation = BrowserImportSourceProfilesPresentation(profileCount: 1) + let manyProfilesPresentation = BrowserImportSourceProfilesPresentation(profileCount: 9) + + XCTAssertEqual(singleProfilePresentation.scrollHeight, 76) + XCTAssertFalse(singleProfilePresentation.showsHelpText) + XCTAssertEqual(manyProfilesPresentation.scrollHeight, 144) + XCTAssertTrue(manyProfilesPresentation.showsHelpText) + } + @MainActor func testRealizePlanCreatesMissingDestinationProfilesOnlyWhenRequested() throws { let suiteName = "BrowserImportMappingTests-\(UUID().uuidString)" diff --git a/cmuxUITests/BrowserImportProfilesUITests.swift b/cmuxUITests/BrowserImportProfilesUITests.swift index feb55471..8ba0e7d6 100644 --- a/cmuxUITests/BrowserImportProfilesUITests.swift +++ b/cmuxUITests/BrowserImportProfilesUITests.swift @@ -19,10 +19,10 @@ final class BrowserImportProfilesUITests: XCTestCase { app.buttons["Next"].click() XCTAssertTrue( - app.radioButtons["Keep profiles separate"].waitForExistence(timeout: 5.0), + app.radioButtons["Separate profiles"].waitForExistence(timeout: 5.0), "Expected Step 3 to show the separate-profiles default" ) - XCTAssertTrue(app.radioButtons["Merge all into one cmux profile"].exists) + XCTAssertTrue(app.radioButtons["Merge into one"].exists) XCTAssertTrue(app.popUpButtons["BrowserImportDestinationPopup-you"].exists) XCTAssertTrue(app.popUpButtons["BrowserImportDestinationPopup-austin"].exists) @@ -49,7 +49,7 @@ final class BrowserImportProfilesUITests: XCTestCase { app.buttons["Next"].click() app.buttons["Next"].click() - let mergeRadio = app.radioButtons["Merge all into one cmux profile"] + let mergeRadio = app.radioButtons["Merge into one"] XCTAssertTrue(mergeRadio.waitForExistence(timeout: 5.0)) mergeRadio.click()