Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@ import Combine
final class AIChatOmnibarTextContainerViewController: NSViewController, ThemeUpdateListening, NSTextViewDelegate {

private enum Constants {
static let bottomPadding: CGFloat = 54.0
static let minimumPanelHeight: CGFloat = 100.0
static let bottomPadding: CGFloat = 34.0
static let minimumPanelHeight: CGFloat = 60
static let maximumPanelHeight: CGFloat = 512.0
static let dividerLeadingOffset: CGFloat = -9.0
static let dividerTrailingOffset: CGFloat = 77.0
static let dividerTopOffset: CGFloat = 8.0
static let dividerTopOffset: CGFloat = -10.0
}

private let backgroundView = MouseBlockingBackgroundView()
Expand Down Expand Up @@ -155,7 +155,7 @@ final class AIChatOmnibarTextContainerViewController: NSViewController, ThemeUpd
backgroundView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -4),
backgroundView.bottomAnchor.constraint(equalTo: view.bottomAnchor),

containerView.topAnchor.constraint(equalTo: backgroundView.topAnchor),
containerView.topAnchor.constraint(equalTo: backgroundView.topAnchor, constant: 1.0),
containerView.leadingAnchor.constraint(equalTo: backgroundView.leadingAnchor),
containerView.trailingAnchor.constraint(equalTo: backgroundView.trailingAnchor),
containerView.bottomAnchor.constraint(equalTo: backgroundView.bottomAnchor),
Expand Down Expand Up @@ -254,7 +254,7 @@ final class AIChatOmnibarTextContainerViewController: NSViewController, ThemeUpd
let usedRect = layoutManager.usedRect(for: textContainer)
let textInsets = textView.textContainerInset
let bottomSpacing: CGFloat = Constants.bottomPadding
let totalHeight = usedRect.height + textInsets.height + textInsets.height + 20 + bottomSpacing
let totalHeight = usedRect.height + textInsets.height + bottomSpacing

return min(totalHeight, Constants.maximumPanelHeight)
}
Expand Down Expand Up @@ -300,6 +300,10 @@ final class AIChatOmnibarTextContainerViewController: NSViewController, ThemeUpd
view.window?.makeFirstResponder(textView)
}

func insertNewline() {
textView.insertNewlineIgnoringFieldEditor(nil)
}

