8
8
import Cocoa
9
9
import SwiftUI
10
10
11
- struct CodeEditSplitView : NSViewControllerRepresentable {
12
- let controller : NSSplitViewController
13
-
14
- func makeNSViewController( context: Context ) -> NSSplitViewController {
15
- controller
16
- }
17
-
18
- func updateNSViewController( _ nsViewController: NSSplitViewController , context: Context ) { }
19
- }
20
-
21
- private extension CGFloat {
11
+ final class CodeEditSplitViewController : NSSplitViewController {
12
+ static let minSidebarWidth : CGFloat = 242
13
+ static let maxSnapWidth : CGFloat = snapWidth + 10
22
14
static let snapWidth : CGFloat = 272
23
-
24
15
static let minSnapWidth : CGFloat = snapWidth - 10
25
- static let maxSnapWidth : CGFloat = snapWidth + 10
26
- }
27
16
28
- final class CodeEditSplitViewController : NSSplitViewController {
29
17
private var workspace : WorkspaceDocument
30
- private var setWidthFromState = false
31
- private var viewIsReady = false
32
-
33
- // Properties
34
- private( set) var isSnapped : Bool = false {
35
- willSet {
36
- if newValue, newValue != isSnapped && viewIsReady {
37
- feedbackPerformer. perform ( . alignment, performanceTime: . now)
38
- }
39
- }
40
- }
41
-
42
- // Dependencies
43
- private let feedbackPerformer : NSHapticFeedbackPerformer
18
+ private var navigatorViewModel : NavigatorSidebarViewModel
19
+ private weak var windowRef : NSWindow ?
20
+ private unowned var hapticPerformer : NSHapticFeedbackPerformer
44
21
45
22
// MARK: - Initialization
46
23
47
- init ( workspace: WorkspaceDocument , feedbackPerformer: NSHapticFeedbackPerformer ) {
24
+ init (
25
+ workspace: WorkspaceDocument ,
26
+ navigatorViewModel: NavigatorSidebarViewModel ,
27
+ windowRef: NSWindow ,
28
+ hapticPerformer: NSHapticFeedbackPerformer = NSHapticFeedbackManager . defaultPerformer
29
+ ) {
48
30
self . workspace = workspace
49
- self . feedbackPerformer = feedbackPerformer
31
+ self . navigatorViewModel = navigatorViewModel
32
+ self . windowRef = windowRef
33
+ self . hapticPerformer = hapticPerformer
50
34
super. init ( nibName: nil , bundle: nil )
51
35
}
52
36
@@ -55,13 +39,67 @@ final class CodeEditSplitViewController: NSSplitViewController {
55
39
fatalError ( " init(coder:) has not been implemented " )
56
40
}
57
41
42
+ override func viewDidLoad( ) {
43
+ super. viewDidLoad ( )
44
+ guard let windowRef else {
45
+ // swiftlint:disable:next line_length
46
+ assertionFailure ( " No WindowRef found, not initialized properly or the window was dereferenced and the controller was not. " )
47
+ return
48
+ }
49
+
50
+ splitView. translatesAutoresizingMaskIntoConstraints = false
51
+
52
+ let settingsView = SettingsInjector {
53
+ NavigatorAreaView ( workspace: workspace, viewModel: navigatorViewModel)
54
+ . environmentObject ( workspace)
55
+ . environmentObject ( workspace. editorManager)
56
+ }
57
+
58
+ let navigator = NSSplitViewItem ( sidebarWithViewController: NSHostingController ( rootView: settingsView) )
59
+ navigator. titlebarSeparatorStyle = . none
60
+ navigator. isSpringLoaded = true
61
+ navigator. minimumThickness = Self . minSidebarWidth
62
+ navigator. collapseBehavior = . useConstraints
63
+
64
+ addSplitViewItem ( navigator)
65
+
66
+ let workspaceView = SettingsInjector {
67
+ WindowObserver ( window: windowRef) {
68
+ WorkspaceView ( )
69
+ . environmentObject ( workspace)
70
+ . environmentObject ( workspace. editorManager)
71
+ . environmentObject ( workspace. statusBarViewModel)
72
+ . environmentObject ( workspace. utilityAreaModel)
73
+ }
74
+ }
75
+
76
+ let mainContent = NSSplitViewItem ( viewController: NSHostingController ( rootView: workspaceView) )
77
+ mainContent. titlebarSeparatorStyle = . line
78
+ mainContent. minimumThickness = 200
79
+
80
+ addSplitViewItem ( mainContent)
81
+
82
+ let inspectorView = SettingsInjector {
83
+ InspectorAreaView ( viewModel: InspectorAreaViewModel ( ) )
84
+ . environmentObject ( workspace)
85
+ . environmentObject ( workspace. editorManager)
86
+ }
87
+
88
+ let inspector = NSSplitViewItem ( inspectorWithViewController: NSHostingController ( rootView: inspectorView) )
89
+ inspector. titlebarSeparatorStyle = . none
90
+ inspector. minimumThickness = Self . minSidebarWidth
91
+ inspector. maximumThickness = . greatestFiniteMagnitude
92
+ inspector. collapseBehavior = . useConstraints
93
+ inspector. isSpringLoaded = true
94
+
95
+ addSplitViewItem ( inspector)
96
+ }
97
+
58
98
override func viewWillAppear( ) {
59
99
super. viewWillAppear ( )
60
100
61
- viewIsReady = false
62
- let width = workspace. getFromWorkspaceState ( . splitViewWidth) as? CGFloat
63
- splitView. setPosition ( width ?? . snapWidth, ofDividerAt: . zero)
64
- setWidthFromState = true
101
+ let navigatorWidth = workspace. getFromWorkspaceState ( . splitViewWidth) as? CGFloat
102
+ splitView. setPosition ( navigatorWidth ?? Self . minSidebarWidth, ofDividerAt: 0 )
65
103
66
104
if let firstSplitView = splitViewItems. first {
67
105
firstSplitView. isCollapsed = workspace. getFromWorkspaceState (
@@ -74,58 +112,72 @@ final class CodeEditSplitViewController: NSSplitViewController {
74
112
. inspectorCollapsed
75
113
) as? Bool ?? true
76
114
}
77
-
78
- self . insertToolbarItemIfNeeded ( )
79
- }
80
-
81
- override func viewDidAppear( ) {
82
- viewIsReady = true
83
- hideInspectorToolbarBackground ( )
84
115
}
85
116
86
117
// MARK: - NSSplitViewDelegate
87
118
119
+ /// Perform the spring loaded navigator splits.
120
+ /// - Note: This could be removed. The only additional functionality this provides over using just the
121
+ /// `NSSplitViewItem.isSpringLoaded` & `NSSplitViewItem.minimumThickness` is the haptic feedback we add.
122
+ /// - Parameters:
123
+ /// - splitView: The split view to use.
124
+ /// - proposedPosition: The proposed drag position.
125
+ /// - dividerIndex: The index of the divider being dragged.
126
+ /// - Returns: The position to move the divider to.
88
127
override func splitView(
89
128
_ splitView: NSSplitView ,
90
129
constrainSplitPosition proposedPosition: CGFloat ,
91
130
ofSubviewAt dividerIndex: Int
92
131
) -> CGFloat {
93
- if dividerIndex == 0 {
132
+ switch dividerIndex {
133
+ case 0 :
94
134
// Navigator
95
- if ( CGFloat . minSnapWidth... CGFloat . maxSnapWidth) . contains ( proposedPosition) {
96
- isSnapped = true
97
- return . snapWidth
135
+ if ( Self . minSnapWidth... Self . maxSnapWidth) . contains ( proposedPosition) {
136
+ return Self . snapWidth
137
+ } else if proposedPosition <= Self . minSidebarWidth / 2 {
138
+ hapticCollapse ( splitViewItems. first, collapseAction: true )
139
+ return 0
98
140
} else {
99
- isSnapped = false
100
- if proposedPosition <= CodeEditWindowController . minSidebarWidth / 2 {
101
- splitViewItems. first? . isCollapsed = true
102
- return 0
103
- }
104
- return max ( CodeEditWindowController . minSidebarWidth, proposedPosition)
141
+ hapticCollapse ( splitViewItems. first, collapseAction: false )
142
+ return max ( Self . minSidebarWidth, proposedPosition)
105
143
}
106
- } else if dividerIndex == 1 {
144
+ case 1 :
107
145
let proposedWidth = view. frame. width - proposedPosition
108
- if proposedWidth <= CodeEditWindowController . minSidebarWidth / 2 {
109
- splitViewItems. last? . isCollapsed = true
110
- removeToolbarItemIfNeeded ( )
146
+ if proposedWidth <= Self . minSidebarWidth / 2 {
147
+ hapticCollapse ( splitViewItems. last, collapseAction: true )
111
148
return proposedPosition
149
+ } else {
150
+ hapticCollapse ( splitViewItems. last, collapseAction: false )
151
+ return min ( view. frame. width - Self. minSidebarWidth, proposedPosition)
112
152
}
113
- splitViewItems. last? . isCollapsed = false
114
- insertToolbarItemIfNeeded ( )
115
- return min ( view. frame. width - CodeEditWindowController. minSidebarWidth, proposedPosition)
153
+ default :
154
+ return proposedPosition
155
+ }
156
+ }
157
+
158
+ /// Performs a haptic feedback while collapsing or revealing a split item.
159
+ /// If the item was not previously in the new intended state, a haptic `.alignment` feedback is sent.
160
+ /// - Parameters:
161
+ /// - item: The item to collapse or reveal
162
+ /// - collapseAction: Whether or not to collapse the item. Set to true to collapse it.
163
+ private func hapticCollapse( _ item: NSSplitViewItem ? , collapseAction: Bool ) {
164
+ if item? . isCollapsed == !collapseAction {
165
+ hapticPerformer. perform ( . alignment, performanceTime: . now)
116
166
}
117
- return proposedPosition
167
+ item ? . isCollapsed = collapseAction
118
168
}
119
169
170
+ /// Save the width of the inspector and navigator between sessions.
120
171
override func splitViewDidResizeSubviews( _ notification: Notification ) {
172
+ super. splitViewDidResizeSubviews ( notification)
121
173
guard let resizedDivider = notification. userInfo ? [ " NSSplitViewDividerIndex " ] as? Int else {
122
174
return
123
175
}
124
176
125
177
if resizedDivider == 0 {
126
178
let panel = splitView. subviews [ 0 ]
127
179
let width = panel. frame. size. width
128
- if width > 0 && setWidthFromState {
180
+ if width > 0 {
129
181
workspace. addToWorkspaceState ( key: . splitViewWidth, value: width)
130
182
}
131
183
}
@@ -138,36 +190,4 @@ final class CodeEditSplitViewController: NSSplitViewController {
138
190
func saveInspectorCollapsedState( isCollapsed: Bool ) {
139
191
workspace. addToWorkspaceState ( key: . inspectorCollapsed, value: isCollapsed)
140
192
}
141
-
142
- /// Quick fix for list tracking separator needing to be added again after closing,
143
- /// then opening the inspector with a drag.
144
- private func insertToolbarItemIfNeeded( ) {
145
- guard !(
146
- view. window? . toolbar? . items. contains ( where: { $0. itemIdentifier == . itemListTrackingSeparator } ) ?? true
147
- ) else {
148
- return
149
- }
150
- view. window? . toolbar? . insertItem ( withItemIdentifier: . itemListTrackingSeparator, at: 4 )
151
- }
152
-
153
- /// Quick fix for list tracking separator needing to be removed after closing the inspector with a drag
154
- private func removeToolbarItemIfNeeded( ) {
155
- guard let index = view. window? . toolbar? . items. firstIndex (
156
- where: { $0. itemIdentifier == . itemListTrackingSeparator }
157
- ) else {
158
- return
159
- }
160
- view. window? . toolbar? . removeItem ( at: index)
161
- }
162
-
163
- func hideInspectorToolbarBackground( ) {
164
- let controller = self . view. window? . perform ( Selector ( ( " titlebarViewController " ) ) ) . takeUnretainedValue ( )
165
- if let controller = controller as? NSViewController {
166
- let effectViewCount = controller. view. subviews. filter { $0 is NSVisualEffectView } . count
167
- guard effectViewCount > 2 else { return }
168
- if let view = controller. view. subviews [ 0 ] as? NSVisualEffectView {
169
- view. isHidden = true
170
- }
171
- }
172
- }
173
193
}
0 commit comments