From b85892fd88ab92df5e0b80cebee5028931c23533 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Sun, 23 Jun 2024 09:49:11 -0500 Subject: [PATCH 1/7] Fix ActivityView Resize, Inspector Bar --- .../ActivityViewer/ActivityViewer.swift | 40 +++++++++---------- .../CodeEditSplitViewController.swift | 25 ------------ .../CodeEditWindowController+Toolbar.swift | 21 +++++++++- .../CodeEditWindowController.swift | 2 +- .../CodeEditWindowControllerExtensions.swift | 20 +++------- 5 files changed, 47 insertions(+), 61 deletions(-) diff --git a/CodeEdit/Features/ActivityViewer/ActivityViewer.swift b/CodeEdit/Features/ActivityViewer/ActivityViewer.swift index 51cd6650b..4b7c6987f 100644 --- a/CodeEdit/Features/ActivityViewer/ActivityViewer.swift +++ b/CodeEdit/Features/ActivityViewer/ActivityViewer.swift @@ -13,30 +13,30 @@ struct ActivityViewer: View { var colorScheme @ObservedObject var taskNotificationHandler: TaskNotificationHandler + var body: some View { - HStack { - HStack(spacing: 0) { - // This is only a placeholder for the task popover(coming in the next pr) - Rectangle() - .frame(height: 22) - .hidden() + HStack(spacing: 0) { + // This is only a placeholder for the task popover(coming in the next pr) + Rectangle() + .frame(height: 22) + .hidden() + .fixedSize() - Spacer() + Spacer(minLength: 0) - TaskNotificationView(taskNotificationHandler: taskNotificationHandler) - } - .padding(.horizontal, 10) - .background { - if colorScheme == .dark { - RoundedRectangle(cornerRadius: 5) - .opacity(0.10) - } else { - RoundedRectangle(cornerRadius: 5) - .opacity(0.1) - } + TaskNotificationView(taskNotificationHandler: taskNotificationHandler) + .fixedSize() + } + .fixedSize(horizontal: false, vertical: false) + .padding(.horizontal, 10) + .background { + if colorScheme == .dark { + RoundedRectangle(cornerRadius: 5) + .opacity(0.10) + } else { + RoundedRectangle(cornerRadius: 5) + .opacity(0.1) } - .frame(minWidth: 200, idealWidth: 680) } - .frame(height: 22) } } diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift index 9f270f061..b0c71d191 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift @@ -74,8 +74,6 @@ final class CodeEditSplitViewController: NSSplitViewController { .inspectorCollapsed ) as? Bool ?? true } - - self.insertToolbarItemIfNeeded() } override func viewDidAppear() { @@ -107,11 +105,9 @@ final class CodeEditSplitViewController: NSSplitViewController { let proposedWidth = view.frame.width - proposedPosition if proposedWidth <= CodeEditWindowController.minSidebarWidth / 2 { splitViewItems.last?.isCollapsed = true - removeToolbarItemIfNeeded() return proposedPosition } splitViewItems.last?.isCollapsed = false - insertToolbarItemIfNeeded() return min(view.frame.width - CodeEditWindowController.minSidebarWidth, proposedPosition) } return proposedPosition @@ -139,27 +135,6 @@ final class CodeEditSplitViewController: NSSplitViewController { workspace.addToWorkspaceState(key: .inspectorCollapsed, value: isCollapsed) } - /// Quick fix for list tracking separator needing to be added again after closing, - /// then opening the inspector with a drag. - private func insertToolbarItemIfNeeded() { - guard !( - view.window?.toolbar?.items.contains(where: { $0.itemIdentifier == .itemListTrackingSeparator }) ?? true - ) else { - return - } - view.window?.toolbar?.insertItem(withItemIdentifier: .itemListTrackingSeparator, at: 4) - } - - /// Quick fix for list tracking separator needing to be removed after closing the inspector with a drag - private func removeToolbarItemIfNeeded() { - guard let index = view.window?.toolbar?.items.firstIndex( - where: { $0.itemIdentifier == .itemListTrackingSeparator } - ) else { - return - } - view.window?.toolbar?.removeItem(at: index) - } - func hideInspectorToolbarBackground() { let controller = self.view.window?.perform(Selector(("titlebarViewController"))).takeUnretainedValue() if let controller = controller as? NSViewController { diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Toolbar.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Toolbar.swift index ba7b06b3e..4512ef589 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Toolbar.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Toolbar.swift @@ -118,15 +118,34 @@ extension CodeEditWindowController { case .activityViewer: let toolbarItem = NSToolbarItem(itemIdentifier: NSToolbarItem.Identifier.activityViewer) toolbarItem.visibilityPriority = .user - toolbarItem.view = NSHostingView( + let view = NSHostingView( rootView: ActivityViewer( taskNotificationHandler: taskNotificationHandler ) ) + let weakWidth = view.widthAnchor.constraint(equalToConstant: 650) + weakWidth.priority = .defaultLow + let strongWidth = view.widthAnchor.constraint(greaterThanOrEqualToConstant: 200) + strongWidth.priority = .defaultHigh + + NSLayoutConstraint.activate([ + weakWidth, + strongWidth + ]) + + toolbarItem.view = view return toolbarItem default: return NSToolbarItem(itemIdentifier: itemIdentifier) } } } + +class CETrackingSeparatorToolbarItem: NSTrackingSeparatorToolbarItem { + init(_ splitView: NSSplitView, dividerIndex: Int) { + super.init(itemIdentifier: .itemListTrackingSeparator) + self.splitView = splitView + self.dividerIndex = dividerIndex + } +} diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift index 7da694c2e..94d1fde8c 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift @@ -113,7 +113,7 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs .environmentObject(workspace.editorManager) } - let inspector = NSSplitViewItem(viewController: NSHostingController(rootView: inspectorView)) + let inspector = NSSplitViewItem(inspectorWithViewController: NSHostingController(rootView: inspectorView)) inspector.titlebarSeparatorStyle = .none inspector.minimumThickness = Self.minSidebarWidth inspector.isCollapsed = true diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift index f3bea3c5f..d49f492ee 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift @@ -20,25 +20,17 @@ extension CodeEditWindowController { @objc func toggleLastPanel() { - guard let lastSplitView = splitViewController.splitViewItems.last else { return } - - if let toolbar = window?.toolbar, - lastSplitView.isCollapsed, - !toolbar.items.map(\.itemIdentifier).contains(.itemListTrackingSeparator) { - window?.toolbar?.insertItem(withItemIdentifier: .itemListTrackingSeparator, at: 4) + guard let lastSplitView = splitViewController.splitViewItems.last, + let codeEditSplitVC = splitViewController as? CodeEditSplitViewController else { + return } + NSAnimationContext.runAnimationGroup { _ in lastSplitView.animator().isCollapsed.toggle() - } completionHandler: { [weak self] in - if lastSplitView.isCollapsed { - self?.window?.animator().toolbar?.removeItem(at: 4) - } } - if let codeEditSplitVC = splitViewController as? CodeEditSplitViewController { - codeEditSplitVC.saveInspectorCollapsedState(isCollapsed: lastSplitView.isCollapsed) - codeEditSplitVC.hideInspectorToolbarBackground() - } + codeEditSplitVC.saveInspectorCollapsedState(isCollapsed: lastSplitView.isCollapsed) + codeEditSplitVC.hideInspectorToolbarBackground() } /// These are example items that added as commands to command palette From 314f986186e4b5cd1fdaa4fa5f3bcb0790bb039b Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Sun, 23 Jun 2024 17:34:02 -0500 Subject: [PATCH 2/7] Fix The Toolbar, Window Resizing --- .../CodeEditSplitViewController.swift | 173 +++++++++++------- .../CodeEditWindowController.swift | 77 +++----- .../CodeEditWindowControllerExtensions.swift | 10 +- .../Documents/WorkspaceDocument.swift | 8 +- .../Views/InspectorAreaView.swift | 8 +- 5 files changed, 137 insertions(+), 139 deletions(-) diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift index b0c71d191..e87c8095c 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift @@ -8,45 +8,21 @@ import Cocoa import SwiftUI -struct CodeEditSplitView: NSViewControllerRepresentable { - let controller: NSSplitViewController - - func makeNSViewController(context: Context) -> NSSplitViewController { - controller - } - - func updateNSViewController(_ nsViewController: NSSplitViewController, context: Context) {} -} - -private extension CGFloat { - static let snapWidth: CGFloat = 272 - - static let minSnapWidth: CGFloat = snapWidth - 10 - static let maxSnapWidth: CGFloat = snapWidth + 10 -} - final class CodeEditSplitViewController: NSSplitViewController { - private var workspace: WorkspaceDocument - private var setWidthFromState = false - private var viewIsReady = false - - // Properties - private(set) var isSnapped: Bool = false { - willSet { - if newValue, newValue != isSnapped && viewIsReady { - feedbackPerformer.perform(.alignment, performanceTime: .now) - } - } - } + static let minSidebarWidth: CGFloat = 242 + static let maxSnapWidth: CGFloat = minSidebarWidth + 10 + static let minSnapWidth: CGFloat = minSidebarWidth + 10 - // Dependencies - private let feedbackPerformer: NSHapticFeedbackPerformer + private var workspace: WorkspaceDocument + private var navigatorViewModel: NavigatorSidebarViewModel + private weak var windowRef: NSWindow? // MARK: - Initialization - init(workspace: WorkspaceDocument, feedbackPerformer: NSHapticFeedbackPerformer) { + init(workspace: WorkspaceDocument, navigatorViewModel: NavigatorSidebarViewModel, windowRef: NSWindow) { self.workspace = workspace - self.feedbackPerformer = feedbackPerformer + self.navigatorViewModel = navigatorViewModel + self.windowRef = windowRef super.init(nibName: nil, bundle: nil) } @@ -55,13 +31,67 @@ final class CodeEditSplitViewController: NSSplitViewController { fatalError("init(coder:) has not been implemented") } + override func viewDidLoad() { + super.viewDidLoad() + guard let windowRef else { + // swiftlint:disable:next line_length + assertionFailure("No WindowRef found, not initialized properly or the window was dereferenced and the controller was not.") + return + } + + splitView.translatesAutoresizingMaskIntoConstraints = false + + let settingsView = SettingsInjector { + NavigatorAreaView(workspace: workspace, viewModel: navigatorViewModel) + .environmentObject(workspace) + .environmentObject(workspace.editorManager) + } + + let navigator = NSSplitViewItem(sidebarWithViewController: NSHostingController(rootView: settingsView)) + navigator.titlebarSeparatorStyle = .none + navigator.isSpringLoaded = true + navigator.minimumThickness = Self.minSidebarWidth + navigator.collapseBehavior = .useConstraints + + addSplitViewItem(navigator) + + let workspaceView = SettingsInjector { + WindowObserver(window: windowRef) { + WorkspaceView() + .environmentObject(workspace) + .environmentObject(workspace.editorManager) + .environmentObject(workspace.statusBarViewModel) + .environmentObject(workspace.utilityAreaModel) + } + } + + let mainContent = NSSplitViewItem(viewController: NSHostingController(rootView: workspaceView)) + mainContent.titlebarSeparatorStyle = .line + mainContent.minimumThickness = 200 + + addSplitViewItem(mainContent) + + let inspectorView = SettingsInjector { + InspectorAreaView(viewModel: InspectorAreaViewModel()) + .environmentObject(workspace) + .environmentObject(workspace.editorManager) + } + + let inspector = NSSplitViewItem(inspectorWithViewController: NSHostingController(rootView: inspectorView)) + inspector.titlebarSeparatorStyle = .none + inspector.minimumThickness = Self.minSidebarWidth + inspector.maximumThickness = .greatestFiniteMagnitude + inspector.collapseBehavior = .useConstraints + inspector.isSpringLoaded = true + + addSplitViewItem(inspector) + } + override func viewWillAppear() { super.viewWillAppear() - viewIsReady = false - let width = workspace.getFromWorkspaceState(.splitViewWidth) as? CGFloat - splitView.setPosition(width ?? .snapWidth, ofDividerAt: .zero) - setWidthFromState = true + let navigatorWidth = workspace.getFromWorkspaceState(.splitViewWidth) as? CGFloat + splitView.setPosition(navigatorWidth ?? Self.minSidebarWidth, ofDividerAt: 0) if let firstSplitView = splitViewItems.first { firstSplitView.isCollapsed = workspace.getFromWorkspaceState( @@ -76,44 +106,62 @@ final class CodeEditSplitViewController: NSSplitViewController { } } - override func viewDidAppear() { - viewIsReady = true - hideInspectorToolbarBackground() - } - // MARK: - NSSplitViewDelegate + /// Perform the spring loaded navigator splits. + /// - Note: This could be removed. The only additional functionality this provides over using just the + /// `NSSplitViewItem.isSpringLoaded` & `NSSplitViewItem.minimumThickness` is the haptic feedback we add. + /// - Parameters: + /// - splitView: The split view to use. + /// - proposedPosition: The proposed drag position. + /// - dividerIndex: The index of the divider being dragged. + /// - Returns: The position to move the divider to. override func splitView( _ splitView: NSSplitView, constrainSplitPosition proposedPosition: CGFloat, ofSubviewAt dividerIndex: Int ) -> CGFloat { - if dividerIndex == 0 { + switch dividerIndex { + case 0: // Navigator - if (CGFloat.minSnapWidth...CGFloat.maxSnapWidth).contains(proposedPosition) { - isSnapped = true - return .snapWidth + if (Self.minSnapWidth...Self.maxSnapWidth).contains(proposedPosition) { + return Self.minSidebarWidth } else { - isSnapped = false - if proposedPosition <= CodeEditWindowController.minSidebarWidth / 2 { - splitViewItems.first?.isCollapsed = true + if proposedPosition <= Self.minSidebarWidth / 2 { + hapticCollapse(splitViewItems.first, collapseAction: true) return 0 } - return max(CodeEditWindowController.minSidebarWidth, proposedPosition) + hapticCollapse(splitViewItems.first, collapseAction: false) + return max(Self.minSidebarWidth, proposedPosition) } - } else if dividerIndex == 1 { + case 1: let proposedWidth = view.frame.width - proposedPosition - if proposedWidth <= CodeEditWindowController.minSidebarWidth / 2 { - splitViewItems.last?.isCollapsed = true + if proposedWidth <= Self.minSidebarWidth / 2 { + hapticCollapse(splitViewItems.last, collapseAction: true) return proposedPosition } - splitViewItems.last?.isCollapsed = false - return min(view.frame.width - CodeEditWindowController.minSidebarWidth, proposedPosition) + hapticCollapse(splitViewItems.last, collapseAction: false) + return min(view.frame.width - Self.minSidebarWidth, proposedPosition) + default: + return proposedPosition + } + } + + /// Performs a haptic feedback while collapsing or revealing a split item. + /// If the item was not previously in the new intended state, a haptic `.alignment` feedback is sent. + /// - Parameters: + /// - item: The item to collapse or reveal + /// - collapseAction: Whether or not to collapse the item. Set to true to collapse it. + private func hapticCollapse(_ item: NSSplitViewItem?, collapseAction: Bool) { + if item?.isCollapsed == !collapseAction { + NSHapticFeedbackManager.defaultPerformer.perform(.alignment, performanceTime: .now) } - return proposedPosition + item?.isCollapsed = collapseAction } + /// Save the width of the inspector and navigator between sessions. override func splitViewDidResizeSubviews(_ notification: Notification) { + super.splitViewDidResizeSubviews(notification) guard let resizedDivider = notification.userInfo?["NSSplitViewDividerIndex"] as? Int else { return } @@ -121,7 +169,7 @@ final class CodeEditSplitViewController: NSSplitViewController { if resizedDivider == 0 { let panel = splitView.subviews[0] let width = panel.frame.size.width - if width > 0 && setWidthFromState { + if width > 0 { workspace.addToWorkspaceState(key: .splitViewWidth, value: width) } } @@ -134,15 +182,4 @@ final class CodeEditSplitViewController: NSSplitViewController { func saveInspectorCollapsedState(isCollapsed: Bool) { workspace.addToWorkspaceState(key: .inspectorCollapsed, value: isCollapsed) } - - func hideInspectorToolbarBackground() { - let controller = self.view.window?.perform(Selector(("titlebarViewController"))).takeUnretainedValue() - if let controller = controller as? NSViewController { - let effectViewCount = controller.view.subviews.filter { $0 is NSVisualEffectView }.count - guard effectViewCount > 2 else { return } - if let view = controller.view.subviews[0] as? NSVisualEffectView { - view.isHidden = true - } - } - } } diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift index 94d1fde8c..0b4066585 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift @@ -10,8 +10,6 @@ import SwiftUI import Combine final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, ObservableObject { - static let minSidebarWidth: CGFloat = 242 - @Published var navigatorCollapsed = false @Published var inspectorCollapsed = false @Published var toolbarCollapsed = false @@ -43,11 +41,20 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs self.workspaceSettings = CEWorkspaceSettings(workspaceDocument: workspace) setupSplitView(with: workspace) - let view = CodeEditSplitView(controller: splitViewController).ignoresSafeArea() - + // Previous: // An NSHostingController is used, so the root viewController of the window is a SwiftUI-managed one. // This allows us to use some SwiftUI features, like focusedSceneObject. - contentViewController = NSHostingController(rootView: view) + // ----- + // let view = CodeEditSplitView(controller: splitViewController).ignoresSafeArea() + // contentViewController = NSHostingController(rootView: view) + // ----- + // + // New: + // The previous decision led to a very jank split controller mechanism because SwiftUI's layout system is not + // very compatible with AppKit's when it comes to the inspector/navigator toolbar & split view system. + // ----- + contentViewController = splitViewController + // ----- observers = [ splitViewController.splitViewItems.first!.observe(\.isCollapsed, changeHandler: { [weak self] item, _ in @@ -70,60 +77,18 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs } private func setupSplitView(with workspace: WorkspaceDocument) { - let feedbackPerformer = NSHapticFeedbackManager.defaultPerformer - let splitVC = CodeEditSplitViewController(workspace: workspace, feedbackPerformer: feedbackPerformer) - - let navigatorViewModel = NavigatorSidebarViewModel() - navigatorSidebarViewModel = navigatorViewModel - - let settingsView = SettingsInjector { - NavigatorAreaView(workspace: workspace, viewModel: navigatorViewModel) - .environmentObject(workspace) - .environmentObject(workspace.editorManager) + guard let window else { + assertionFailure("No window found for this controller. Cannot set up content.") + return } - let navigator = NSSplitViewItem( - sidebarWithViewController: NSHostingController(rootView: settingsView) + let navigatorModel = NavigatorSidebarViewModel() + navigatorSidebarViewModel = navigatorModel + self.splitViewController = CodeEditSplitViewController( + workspace: workspace, + navigatorViewModel: navigatorModel, + windowRef: window ) - navigator.titlebarSeparatorStyle = .none - navigator.minimumThickness = Self.minSidebarWidth - navigator.collapseBehavior = .useConstraints - - splitVC.addSplitViewItem(navigator) - - let workspaceView = SettingsInjector { - WindowObserver(window: window!) { - WorkspaceView() - .environmentObject(workspace) - .environmentObject(workspace.editorManager) - .environmentObject(workspace.statusBarViewModel) - .environmentObject(workspace.utilityAreaModel) - } - } - - let mainContent = NSSplitViewItem(viewController: NSHostingController(rootView: workspaceView)) - mainContent.titlebarSeparatorStyle = .line - mainContent.holdingPriority = .init(50) - - splitVC.addSplitViewItem(mainContent) - - let inspectorView = SettingsInjector { - InspectorAreaView(viewModel: InspectorAreaViewModel()) - .environmentObject(workspace) - .environmentObject(workspace.editorManager) - } - - let inspector = NSSplitViewItem(inspectorWithViewController: NSHostingController(rootView: inspectorView)) - inspector.titlebarSeparatorStyle = .none - inspector.minimumThickness = Self.minSidebarWidth - inspector.isCollapsed = true - inspector.canCollapse = true - inspector.collapseBehavior = .useConstraints - inspector.isSpringLoaded = true - - splitVC.addSplitViewItem(inspector) - - self.splitViewController = splitVC self.listenToDocumentEdited(workspace: workspace) } diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift index d49f492ee..96a3be550 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift @@ -13,9 +13,9 @@ extension CodeEditWindowController { func toggleFirstPanel() { guard let firstSplitView = splitViewController.splitViewItems.first else { return } firstSplitView.animator().isCollapsed.toggle() - if let codeEditSplitVC = splitViewController as? CodeEditSplitViewController { - codeEditSplitVC.saveNavigatorCollapsedState(isCollapsed: firstSplitView.isCollapsed) - } +// if let codeEditSplitVC = splitViewController as? CodeEditSplitViewController { +// codeEditSplitVC.saveNavigatorCollapsedState(isCollapsed: firstSplitView.isCollapsed) +// } } @objc @@ -29,8 +29,8 @@ extension CodeEditWindowController { lastSplitView.animator().isCollapsed.toggle() } - codeEditSplitVC.saveInspectorCollapsedState(isCollapsed: lastSplitView.isCollapsed) - codeEditSplitVC.hideInspectorToolbarBackground() +// codeEditSplitVC.saveInspectorCollapsedState(isCollapsed: lastSplitView.isCollapsed) +// codeEditSplitVC.hideInspectorToolbarBackground() } /// These are example items that added as commands to command palette diff --git a/CodeEdit/Features/Documents/WorkspaceDocument.swift b/CodeEdit/Features/Documents/WorkspaceDocument.swift index c9fccc70d..71a187b39 100644 --- a/CodeEdit/Features/Documents/WorkspaceDocument.swift +++ b/CodeEdit/Features/Documents/WorkspaceDocument.swift @@ -80,14 +80,16 @@ final class WorkspaceDocument: NSDocument, ObservableObject, NSToolbarDelegate { backing: .buffered, defer: false ) + // Note For anyone hoping to switch back to a Root-SwiftUI window: + // See Commit 0200c87 for more details and to see what was previously here. + // ----- // Setting the "min size" like this is hacky, but SwiftUI overrides the contentRect and // any of the built-in window size functions & autosave stuff. So we have to set it like this. // SwiftUI also ignores this value, so it just manages to set the initial window size. *Hopefully* this // is fixed in the future. + // ---- if let rectString = getFromWorkspaceState(.workspaceWindowSize) as? String { - window.minSize = NSRectFromString(rectString).size - } else { - window.minSize = .init(width: 1400, height: 900) + window.setContentSize(NSRectFromString(rectString).size) } let windowController = CodeEditWindowController( window: window, diff --git a/CodeEdit/Features/InspectorArea/Views/InspectorAreaView.swift b/CodeEdit/Features/InspectorArea/Views/InspectorAreaView.swift index 2708e784e..f8c4a5757 100644 --- a/CodeEdit/Features/InspectorArea/Views/InspectorAreaView.swift +++ b/CodeEdit/Features/InspectorArea/Views/InspectorAreaView.swift @@ -5,6 +5,7 @@ // Created by Austin Condiff on 3/21/22. // +import AppKit import SwiftUI struct InspectorAreaView: View { @@ -52,13 +53,6 @@ struct InspectorAreaView: View { } } .clipShape(Rectangle()) - .frame( - minWidth: CodeEditWindowController.minSidebarWidth, - idealWidth: 300, - minHeight: 0, - maxHeight: .infinity, - alignment: .top - ) .safeAreaInset(edge: .trailing, spacing: 0) { if sidebarPosition == .side { HStack(spacing: 0) { From 813219720a3563d96a546ad3e165f76257023000 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Sun, 23 Jun 2024 17:43:28 -0500 Subject: [PATCH 3/7] Remove Testing Class --- .../Controllers/CodeEditWindowController+Toolbar.swift | 8 -------- 1 file changed, 8 deletions(-) diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Toolbar.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Toolbar.swift index 4512ef589..f9acda1f1 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Toolbar.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Toolbar.swift @@ -141,11 +141,3 @@ extension CodeEditWindowController { } } } - -class CETrackingSeparatorToolbarItem: NSTrackingSeparatorToolbarItem { - init(_ splitView: NSSplitView, dividerIndex: Int) { - super.init(itemIdentifier: .itemListTrackingSeparator) - self.splitView = splitView - self.dividerIndex = dividerIndex - } -} From 379f5427dd93ede5273d30aac66498427c377608 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Sun, 23 Jun 2024 19:39:17 -0500 Subject: [PATCH 4/7] Remove Unnecessary Import --- CodeEdit/Features/InspectorArea/Views/InspectorAreaView.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/CodeEdit/Features/InspectorArea/Views/InspectorAreaView.swift b/CodeEdit/Features/InspectorArea/Views/InspectorAreaView.swift index f8c4a5757..b46742f64 100644 --- a/CodeEdit/Features/InspectorArea/Views/InspectorAreaView.swift +++ b/CodeEdit/Features/InspectorArea/Views/InspectorAreaView.swift @@ -5,7 +5,6 @@ // Created by Austin Condiff on 3/21/22. // -import AppKit import SwiftUI struct InspectorAreaView: View { @@ -52,7 +51,6 @@ struct InspectorAreaView: View { NoSelectionInspectorView() } } - .clipShape(Rectangle()) .safeAreaInset(edge: .trailing, spacing: 0) { if sidebarPosition == .side { HStack(spacing: 0) { From 2591c8f96849b9f5fd329214e010df2a9d4ebef1 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Sun, 23 Jun 2024 19:56:38 -0500 Subject: [PATCH 5/7] Uncomment Some Stuff --- .../Controllers/CodeEditWindowControllerExtensions.swift | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift index 96a3be550..f1a858615 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift @@ -13,9 +13,9 @@ extension CodeEditWindowController { func toggleFirstPanel() { guard let firstSplitView = splitViewController.splitViewItems.first else { return } firstSplitView.animator().isCollapsed.toggle() -// if let codeEditSplitVC = splitViewController as? CodeEditSplitViewController { -// codeEditSplitVC.saveNavigatorCollapsedState(isCollapsed: firstSplitView.isCollapsed) -// } + if let codeEditSplitVC = splitViewController as? CodeEditSplitViewController { + codeEditSplitVC.saveNavigatorCollapsedState(isCollapsed: firstSplitView.isCollapsed) + } } @objc @@ -29,8 +29,7 @@ extension CodeEditWindowController { lastSplitView.animator().isCollapsed.toggle() } -// codeEditSplitVC.saveInspectorCollapsedState(isCollapsed: lastSplitView.isCollapsed) -// codeEditSplitVC.hideInspectorToolbarBackground() + codeEditSplitVC.saveInspectorCollapsedState(isCollapsed: lastSplitView.isCollapsed) } /// These are example items that added as commands to command palette From 89a76bc7472c698b9ba291b1075a692f3808bd8b Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Sun, 23 Jun 2024 20:17:10 -0500 Subject: [PATCH 6/7] Enable All Tests --- .../xcshareddata/xcschemes/CodeEdit.xcscheme | 12 -- .../CodeEditSplitViewController.swift | 30 ++-- .../Documents/DocumentsUnitTests.swift | 151 +++++++++++------- .../Mocks/NSHapticFeedbackPerformerMock.swift | 9 +- 4 files changed, 116 insertions(+), 86 deletions(-) diff --git a/CodeEdit.xcodeproj/xcshareddata/xcschemes/CodeEdit.xcscheme b/CodeEdit.xcodeproj/xcshareddata/xcschemes/CodeEdit.xcscheme index 8720f4dbb..c6868c5f6 100644 --- a/CodeEdit.xcodeproj/xcshareddata/xcschemes/CodeEdit.xcscheme +++ b/CodeEdit.xcodeproj/xcshareddata/xcschemes/CodeEdit.xcscheme @@ -77,18 +77,6 @@ - - - - - - - - diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift index e87c8095c..af83e56ac 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift @@ -10,19 +10,27 @@ import SwiftUI final class CodeEditSplitViewController: NSSplitViewController { static let minSidebarWidth: CGFloat = 242 - static let maxSnapWidth: CGFloat = minSidebarWidth + 10 - static let minSnapWidth: CGFloat = minSidebarWidth + 10 + static let maxSnapWidth: CGFloat = snapWidth + 10 + static let snapWidth: CGFloat = 272 + static let minSnapWidth: CGFloat = snapWidth - 10 private var workspace: WorkspaceDocument private var navigatorViewModel: NavigatorSidebarViewModel private weak var windowRef: NSWindow? + private unowned var hapticPerformer: NSHapticFeedbackPerformer // MARK: - Initialization - init(workspace: WorkspaceDocument, navigatorViewModel: NavigatorSidebarViewModel, windowRef: NSWindow) { + init( + workspace: WorkspaceDocument, + navigatorViewModel: NavigatorSidebarViewModel, + windowRef: NSWindow, + hapticPerformer: NSHapticFeedbackPerformer = NSHapticFeedbackManager.defaultPerformer + ) { self.workspace = workspace self.navigatorViewModel = navigatorViewModel self.windowRef = windowRef + self.hapticPerformer = hapticPerformer super.init(nibName: nil, bundle: nil) } @@ -125,12 +133,11 @@ final class CodeEditSplitViewController: NSSplitViewController { case 0: // Navigator if (Self.minSnapWidth...Self.maxSnapWidth).contains(proposedPosition) { - return Self.minSidebarWidth + return Self.snapWidth + } else if proposedPosition <= Self.minSidebarWidth / 2 { + hapticCollapse(splitViewItems.first, collapseAction: true) + return 0 } else { - if proposedPosition <= Self.minSidebarWidth / 2 { - hapticCollapse(splitViewItems.first, collapseAction: true) - return 0 - } hapticCollapse(splitViewItems.first, collapseAction: false) return max(Self.minSidebarWidth, proposedPosition) } @@ -139,9 +146,10 @@ final class CodeEditSplitViewController: NSSplitViewController { if proposedWidth <= Self.minSidebarWidth / 2 { hapticCollapse(splitViewItems.last, collapseAction: true) return proposedPosition + } else { + hapticCollapse(splitViewItems.last, collapseAction: false) + return min(view.frame.width - Self.minSidebarWidth, proposedPosition) } - hapticCollapse(splitViewItems.last, collapseAction: false) - return min(view.frame.width - Self.minSidebarWidth, proposedPosition) default: return proposedPosition } @@ -154,7 +162,7 @@ final class CodeEditSplitViewController: NSSplitViewController { /// - collapseAction: Whether or not to collapse the item. Set to true to collapse it. private func hapticCollapse(_ item: NSSplitViewItem?, collapseAction: Bool) { if item?.isCollapsed == !collapseAction { - NSHapticFeedbackManager.defaultPerformer.perform(.alignment, performanceTime: .now) + hapticPerformer.perform(.alignment, performanceTime: .now) } item?.isCollapsed = collapseAction } diff --git a/CodeEditTests/Features/Documents/DocumentsUnitTests.swift b/CodeEditTests/Features/Documents/DocumentsUnitTests.swift index 6cce3e50f..c116d7a19 100644 --- a/CodeEditTests/Features/Documents/DocumentsUnitTests.swift +++ b/CodeEditTests/Features/Documents/DocumentsUnitTests.swift @@ -12,17 +12,26 @@ final class DocumentsUnitTests: XCTestCase { // Properties private var splitViewController: CodeEditSplitViewController! private var hapticFeedbackPerformerMock: NSHapticFeedbackPerformerMock! + private var navigatorViewModel: NavigatorSidebarViewModel! + private var window: NSWindow! // MARK: - Lifecycle override func setUp() { super.setUp() - hapticFeedbackPerformerMock = .init() - splitViewController = .init(workspace: WorkspaceDocument(), feedbackPerformer: hapticFeedbackPerformerMock) + hapticFeedbackPerformerMock = NSHapticFeedbackPerformerMock() + navigatorViewModel = .init() + window = NSWindow() + splitViewController = .init( + workspace: WorkspaceDocument(), + navigatorViewModel: navigatorViewModel, + windowRef: window, + hapticPerformer: hapticFeedbackPerformerMock + ) + splitViewController.viewDidLoad() } override func tearDown() { - hapticFeedbackPerformerMock = nil splitViewController = nil super.tearDown() } @@ -30,83 +39,103 @@ final class DocumentsUnitTests: XCTestCase { // MARK: - Tests func testSplitViewControllerSnappedWhenWidthInAppropriateRange() { - // Given - let position = (260...280).randomElement() ?? .zero - - // When - let result = splitViewController.splitView( - splitViewController.splitView, - constrainSplitPosition: .init(position), - ofSubviewAt: .zero - ) + for _ in 0..<10 { + // Given + let position = CGFloat.random( + in: (CodeEditSplitViewController.minSnapWidth...CodeEditSplitViewController.maxSnapWidth) + ) - // Then - XCTAssertEqual(result, 272) + // When + let result = splitViewController.splitView( + splitViewController.splitView, + constrainSplitPosition: .init(position), + ofSubviewAt: .zero + ) + + // Then + XCTAssertEqual(result, CodeEditSplitViewController.snapWidth) + } } func testSplitViewControllerStopSnappedWhenWidthIsLowerAppropriateRange() { - // Given - // 242 is the minimum width of the sidebar - let position = (242..<260).randomElement() ?? .zero - - // When - let result = splitViewController.splitView( - splitViewController.splitView, - constrainSplitPosition: .init(position), - ofSubviewAt: .zero - ) + for _ in 0..<10 { + // Given + let position = CGFloat.random(in: 0..<(CodeEditSplitViewController.minSidebarWidth / 2)) - // Then - XCTAssertEqual(result, .init(position)) + // When + let result = splitViewController.splitView( + splitViewController.splitView, + constrainSplitPosition: .init(position), + ofSubviewAt: .zero + ) + + // Then + XCTAssertEqual(result, .zero) + } } func testSplitViewControllerStopSnappedWhenWidthIsHigherAppropriateRange() { - // Given - let position = (281...500).randomElement() ?? .zero - - // When - let result = splitViewController.splitView( - splitViewController.splitView, - constrainSplitPosition: .init(position), - ofSubviewAt: .zero - ) + for _ in 0..<10 { + // Given + let position = CGFloat.random(in: (CodeEditSplitViewController.maxSnapWidth...500)) - // Then - XCTAssertEqual(result, .init(position)) - } - - func testSplitViewControllerProducedHapticFeedback() { - // Given - let position = (260...280).randomElement() ?? .zero - - // When - _ = splitViewController.splitView( - splitViewController.splitView, - constrainSplitPosition: .init(position), - ofSubviewAt: .zero - ) + // When + let result = splitViewController.splitView( + splitViewController.splitView, + constrainSplitPosition: .init(position), + ofSubviewAt: .zero + ) - // Then - XCTAssertTrue(hapticFeedbackPerformerMock.invokedPerform) - XCTAssertEqual(hapticFeedbackPerformerMock.invokedPerformCount, 1) + // Then + XCTAssertEqual(result, .init(position)) + } } - func testSplitViewControllerProducedHapticFeedbackOnceWhenPlentyChangesOccur() { - // Given - let firstPosition = (260...280).randomElement() ?? .zero - let secondPosition = 300 + // Test moving from collapsed to uncollapsed makes a haptic. + func testSplitViewControllerProducedHapticFeedback() { + for _ in 0..<10 { + // Given + splitViewController.splitViewItems.first?.isCollapsed = true + let position = CGFloat.random( + in: (CodeEditSplitViewController.minSidebarWidth / 2)...CodeEditSplitViewController.minSidebarWidth + ) - // When - [firstPosition, secondPosition].forEach { position in + // When _ = splitViewController.splitView( splitViewController.splitView, constrainSplitPosition: .init(position), ofSubviewAt: .zero ) + + // Then + XCTAssertTrue(hapticFeedbackPerformerMock.invokedPerform) + XCTAssertEqual(hapticFeedbackPerformerMock.invokedPerformCount, 1) + hapticFeedbackPerformerMock.reset() } + } - // Then - XCTAssertTrue(hapticFeedbackPerformerMock.invokedPerform) - XCTAssertEqual(hapticFeedbackPerformerMock.invokedPerformCount, 1) + func testSplitViewControllerProducedHapticFeedbackOnceWhenPlentyChangesOccur() { + for _ in 0..<10 { + // Given + splitViewController.splitViewItems.first?.isCollapsed = true + let firstPosition = CGFloat.random(in: 0..<(CodeEditSplitViewController.minSidebarWidth / 2)) + let secondPosition = CGFloat.random( + in: (CodeEditSplitViewController.minSidebarWidth / 2)...CodeEditSplitViewController.minSidebarWidth + ) + + // When + [firstPosition, secondPosition].forEach { position in + _ = splitViewController.splitView( + splitViewController.splitView, + constrainSplitPosition: .init(position), + ofSubviewAt: .zero + ) + } + + // Then + XCTAssertTrue(hapticFeedbackPerformerMock.invokedPerform) + XCTAssertEqual(hapticFeedbackPerformerMock.invokedPerformCount, 1) + hapticFeedbackPerformerMock.reset() + } } } diff --git a/CodeEditTests/Features/Documents/Mocks/NSHapticFeedbackPerformerMock.swift b/CodeEditTests/Features/Documents/Mocks/NSHapticFeedbackPerformerMock.swift index 7a8b12e27..5ee85dbf7 100644 --- a/CodeEditTests/Features/Documents/Mocks/NSHapticFeedbackPerformerMock.swift +++ b/CodeEditTests/Features/Documents/Mocks/NSHapticFeedbackPerformerMock.swift @@ -9,14 +9,19 @@ import Cocoa final class NSHapticFeedbackPerformerMock: NSObject, NSHapticFeedbackPerformer { - var invokedPerform = false + var invokedPerform: Bool { + invokedPerformCount > 0 + } var invokedPerformCount = 0 func perform( _ pattern: NSHapticFeedbackManager.FeedbackPattern, performanceTime: NSHapticFeedbackManager.PerformanceTime ) { - invokedPerform = true invokedPerformCount += 1 } + + func reset() { + invokedPerformCount = 0 + } } From 2c892a44bd2e9a848aff0b35562a4806c6ac995f Mon Sep 17 00:00:00 2001 From: Austin Condiff Date: Tue, 25 Jun 2024 21:57:15 -0500 Subject: [PATCH 7/7] Update CodeEdit/Features/ActivityViewer/ActivityViewer.swift --- CodeEdit/Features/ActivityViewer/ActivityViewer.swift | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/CodeEdit/Features/ActivityViewer/ActivityViewer.swift b/CodeEdit/Features/ActivityViewer/ActivityViewer.swift index 4b7c6987f..1cc2cca5f 100644 --- a/CodeEdit/Features/ActivityViewer/ActivityViewer.swift +++ b/CodeEdit/Features/ActivityViewer/ActivityViewer.swift @@ -30,13 +30,8 @@ struct ActivityViewer: View { .fixedSize(horizontal: false, vertical: false) .padding(.horizontal, 10) .background { - if colorScheme == .dark { - RoundedRectangle(cornerRadius: 5) - .opacity(0.10) - } else { - RoundedRectangle(cornerRadius: 5) - .opacity(0.1) - } + RoundedRectangle(cornerRadius: 5) + .opacity(0.1) } } }