Skip to content

Commit 051a837

Browse files
Activity View Resize, Inspector Toggle, Window Consistency (#1776)
* Fix ActivityView Resize, Inspector Bar * Fix The Toolbar, Window Resizing * Remove Testing Class * Remove Unnecessary Import * Uncomment Some Stuff * Enable All Tests * Update CodeEdit/Features/ActivityViewer/ActivityViewer.swift --------- Co-authored-by: Austin Condiff <austin.condiff@gmail.com>
1 parent c78ef96 commit 051a837

File tree

10 files changed

+269
-271
lines changed

10 files changed

+269
-271
lines changed

CodeEdit.xcodeproj/xcshareddata/xcschemes/CodeEdit.xcscheme

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -77,18 +77,6 @@
7777
<Test
7878
Identifier = "CodeEditUIUnitTests/testSegmentedControlProminentLight()">
7979
</Test>
80-
<Test
81-
Identifier = "DocumentsUnitTests/testSplitViewControllerProducedHapticFeedback()">
82-
</Test>
83-
<Test
84-
Identifier = "DocumentsUnitTests/testSplitViewControllerProducedHapticFeedbackOnceWhenPlentyChangesOccur()">
85-
</Test>
86-
<Test
87-
Identifier = "DocumentsUnitTests/testSplitViewControllerSnappedWhenWidthInAppropriateRange()">
88-
</Test>
89-
<Test
90-
Identifier = "DocumentsUnitTests/testSplitViewControllerStopSnappedWhenWidthIsHigherAppropriateRange()">
91-
</Test>
9280
<Test
9381
Identifier = "WelcomeModuleUnitTests">
9482
</Test>

CodeEdit/Features/ActivityViewer/ActivityViewer.swift

Lines changed: 16 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -13,30 +13,25 @@ struct ActivityViewer: View {
1313
var colorScheme
1414

1515
@ObservedObject var taskNotificationHandler: TaskNotificationHandler
16+
1617
var body: some View {
17-
HStack {
18-
HStack(spacing: 0) {
19-
// This is only a placeholder for the task popover(coming in the next pr)
20-
Rectangle()
21-
.frame(height: 22)
22-
.hidden()
18+
HStack(spacing: 0) {
19+
// This is only a placeholder for the task popover(coming in the next pr)
20+
Rectangle()
21+
.frame(height: 22)
22+
.hidden()
23+
.fixedSize()
2324

24-
Spacer()
25+
Spacer(minLength: 0)
2526

26-
TaskNotificationView(taskNotificationHandler: taskNotificationHandler)
27-
}
28-
.padding(.horizontal, 10)
29-
.background {
30-
if colorScheme == .dark {
31-
RoundedRectangle(cornerRadius: 5)
32-
.opacity(0.10)
33-
} else {
34-
RoundedRectangle(cornerRadius: 5)
35-
.opacity(0.1)
36-
}
37-
}
38-
.frame(minWidth: 200, idealWidth: 680)
27+
TaskNotificationView(taskNotificationHandler: taskNotificationHandler)
28+
.fixedSize()
29+
}
30+
.fixedSize(horizontal: false, vertical: false)
31+
.padding(.horizontal, 10)
32+
.background {
33+
RoundedRectangle(cornerRadius: 5)
34+
.opacity(0.1)
3935
}
40-
.frame(height: 22)
4136
}
4237
}

CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift

Lines changed: 113 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -8,45 +8,29 @@
88
import Cocoa
99
import SwiftUI
1010

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
2214
static let snapWidth: CGFloat = 272
23-
2415
static let minSnapWidth: CGFloat = snapWidth - 10
25-
static let maxSnapWidth: CGFloat = snapWidth + 10
26-
}
2716

28-
final class CodeEditSplitViewController: NSSplitViewController {
2917
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
4421

4522
// MARK: - Initialization
4623

47-
init(workspace: WorkspaceDocument, feedbackPerformer: NSHapticFeedbackPerformer) {
24+
init(
25+
workspace: WorkspaceDocument,
26+
navigatorViewModel: NavigatorSidebarViewModel,
27+
windowRef: NSWindow,
28+
hapticPerformer: NSHapticFeedbackPerformer = NSHapticFeedbackManager.defaultPerformer
29+
) {
4830
self.workspace = workspace
49-
self.feedbackPerformer = feedbackPerformer
31+
self.navigatorViewModel = navigatorViewModel
32+
self.windowRef = windowRef
33+
self.hapticPerformer = hapticPerformer
5034
super.init(nibName: nil, bundle: nil)
5135
}
5236

@@ -55,13 +39,67 @@ final class CodeEditSplitViewController: NSSplitViewController {
5539
fatalError("init(coder:) has not been implemented")
5640
}
5741

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+
5898
override func viewWillAppear() {
5999
super.viewWillAppear()
60100

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)
65103

66104
if let firstSplitView = splitViewItems.first {
67105
firstSplitView.isCollapsed = workspace.getFromWorkspaceState(
@@ -74,58 +112,72 @@ final class CodeEditSplitViewController: NSSplitViewController {
74112
.inspectorCollapsed
75113
) as? Bool ?? true
76114
}
77-
78-
self.insertToolbarItemIfNeeded()
79-
}
80-
81-
override func viewDidAppear() {
82-
viewIsReady = true
83-
hideInspectorToolbarBackground()
84115
}
85116

86117
// MARK: - NSSplitViewDelegate
87118

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.
88127
override func splitView(
89128
_ splitView: NSSplitView,
90129
constrainSplitPosition proposedPosition: CGFloat,
91130
ofSubviewAt dividerIndex: Int
92131
) -> CGFloat {
93-
if dividerIndex == 0 {
132+
switch dividerIndex {
133+
case 0:
94134
// 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
98140
} 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)
105143
}
106-
} else if dividerIndex == 1 {
144+
case 1:
107145
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)
111148
return proposedPosition
149+
} else {
150+
hapticCollapse(splitViewItems.last, collapseAction: false)
151+
return min(view.frame.width - Self.minSidebarWidth, proposedPosition)
112152
}
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)
116166
}
117-
return proposedPosition
167+
item?.isCollapsed = collapseAction
118168
}
119169

