Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Activity View Resize, Inspector Toggle, Window Consistency #1776

Merged
merged 7 commits into from
Jun 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 0 additions & 12 deletions CodeEdit.xcodeproj/xcshareddata/xcschemes/CodeEdit.xcscheme
Original file line number Diff line number Diff line change
Expand Up @@ -77,18 +77,6 @@
<Test
Identifier = "CodeEditUIUnitTests/testSegmentedControlProminentLight()">
</Test>
<Test
Identifier = "DocumentsUnitTests/testSplitViewControllerProducedHapticFeedback()">
</Test>
<Test
Identifier = "DocumentsUnitTests/testSplitViewControllerProducedHapticFeedbackOnceWhenPlentyChangesOccur()">
</Test>
<Test
Identifier = "DocumentsUnitTests/testSplitViewControllerSnappedWhenWidthInAppropriateRange()">
</Test>
<Test
Identifier = "DocumentsUnitTests/testSplitViewControllerStopSnappedWhenWidthIsHigherAppropriateRange()">
</Test>
<Test
Identifier = "WelcomeModuleUnitTests">
</Test>
Expand Down
37 changes: 16 additions & 21 deletions CodeEdit/Features/ActivityViewer/ActivityViewer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,30 +13,25 @@ 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)
}
}
.frame(minWidth: 200, idealWidth: 680)
TaskNotificationView(taskNotificationHandler: taskNotificationHandler)
.fixedSize()
}
.fixedSize(horizontal: false, vertical: false)
.padding(.horizontal, 10)
.background {
RoundedRectangle(cornerRadius: 5)
.opacity(0.1)
}
.frame(height: 22)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,45 +8,29 @@
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 {
final class CodeEditSplitViewController: NSSplitViewController {
static let minSidebarWidth: CGFloat = 242
static let maxSnapWidth: CGFloat = snapWidth + 10
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)
}
}
}

// Dependencies
private let feedbackPerformer: NSHapticFeedbackPerformer
private var navigatorViewModel: NavigatorSidebarViewModel
private weak var windowRef: NSWindow?
private unowned var hapticPerformer: NSHapticFeedbackPerformer

// MARK: - Initialization

init(workspace: WorkspaceDocument, feedbackPerformer: NSHapticFeedbackPerformer) {
init(
workspace: WorkspaceDocument,
navigatorViewModel: NavigatorSidebarViewModel,
windowRef: NSWindow,
hapticPerformer: NSHapticFeedbackPerformer = NSHapticFeedbackManager.defaultPerformer
) {
self.workspace = workspace
self.feedbackPerformer = feedbackPerformer
self.navigatorViewModel = navigatorViewModel
self.windowRef = windowRef
self.hapticPerformer = hapticPerformer
super.init(nibName: nil, bundle: nil)
}

Expand All @@ -55,13 +39,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(
Expand All @@ -74,58 +112,72 @@ final class CodeEditSplitViewController: NSSplitViewController {
.inspectorCollapsed
) as? Bool ?? true
}

self.insertToolbarItemIfNeeded()
}

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.snapWidth
} else if proposedPosition <= Self.minSidebarWidth / 2 {
hapticCollapse(splitViewItems.first, collapseAction: true)
return 0
} else {
isSnapped = false
if proposedPosition <= CodeEditWindowController.minSidebarWidth / 2 {
splitViewItems.first?.isCollapsed = 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
removeToolbarItemIfNeeded()
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)
}
splitViewItems.last?.isCollapsed = false
insertToolbarItemIfNeeded()
return min(view.frame.width - CodeEditWindowController.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 {
hapticPerformer.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
}

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)
}
}
Expand All @@ -138,36 +190,4 @@ final class CodeEditSplitViewController: NSSplitViewController {
func saveInspectorCollapsedState(isCollapsed: Bool) {
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 {
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
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -118,12 +118,23 @@ 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)
Expand Down
Loading
Loading