Skip to content

Commit d81cf82

Browse files
committed
Merge branch 'feature/detach-chat-not-stay-at-top' into develop
2 parents 9326772 + 63bc41e commit d81cf82

7 files changed

Lines changed: 398 additions & 273 deletions

File tree

Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ struct ChatSettingsView: View {
2222
@AppStorage(\.chatModels) var chatModels
2323
@AppStorage(\.embeddingModels) var embeddingModels
2424
@AppStorage(\.wrapCodeInChatCodeBlock) var wrapCodeInCodeBlock
25+
@AppStorage(\.keepFloatOnTopIfChatPanelAndXcodeOverlaps) var keepFloatOnTopIfChatPanelAndXcodeOverlaps
26+
@AppStorage(\.disableFloatOnTopWhenTheChatPanelIsDetached) var disableFloatOnTopWhenTheChatPanelIsDetached
2527

2628
init() {}
2729
}
@@ -157,6 +159,14 @@ struct ChatSettingsView: View {
157159
Toggle(isOn: $settings.wrapCodeInCodeBlock) {
158160
Text("Wrap code in code block")
159161
}
162+
163+
Toggle(isOn: $settings.disableFloatOnTopWhenTheChatPanelIsDetached) {
164+
Text("Disable float on top when the chat panel is detached")
165+
}
166+
167+
Toggle(isOn: $settings.keepFloatOnTopIfChatPanelAndXcodeOverlaps) {
168+
Text("But, keep float on top if chat panel and Xcode overlaps")
169+
}
160170

161171
#if canImport(ProHostApp)
162172

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import AppKit
2+
import ChatTab
3+
import Combine
4+
import ComposableArchitecture
5+
import Foundation
6+
import SwiftUI
7+
8+
final class ChatPanelWindow: NSWindow {
9+
override var canBecomeKey: Bool { true }
10+
override var canBecomeMain: Bool { true }
11+
12+
private var cancellable: Set<AnyCancellable> = []
13+
14+
var minimizeWindow: () -> Void = {}
15+
16+
init(
17+
store: StoreOf<ChatPanelFeature>,
18+
chatTabPool: ChatTabPool,
19+
minimizeWindow: @escaping () -> Void
20+
) {
21+
self.minimizeWindow = minimizeWindow
22+
super.init(
23+
contentRect: .zero,
24+
styleMask: [.resizable, .titled, .miniaturizable, .fullSizeContentView],
25+
backing: .buffered,
26+
defer: false
27+
)
28+
29+
titleVisibility = .hidden
30+
addTitlebarAccessoryViewController({
31+
let controller = NSTitlebarAccessoryViewController()
32+
let view = NSHostingView(rootView: ChatTitleBar(store: store))
33+
controller.view = view
34+
view.frame = .init(x: 0, y: 0, width: 100, height: 40)
35+
controller.layoutAttribute = .right
36+
return controller
37+
}())
38+
titlebarAppearsTransparent = true
39+
isReleasedWhenClosed = false
40+
isOpaque = false
41+
backgroundColor = .clear
42+
level = .init(NSWindow.Level.floating.rawValue + 1)
43+
collectionBehavior = [
44+
.fullScreenAuxiliary,
45+
.transient,
46+
.fullScreenPrimary,
47+
.fullScreenAllowsTiling,
48+
]
49+
hasShadow = true
50+
contentView = NSHostingView(
51+
rootView: ChatWindowView(
52+
store: store,
53+
toggleVisibility: { [weak self] isDisplayed in
54+
guard let self else { return }
55+
self.isPanelDisplayed = isDisplayed
56+
}
57+
)
58+
.environment(\.chatTabPool, chatTabPool)
59+
)
60+
setIsVisible(true)
61+
isPanelDisplayed = false
62+
63+
let viewStore = ViewStore(store)
64+
viewStore.publisher
65+
.map(\.isDetached)
66+
.receive(on: DispatchQueue.main)
67+
.sink { [weak self] isDetached in
68+
guard let self else { return }
69+
if UserDefaults.shared.value(for: \.disableFloatOnTopWhenTheChatPanelIsDetached) {
70+
self.setFloatOnTop(!isDetached)
71+
} else {
72+
self.setFloatOnTop(true)
73+
}
74+
}.store(in: &cancellable)
75+
}
76+
77+
func setFloatOnTop(_ isFloatOnTop: Bool) {
78+
let targetLevel: NSWindow.Level = isFloatOnTop
79+
? .init(NSWindow.Level.floating.rawValue + 1)
80+
: .normal
81+
82+
if targetLevel != level {
83+
level = targetLevel
84+
}
85+
}
86+
87+
var isWindowHidden: Bool = false {
88+
didSet {
89+
alphaValue = isPanelDisplayed && !isWindowHidden ? 1 : 0
90+
}
91+
}
92+
93+
var isPanelDisplayed: Bool = false {
94+
didSet {
95+
alphaValue = isPanelDisplayed && !isWindowHidden ? 1 : 0
96+
}
97+
}
98+
99+
override var alphaValue: CGFloat {
100+
didSet {
101+
ignoresMouseEvents = alphaValue <= 0
102+
}
103+
}
104+
105+
override func miniaturize(_: Any?) {
106+
minimizeWindow()
107+
}
108+
}
109+

Core/Sources/SuggestionWidget/ChatWindowView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ struct ChatTitleBar: View {
7979

8080
Spacer()
8181

82-
WithViewStore(store, observe: { $0.chatPanelInASeparateWindow }) { viewStore in
82+
WithViewStore(store, observe: { $0.isDetached }) { viewStore in
8383
TrafficLightButton(
8484
isHovering: isHovering,
8585
isActive: viewStore.state,

Core/Sources/SuggestionWidget/FeatureReducers/ChatPanelFeature.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public struct ChatPanelFeature: ReducerProtocol {
4848
public var chatTabGroup = ChatTabGroup()
4949
var colorScheme: ColorScheme = .light
5050
public internal(set) var isPanelDisplayed = false
51-
var chatPanelInASeparateWindow = false
51+
var isDetached = false
5252
var isFullScreen = false
5353
}
5454

@@ -118,17 +118,17 @@ public struct ChatPanelFeature: ReducerProtocol {
118118
return .none
119119

120120
case .toggleChatPanelDetachedButtonClicked:
121-
if state.isFullScreen, state.chatPanelInASeparateWindow {
121+
if state.isFullScreen, state.isDetached {
122122
return .run { send in
123123
await send(.attachChatPanel)
124124
}
125125
}
126126

127-
state.chatPanelInASeparateWindow.toggle()
127+
state.isDetached.toggle()
128128
return .none
129129

130130
case .detachChatPanel:
131-
state.chatPanelInASeparateWindow = true
131+
state.isDetached = true
132132
return .none
133133

134134
case .attachChatPanel:
@@ -140,7 +140,7 @@ public struct ChatPanelFeature: ReducerProtocol {
140140
}
141141
}
142142

143-
state.chatPanelInASeparateWindow = false
143+
state.isDetached = false
144144
return .none
145145

146146
case .enterFullScreen:
@@ -155,7 +155,7 @@ public struct ChatPanelFeature: ReducerProtocol {
155155

156156
case let .presentChatPanel(forceDetach):
157157
if forceDetach {
158-
state.chatPanelInASeparateWindow = true
158+
state.isDetached = true
159159
}
160160
state.isPanelDisplayed = true
161161
return .run { send in

Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ public struct WidgetFeature: ReducerProtocol {
6565
}(),
6666
isContentEmpty: chatPanelState.chatTabGroup.tabInfo.isEmpty
6767
&& panelState.sharedPanelState.isEmpty,
68-
isChatPanelDetached: chatPanelState.chatPanelInASeparateWindow,
68+
isChatPanelDetached: chatPanelState.isDetached,
6969
isChatOpen: chatPanelState.isPanelDisplayed
7070
)
7171
}
@@ -191,7 +191,7 @@ public struct WidgetFeature: ReducerProtocol {
191191
Reduce { state, action in
192192
switch action {
193193
case .chatPanel(.presentChatPanel):
194-
let isDetached = state.chatPanelState.chatPanelInASeparateWindow
194+
let isDetached = state.chatPanelState.isDetached
195195
return .run { _ in
196196
await windowsController?.updateWindowLocation(
197197
animated: false,
@@ -206,7 +206,7 @@ public struct WidgetFeature: ReducerProtocol {
206206
}
207207

208208
case .chatPanel(.toggleChatPanelDetachedButtonClicked):
209-
let isDetached = state.chatPanelState.chatPanelInASeparateWindow
209+
let isDetached = state.chatPanelState.isDetached
210210
return .run { _ in
211211
await windowsController?.updateWindowLocation(
212212
animated: !isDetached,

0 commit comments

Comments
 (0)