170+
/// Save the width of the inspector and navigator between sessions.
120171
override func splitViewDidResizeSubviews(_ notification: Notification) {
172+
super.splitViewDidResizeSubviews(notification)
121173
guard let resizedDivider = notification.userInfo?["NSSplitViewDividerIndex"] as? Int else {
122174
return
123175
}
124176

125177
if resizedDivider == 0 {
126178
let panel = splitView.subviews[0]
127179
let width = panel.frame.size.width
128-
if width > 0 && setWidthFromState {
180+
if width > 0 {
129181
workspace.addToWorkspaceState(key: .splitViewWidth, value: width)
130182
}
131183
}
@@ -138,36 +190,4 @@ final class CodeEditSplitViewController: NSSplitViewController {
138190
func saveInspectorCollapsedState(isCollapsed: Bool) {
139191
workspace.addToWorkspaceState(key: .inspectorCollapsed, value: isCollapsed)
140192
}
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-
}
173193
}

CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Toolbar.swift

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,12 +118,23 @@ extension CodeEditWindowController {
118118
case .activityViewer:
119119
let toolbarItem = NSToolbarItem(itemIdentifier: NSToolbarItem.Identifier.activityViewer)
120120
toolbarItem.visibilityPriority = .user
121-
toolbarItem.view = NSHostingView(
121+
let view = NSHostingView(
122122
rootView: ActivityViewer(
123123
taskNotificationHandler: taskNotificationHandler
124124
)
125125
)
126126

127+
let weakWidth = view.widthAnchor.constraint(equalToConstant: 650)
128+
weakWidth.priority = .defaultLow
129+
let strongWidth = view.widthAnchor.constraint(greaterThanOrEqualToConstant: 200)
130+
strongWidth.priority = .defaultHigh
131+
132+
NSLayoutConstraint.activate([
133+
weakWidth,
134+
strongWidth
135+
])
136+
137+
toolbarItem.view = view
127138
return toolbarItem
128139
default:
129140
return NSToolbarItem(itemIdentifier: itemIdentifier)

0 commit comments

Comments
 (0)