func updateScrollingBehavior(maxHeight: CGFloat) {
let desiredHeight = calculateDesiredPanelHeight()
let effectiveMaxHeight = min(maxHeight, Constants.maximumPanelHeight)
Expand Down
2 changes: 1 addition & 1 deletion macOS/DuckDuckGo/MainWindow/MainView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ final class MainView: NSView {
static let findInPageContainerTopOffset: CGFloat = -4
static let fireContainerHeight: CGFloat = 32
static let bannerHeight: CGFloat = 48
static let aiChatOmnibarContainerMinHeight: CGFloat = 100
static let aiChatOmnibarContainerMinHeight: CGFloat = 60
static let aiChatOmnibarContainerPadding: CGFloat = 50
}

Expand Down
4 changes: 4 additions & 0 deletions macOS/DuckDuckGo/MainWindow/MainViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1009,7 +1009,11 @@ extension MainViewController {
if flags.contains(.shift) || flags.contains(.option),
featureFlagger.isFeatureOn(.aiChatOmnibarToggle),
let buttonsViewController = navigationBarViewController.addressBarViewController?.addressBarButtonsViewController {
let isSwitchingToAIChatMode = buttonsViewController.searchModeToggleControl?.selectedSegment == 0
buttonsViewController.toggleSearchMode()
if isSwitchingToAIChatMode {
self.aiChatOmnibarTextContainerViewController.insertNewline()
}
return true
} else if flags.contains(.control),
featureFlagger.isFeatureOn(.aiChatOmnibarToggle) {
Expand Down
16 changes: 16 additions & 0 deletions macOS/DuckDuckGo/NavigationBar/AddressBarSharedTextState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,28 @@ final class AddressBarSharedTextState: ObservableObject {
/// Whether the user has typed anything (triggers text sharing between modes)
@Published private(set) var hasUserInteractedWithText: Bool = false

/// Whether the user has type anything after switching modes
private(set) var hasUserInteractedWithTextAfterSwitchingModes: Bool = false

/// Resets the shared state to initial values
func reset() {
text = ""
selectionRange = NSRange(location: 0, length: 0)
hasUserInteractedWithText = false
}

func resetUserInteraction() {
hasUserInteractedWithText = false
}

func setHasUserInteractedWithTextAfterSwitchingModes(_ value: Bool) {
hasUserInteractedWithTextAfterSwitchingModes = value
}

func resetUserInteractionAfterSwitchingModes() {
hasUserInteractedWithTextAfterSwitchingModes = false
}

/// Updates the shared text content
/// - Parameters:
/// - newText: The new text value
Expand All @@ -47,6 +62,7 @@ final class AddressBarSharedTextState: ObservableObject {
text = newText
if markInteraction && !newText.isEmpty {
hasUserInteractedWithText = true
hasUserInteractedWithTextAfterSwitchingModes = true
}

// Adjust selection range if it's now beyond the text length
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -928,6 +928,12 @@ final class AddressBarButtonsViewController: NSViewController {
return
}

if isTextFieldEditorFirstResponder && featureFlagger.isFeatureOn(.aiChatOmnibarToggle) {
bookmarkButton.isShown = false
updateAIChatDividerVisibility()
return
}

let hasEmptyAddressBar = textFieldValue?.isEmpty ?? true
var shouldShowBookmarkButton: Bool {
guard let tabViewModel, tabViewModel.canBeBookmarked else { return false }
Expand Down
19 changes: 13 additions & 6 deletions macOS/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -684,10 +684,12 @@ final class AddressBarViewController: NSViewController {
&& aiChatSettings.isAIFeaturesEnabled
&& aiChatSettings.showSearchAndDuckAIToggle

if shouldShowDuckAIHint {
addressBarPlaceholder = UserText.addressBarPlaceholderWithDuckAI
} else if isNewTab {
addressBarPlaceholder = UserText.addressBarPlaceholder
if isNewTab {
if shouldShowDuckAIHint {
addressBarPlaceholder = UserText.addressBarPlaceholderWithDuckAI
} else {
addressBarPlaceholder = UserText.addressBarPlaceholder
}
} else {
addressBarPlaceholder = ""
}
Expand Down Expand Up @@ -871,6 +873,8 @@ final class AddressBarViewController: NSViewController {
delegate?.resizeAddressBarForHomePage(self)
addressBarButtonsViewController?.setupButtonPaddings(isFocused: false)
}

setupAddressBarPlaceHolder()
}

private func handleFirstResponderChange() {
Expand Down Expand Up @@ -1091,7 +1095,7 @@ extension AddressBarViewController: AddressBarButtonsViewControllerDelegate {
if isAIChatMode {
if mode.isEditing {
let text = addressBarTextField.stringValueWithoutSuffix
if !text.isEmpty {
if !text.isEmpty && sharedTextState.hasUserInteractedWithTextAfterSwitchingModes == true {
sharedTextState.updateText(text, markInteraction: false)
}
}
Expand All @@ -1111,11 +1115,14 @@ extension AddressBarViewController: AddressBarButtonsViewControllerDelegate {
updateMode()
addressBarTextField.makeMeFirstResponder()

/// Force layout update after becoming first responder to update in case the window was resized
layoutTextFields(withMinX: addressBarButtonsViewController.buttonsWidth)

if shouldRestoreFromSharedState {
addressBarTextField.setCursorPositionAfterRestore()
}
}

sharedTextState.resetUserInteractionAfterSwitchingModes()
delegate?.addressBarViewControllerSearchModeToggleChanged(self, isAIChatMode: isAIChatMode)
}

Expand Down
36 changes: 27 additions & 9 deletions macOS/DuckDuckGo/Suggestions/View/SuggestionViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,14 @@ final class SuggestionViewController: NSViewController {
}

private var suggestionResultCancellable: AnyCancellable?
private var selectionIndexCancellable: AnyCancellable?
private var selectionSyncCancellable: AnyCancellable?

private var eventMonitorCancellables = Set<AnyCancellable>()
private var appObserver: Any?

/// Flag to prevent re-entrancy when programmatically updating table selection
private var isUpdatingTableSelection = false

override func viewDidLoad() {
super.viewDidLoad()

Expand All @@ -79,7 +82,7 @@ final class SuggestionViewController: NSViewController {
setupTableView()
addTrackingArea()
subscribeToSuggestionResult()
subscribeToSelectionIndex()
subscribeToSelectionSync()
subscribeToThemeChanges()

applyThemeStyle()
Expand Down Expand Up @@ -157,11 +160,14 @@ final class SuggestionViewController: NSViewController {
}
}

private func subscribeToSelectionIndex() {
selectionIndexCancellable = suggestionContainerViewModel.$selectedRowIndex.receive(on: DispatchQueue.main).sink { [weak self] selectedRowIndex in
guard let self else { return }
self.selectTableRow(at: selectedRowIndex)
}
/// Subscribes to view model selection changes (e.g., from keyboard navigation)
private func subscribeToSelectionSync() {
selectionSyncCancellable = suggestionContainerViewModel.$selectedRowIndex
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
guard let self, !self.isUpdatingTableSelection else { return }
self.syncTableSelectionWithViewModel()
}
}

private func displayNewSuggestions() {
Expand All @@ -185,24 +191,32 @@ final class SuggestionViewController: NSViewController {
suggestionContainerViewModel.selectRow(at: selectedRowCache)
}

self.selectTableRow(at: self.suggestionContainerViewModel.selectedRowIndex)
syncTableSelectionWithViewModel()
}
}

func syncTableSelectionWithViewModel() {
selectTableRow(at: suggestionContainerViewModel.selectedRowIndex)
}

private func selectTableRow(at rowIndex: Int?) {
if tableView.selectedRow == rowIndex {
if let rowIndex, let cell = tableView.view(atColumn: 0, row: rowIndex, makeIfNecessary: false) as? SuggestionTableCellView {
// Show the delete button if necessary
cell.updateDeleteImageViewVisibility()
}
return
}

isUpdatingTableSelection = true
defer { isUpdatingTableSelection = false }

guard let rowIndex,
rowIndex >= 0,
rowIndex < suggestionContainerViewModel.numberOfRows else {
if let defaultRow = suggestionContainerViewModel.defaultSelectedRow {
tableView.selectRowIndexes(IndexSet(integer: defaultRow), byExtendingSelection: false)
// Sync view model with the default selection so keyboard navigation works correctly
suggestionContainerViewModel.selectRow(at: defaultRow)
} else {
self.clearSelection()
}
Expand All @@ -218,6 +232,7 @@ final class SuggestionViewController: NSViewController {

guard tableRow >= 0 else {
suggestionContainerViewModel.clearRowSelection()
syncTableSelectionWithViewModel()
return
}

Expand All @@ -226,6 +241,7 @@ final class SuggestionViewController: NSViewController {
}

suggestionContainerViewModel.selectRow(at: tableRow)
syncTableSelectionWithViewModel()
}

private func clearSelection() {
Expand Down Expand Up @@ -424,6 +440,8 @@ extension SuggestionViewController: NSTableViewDelegate {
}

func tableViewSelectionDidChange(_ notification: Notification) {
guard !isUpdatingTableSelection else { return }

if tableView.selectedRow == -1 {
suggestionContainerViewModel.clearRowSelection()
return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -253,14 +253,21 @@ final class SuggestionContainerViewModel {
func selectRow(at rowIndex: Int) {
guard rowIndex >= 0, rowIndex < numberOfRows else {
Logger.general.error("SuggestionContainerViewModel: Row index out of bounds")
selectedRowIndex = nil
if selectedRowIndex != nil {
selectedRowIndex = nil
selectionIndex = nil
}
return
}

guard selectedRowIndex != rowIndex else { return }

selectedRowIndex = rowIndex
selectionIndex = selectionIndex(forRow: rowIndex)
}

func clearRowSelection() {
guard selectedRowIndex != nil || selectionIndex != nil else { return }
selectedRowIndex = nil
selectionIndex = nil
}
Expand Down
8 changes: 4 additions & 4 deletions macOS/DuckDuckGo/VisualRefresh/AddressBarStyleProviding.swift
Original file line number Diff line number Diff line change
Expand Up @@ -132,14 +132,14 @@ final class CurrentAddressBarStyleProvider: AddressBarStyleProviding {
private let navigationBarHeightForHomePage: CGFloat = 52
private let navigationBarHeightForPopUpWindow: CGFloat = 42
private let addressBarTopPaddingForDefault: CGFloat = 7
private let addressBarTopPaddingForDefaultFocusedWithAIChat: CGFloat = 4
private let addressBarTopPaddingForDefaultFocusedWithAIChat: CGFloat = 3
private let addressBarTopPaddingForHomePage: CGFloat = 7
private let addressBarTopPaddingForHomePageFocusedWithAIChat: CGFloat = 4
private let addressBarTopPaddingForHomePageFocusedWithAIChat: CGFloat = 3
private let addressBarTopPaddingForPopUpWindow: CGFloat = 7
private let addressBarBottomPaddingForDefault: CGFloat = 7
private let addressBarBottomPaddingForDefaultFocusedWithAIChat: CGFloat = 4
private let addressBarBottomPaddingForDefaultFocusedWithAIChat: CGFloat = 3
private let addressBarBottomPaddingForHomePage: CGFloat = 7
private let addressBarBottomPaddingForHomePageFocusedWithAIChat: CGFloat = 4
private let addressBarBottomPaddingForHomePageFocusedWithAIChat: CGFloat = 3
private let addressBarBottomPaddingForPopUpWindow: CGFloat = 7

private let featureFlagger: FeatureFlagger
Expand Down
Loading