@@ -7,7 +7,7 @@ import Preferences
77import SwiftUI
88
99@MainActor
10- public final class SuggestionWidgetController {
10+ public final class SuggestionWidgetController : NSObject {
1111 class UserDefaultsObserver : NSObject {
1212 var onChange : ( ( ) -> Void ) ?
1313
@@ -22,7 +22,7 @@ public final class SuggestionWidgetController {
2222 }
2323
2424 private lazy var widgetWindow = {
25- let it = NSWindow (
25+ let it = CanBecomeKeyWindow (
2626 contentRect: . zero,
2727 styleMask: . borderless,
2828 backing: . buffered,
@@ -37,6 +37,7 @@ public final class SuggestionWidgetController {
3737 rootView: WidgetView (
3838 viewModel: widgetViewModel,
3939 panelViewModel: suggestionPanelViewModel,
40+ chatWindowViewModel: chatWindowViewModel,
4041 onOpenChatClicked: { [ weak self] in
4142 self ? . onOpenChatClicked ( )
4243 } ,
@@ -46,11 +47,12 @@ public final class SuggestionWidgetController {
4647 )
4748 )
4849 it. setIsVisible ( true )
50+ it. canBecomeKeyChecker = { false }
4951 return it
5052 } ( )
5153
5254 private lazy var tabWindow = {
53- let it = NSWindow (
55+ let it = CanBecomeKeyWindow (
5456 contentRect: . zero,
5557 styleMask: . borderless,
5658 backing: . buffered,
@@ -62,9 +64,10 @@ public final class SuggestionWidgetController {
6264 it. level = . floating
6365 it. hasShadow = true
6466 it. contentView = NSHostingView (
65- rootView: TabView ( panelViewModel : suggestionPanelViewModel )
67+ rootView: TabView ( chatWindowViewModel : chatWindowViewModel )
6668 )
6769 it. setIsVisible ( true )
70+ it. canBecomeKeyChecker = { false }
6871 return it
6972 } ( )
7073
@@ -85,15 +88,14 @@ public final class SuggestionWidgetController {
8588 )
8689 it. setIsVisible ( true )
8790 it. canBecomeKeyChecker = { [ suggestionPanelViewModel] in
88- if case . chat = suggestionPanelViewModel. activeTab { return true }
8991 if case . promptToCode = suggestionPanelViewModel. content { return true }
9092 return false
9193 }
9294 return it
9395 } ( )
9496
9597 private lazy var chatWindow = {
96- let it = CanBecomeKeyWindow (
98+ let it = ChatWindow (
9799 contentRect: . zero,
98100 styleMask: [ . borderless, . resizable] ,
99101 backing: . buffered,
@@ -108,7 +110,7 @@ public final class SuggestionWidgetController {
108110 rootView: ChatWindowView ( viewModel: chatWindowViewModel)
109111 )
110112 it. setIsVisible ( true )
111- it. canBecomeKeyChecker = { true }
113+ it. delegate = self
112114 return it
113115 } ( )
114116
@@ -118,6 +120,7 @@ public final class SuggestionWidgetController {
118120
119121 private var presentationModeChangeObserver = UserDefaultsObserver ( )
120122 private var colorSchemeChangeObserver = UserDefaultsObserver ( )
123+ private var detachChatPanelObserver = UserDefaultsObserver ( )
121124 private var windowChangeObservationTask : Task < Void , Error > ?
122125 private var activeApplicationMonitorTask : Task < Void , Error > ?
123126 private var sourceEditorMonitorTask : Task < Void , Error > ?
@@ -128,7 +131,8 @@ public final class SuggestionWidgetController {
128131 public var onCustomCommandClicked : ( CustomCommand ) -> Void = { _ in }
129132 public var dataSource : SuggestionWidgetDataSource ?
130133
131- public nonisolated init ( ) {
134+ override public nonisolated init ( ) {
135+ super. init ( )
132136 #warning(
133137 " TODO: A test is initializing this class for unknown reasons, try a better way to avoid this. "
134138 )
@@ -156,6 +160,9 @@ public final class SuggestionWidgetController {
156160 self . widgetWindow. alphaValue = 0
157161 self . panelWindow. alphaValue = 0
158162 self . tabWindow. alphaValue = 0
163+ if !UserDefaults. shared. value ( for: \. chatPanelInASeparateWindow) {
164+ self . chatWindow. alphaValue = 0
165+ }
159166 }
160167 }
161168 }
@@ -176,6 +183,19 @@ public final class SuggestionWidgetController {
176183 )
177184 }
178185
186+ Task { @MainActor in
187+ detachChatPanelObserver. onChange = { [ weak self] in
188+ guard let self else { return }
189+ self . updateWindowLocation ( animated: true )
190+ }
191+ UserDefaults . shared. addObserver (
192+ detachChatPanelObserver,
193+ forKeyPath: UserDefaultPreferenceKeys ( ) . chatPanelInASeparateWindow. key,
194+ options: . new,
195+ context: nil
196+ )
197+ }
198+
179199 Task { @MainActor in
180200 let updateColorScheme = { @MainActor [ weak self] in
181201 guard let self else { return }
@@ -194,6 +214,7 @@ public final class SuggestionWidgetController {
194214 }
195215 } ( )
196216 self . suggestionPanelViewModel. colorScheme = self . colorScheme
217+ self . chatWindowViewModel. colorScheme = self . colorScheme
197218 Task {
198219 await self . updateContentForActiveEditor ( )
199220 }
@@ -218,33 +239,6 @@ public final class SuggestionWidgetController {
218239 context: nil
219240 )
220241 }
221-
222- // Task { @MainActor in
223- // var switchTask: Task<Void, Error>?
224- // suggestionPanelViewModel.requestApplicationPolicyUpdate = { viewModel in
225- // #warning("""
226- // TODO: There should be a better way for that
227- // Currently, we have to make the app an accessory so that we can type things in the
228- // chat mode.
229- // But in other modes, we want to keep it prohibited so the helper app won't take
230- // over the focus.
231- // """)
232- // switch (viewModel.activeTab, viewModel.content) {
233- // case (.chat, _), (.suggestion, .promptToCode):
234- // guard NSApp.activationPolicy() != .accessory else { return }
235- // switchTask?.cancel()
236- // NSApp.setActivationPolicy(.accessory)
237- // case (.suggestion, _):
238- // guard NSApp.activationPolicy() != .prohibited else { return }
239- // switchTask?.cancel()
240- // switchTask = Task {
241- // try await Environment.makeXcodeActive()
242- // try Task.checkCancellation()
243- // NSApp.setActivationPolicy(.prohibited)
244- // }
245- // }
246- // }
247- // }
248242 }
249243}
250244
@@ -256,6 +250,7 @@ public extension SuggestionWidgetController {
256250 Task {
257251 if let suggestion = await dataSource? . suggestionForFile ( at: fileURL) {
258252 suggestionPanelViewModel. content = . suggestion( suggestion)
253+ chatWindowViewModel. isPanelDisplayed = true
259254 suggestionPanelViewModel. isPanelDisplayed = true
260255 }
261256 }
@@ -274,6 +269,7 @@ public extension SuggestionWidgetController {
274269
275270 func presentError( _ errorDescription: String ) {
276271 suggestionPanelViewModel. content = . error( errorDescription)
272+ chatWindowViewModel. isPanelDisplayed = true
277273 suggestionPanelViewModel. isPanelDisplayed = true
278274 widgetViewModel. isProcessing = false
279275 }
@@ -283,8 +279,9 @@ public extension SuggestionWidgetController {
283279 Task {
284280 if let chat = await dataSource? . chatForFile ( at: fileURL) {
285281 chatWindowViewModel. chat = chat
286- suggestionPanelViewModel . chat = chat
282+ chatWindowViewModel . isPanelDisplayed = true
287283 suggestionPanelViewModel. isPanelDisplayed = true
284+ suggestionPanelViewModel. chat = chat
288285
289286 if UserDefaults . shared. value ( for: \. chatPanelInASeparateWindow) {
290287 self . updateWindowLocation ( )
@@ -311,6 +308,7 @@ public extension SuggestionWidgetController {
311308 Task {
312309 if let provider = await dataSource? . promptToCodeForFile ( at: fileURL) {
313310 suggestionPanelViewModel. content = . promptToCode( provider)
311+ chatWindowViewModel. isPanelDisplayed = true
314312 suggestionPanelViewModel. isPanelDisplayed = true
315313
316314 Task { @MainActor in
@@ -449,14 +447,19 @@ extension SuggestionWidgetController {
449447 if tabWindow. alphaValue != 0 {
450448 tabWindow. alphaValue = 0
451449 }
450+ if !UserDefaults. shared. value ( for: \. chatPanelInASeparateWindow) {
451+ if chatWindow. alphaValue != 0 {
452+ chatWindow. alphaValue = 0
453+ }
454+ }
452455 }
453456
454457 guard UserDefaults . shared. value ( for: \. suggestionPresentationMode) == . floatingWidget
455458 else {
456459 hide ( )
457460 return
458461 }
459-
462+
460463 let detachChat = UserDefaults . shared. value ( for: \. chatPanelInASeparateWindow)
461464
462465 if detachChat {
@@ -498,9 +501,16 @@ extension SuggestionWidgetController {
498501 tabWindow. setFrame ( result. tabFrame, display: false , animate: animated)
499502 suggestionPanelViewModel. alignTopToAnchor = result. alignPanelTopToAnchor
500503 }
501-
502- if chatWindow. alphaValue == 0 {
504+
505+ if detachChat {
506+ if chatWindow. alphaValue == 0 {
507+ chatWindow. setFrame ( panelWindow. frame, display: false , animate: false )
508+ }
509+ } else {
503510 chatWindow. setFrame ( panelWindow. frame, display: false , animate: false )
511+ if chatWindow. alphaValue != 1 {
512+ chatWindow. alphaValue = 1
513+ }
504514 }
505515
506516 if panelWindow. alphaValue != 1 {
@@ -552,8 +562,70 @@ extension SuggestionWidgetController {
552562 }
553563}
554564
565+ extension SuggestionWidgetController : NSWindowDelegate {
566+ public func windowWillMove( _ notification: Notification ) {
567+ guard ( notification. object as? NSWindow ) === chatWindow else { return }
568+ Task { @MainActor in
569+ await Task . yield ( )
570+ guard chatWindow. isBeingDragged else { return }
571+ UserDefaults . shared. set ( true , for: \. chatPanelInASeparateWindow)
572+ }
573+ }
574+
575+ public func windowWillResize( _ sender: NSWindow , to frameSize: NSSize ) -> NSSize {
576+ guard sender === chatWindow else { return frameSize }
577+ Task { @MainActor in
578+ await Task . yield ( )
579+ guard chatWindow. isBeingDragged else { return }
580+ UserDefaults . shared. set ( true , for: \. chatPanelInASeparateWindow)
581+ }
582+ return frameSize
583+ }
584+ }
585+
555586class CanBecomeKeyWindow : NSWindow {
556587 var canBecomeKeyChecker : ( ) -> Bool = { true }
557588 override var canBecomeKey : Bool { canBecomeKeyChecker ( ) }
558589 override var canBecomeMain : Bool { canBecomeKeyChecker ( ) }
559590}
591+
592+ class ChatWindow : NSWindow {
593+ override var canBecomeKey : Bool { true }
594+ override var canBecomeMain : Bool { true }
595+ var isBeingDragged : Bool = false
596+ var dragStartLocation = NSPoint . zero
597+
598+ override func mouseDragged( with event: NSEvent ) {
599+ isBeingDragged = true
600+
601+ guard dragStartLocation != . zero else { return }
602+
603+ let screenVisibleFrame = NSScreen . main? . visibleFrame
604+ let windowFrame = frame
605+ var newOrigin = windowFrame. origin
606+
607+ let currentLocation = event. locationInWindow
608+ if dragStartLocation. y > windowFrame. size. height - 40 {
609+ newOrigin. x += ( currentLocation. x - dragStartLocation. x)
610+ newOrigin. y += ( currentLocation. y - dragStartLocation. y)
611+
612+ if ( newOrigin. y + windowFrame. size. height) >
613+ ( screenVisibleFrame? . origin. y ?? 0 ) + ( screenVisibleFrame? . size. height ?? 0 )
614+ {
615+ newOrigin. y = screenVisibleFrame? . origin
616+ . y ?? 0 + ( screenVisibleFrame? . size. height ?? 0 - windowFrame. size. height)
617+ }
618+ UserDefaults . shared. set ( true , for: \. chatPanelInASeparateWindow)
619+ setFrameOrigin ( newOrigin)
620+ }
621+ }
622+
623+ override func mouseUp( with event: NSEvent ) {
624+ isBeingDragged = false
625+ dragStartLocation = . zero
626+ }
627+
628+ override func mouseDown( with event: NSEvent ) {
629+ dragStartLocation = event. locationInWindow
630+ }
631+ }
0 commit comments