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

Save UI state per Workspace #984

Merged
merged 11 commits into from
Feb 20, 2023
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
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ private extension CGFloat {
}

final class CodeEditSplitViewController: NSSplitViewController {
private var workspace: WorkspaceDocument
private let widthStateName: String = "\(String(describing: CodeEditSplitViewController.self))-Width"
private let isNavigatorCollapsedStateName: String
= "\(String(describing: CodeEditSplitViewController.self))-IsNavigatorCollapsed"
private let isInspectorCollapsedStateName: String
= "\(String(describing: CodeEditSplitViewController.self))-IsInspectorCollapsed"
private var setWidthFromState = false

// Properties
private(set) var isSnapped: Bool = false {
willSet {
Expand All @@ -29,7 +37,8 @@ final class CodeEditSplitViewController: NSSplitViewController {

// MARK: - Initialization

init(feedbackPerformer: NSHapticFeedbackPerformer) {
init(workspace: WorkspaceDocument, feedbackPerformer: NSHapticFeedbackPerformer) {
self.workspace = workspace
self.feedbackPerformer = feedbackPerformer
super.init(nibName: nil, bundle: nil)
}
Expand All @@ -39,10 +48,26 @@ final class CodeEditSplitViewController: NSSplitViewController {
fatalError("init(coder:) has not been implemented")
}

// TODO: Set user preferences width if it is not the snap width
// override func viewWillAppear() {
// super.viewWillAppear()
// }
override func viewWillAppear() {
super.viewWillAppear()
let width = workspace.getFromWorkspaceState(key: self.widthStateName) as? CGFloat
splitView.setPosition(width ?? .snapWidth, ofDividerAt: .zero)
setWidthFromState = true

if let firstSplitView = splitViewItems.first {
firstSplitView.isCollapsed = workspace.getFromWorkspaceState(
key: isNavigatorCollapsedStateName
) as? Bool ?? false
}

if let lastSplitView = splitViewItems.last {
lastSplitView.isCollapsed = workspace.getFromWorkspaceState(
key: isInspectorCollapsedStateName
) as? Bool ?? true
}

self.insertToolbarItemIfNeeded()
}

// MARK: - NSSplitViewDelegate

Expand Down Expand Up @@ -78,6 +103,28 @@ final class CodeEditSplitViewController: NSSplitViewController {
return proposedPosition
}

override func splitViewDidResizeSubviews(_ notification: 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 {
workspace.addToWorkspaceState(key: self.widthStateName, value: width)
}
}
}

func saveNavigatorCollapsedState(isCollapsed: Bool) {
workspace.addToWorkspaceState(key: isNavigatorCollapsedStateName, value: isCollapsed)
}

func saveInspectorCollapsedState(isCollapsed: Bool) {
workspace.addToWorkspaceState(key: isInspectorCollapsedStateName, 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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate {

private func setupSplitView(with workspace: WorkspaceDocument) {
let feedbackPerformer = NSHapticFeedbackManager.defaultPerformer
let splitVC = CodeEditSplitViewController(feedbackPerformer: feedbackPerformer)
let splitVC = CodeEditSplitViewController(workspace: workspace, feedbackPerformer: feedbackPerformer)

let navigatorView = NavigatorSidebarView(workspace: workspace)
let navigator = NSSplitViewItem(
Expand Down Expand Up @@ -215,6 +215,9 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate {
@objc 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)
}
}

@objc func toggleLastPanel() {
Expand All @@ -225,6 +228,9 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate {
} else {
window?.toolbar?.insertItem(withItemIdentifier: .itemListTrackingSeparator, at: 4)
}
if let codeEditSplitVC = splitViewController as? CodeEditSplitViewController {
codeEditSplitVC.saveInspectorCollapsedState(isCollapsed: lastSplitView.isCollapsed)
}
}

private func getSelectedCodeFile() -> CodeFileDocument? {
Expand Down
120 changes: 88 additions & 32 deletions CodeEdit/Features/Documents/WorkspaceDocument.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,27 @@ import CodeEditKit
@Published var selectionState: WorkspaceSelectionState = .init()
@Published var fileItems: [WorkspaceClient.FileItem] = []

var workspaceState: [String: Any] {
get {
let key = "workspaceState-\(self.fileURL?.absoluteString ?? "")"
return UserDefaults.standard.object(forKey: key) as? [String: Any] ?? [:]
}
set {
let key = "workspaceState-\(self.fileURL?.absoluteString ?? "")"
UserDefaults.standard.set(newValue, forKey: key)
}
}

var statusBarModel: StatusBarViewModel?
var searchState: SearchState?
var quickOpenViewModel: QuickOpenViewModel?
var commandsPaletteState: CommandPaletteViewModel?
var listenerModel: WorkspaceNotificationModel = .init()

private var cancellables = Set<AnyCancellable>()
private let openTabsStateName: String = "\(String(describing: WorkspaceDocument.self))-OpenTabs"
private let activeTabStateName: String = "\(String(describing: WorkspaceDocument.self))-ActiveTab"
private var openedTabsFromState = false

@Published var targets: [Target] = []

Expand All @@ -36,6 +51,14 @@ import CodeEditKit
NotificationCenter.default.removeObserver(self)
}

func getFromWorkspaceState(key: String) -> Any? {
return workspaceState[key]
}

func addToWorkspaceState(key: String, value: Any) {
workspaceState.updateValue(value, forKey: key)
}

// MARK: Open Tabs
/// Opens new tab
/// - Parameter item: any item which can be represented as a tab
Expand Down Expand Up @@ -147,6 +170,30 @@ import CodeEditKit
closeTabs(items: range)
}

/// Switched the active tab to current tab
/// - Parameter item: tab item that is now active.
func switchedTab(item: TabBarItemRepresentable) {
selectionState.selectedId = item.tabID
guard let fileItem = item as? WorkspaceClient.FileItem else { return }
self.addToWorkspaceState(key: activeTabStateName, value: fileItem.url.absoluteString)
}

/// Tabs reordered
/// - Parameter openedTabs: reordered tabs
func reorderedTabs(openedTabs: [TabBarItemID]) {
selectionState.openedTabs = openedTabs

if openedTabsFromState {
var openTabsInState: [String] = []
for openTabId in openedTabs {
guard let item = selectionState.getItemByTab(id: openTabId) as? WorkspaceClient.FileItem
else { continue }
openTabsInState.append(item.url.absoluteString)
}
self.addToWorkspaceState(key: openTabsStateName, value: openTabsInState)
}
}

/// Closes an open temporary tab, does not save the temporary tab's file.
/// Removes the tab item from `openedCodeFiles`, `openedExtensions`, and `openFileItems`.
private func closeTemporaryTab() {
Expand Down Expand Up @@ -196,6 +243,14 @@ import CodeEditKit
selectionState.openedCodeFiles.removeValue(forKey: item)
selectionState.openFileItems.remove(at: openFileItemIndex)
removeTab(id: item.tabID)

if openedTabsFromState {
var openTabsInState = self.getFromWorkspaceState(key: openTabsStateName) as? [String] ?? []
if let index = openTabsInState.firstIndex(of: item.url.absoluteString) {
openTabsInState.remove(at: index)
self.addToWorkspaceState(key: openTabsStateName, value: openTabsInState)
}
}
}

private func closeExtensionTab(item: Plugin) {
Expand All @@ -209,8 +264,19 @@ import CodeEditKit
@objc func convertTemporaryTab() {
if selectionState.selectedId == selectionState.temporaryTab &&
selectionState.temporaryTab != nil {
let item = selectionState.getItemByTab(id: selectionState.temporaryTab!)
selectionState.previousTemporaryTab = selectionState.temporaryTab
selectionState.temporaryTab = nil

guard let file = item as? WorkspaceClient.FileItem else { return }

if openedTabsFromState && item != nil {
var openTabsInState = self.getFromWorkspaceState(key: openTabsStateName) as? [String] ?? []
if !openTabsInState.contains(file.url.absoluteString) {
openTabsInState.append(file.url.absoluteString)
self.addToWorkspaceState(key: openTabsStateName, value: openTabsInState)
}
}
}
}

Expand Down Expand Up @@ -269,6 +335,27 @@ import CodeEditKit
windowController.shouldCascadeWindows = false
windowController.window?.setFrameAutosaveName(self.fileURL?.absoluteString ?? "Untitled")
self.addWindowController(windowController)

var activeTabID: TabBarItemID?
var activeTabInState = self.getFromWorkspaceState(key: activeTabStateName) as? String ?? ""
var openTabsInState = self.getFromWorkspaceState(key: openTabsStateName) as? [String] ?? []
for openTab in openTabsInState {
let tabUrl = URL(string: openTab)!
if FileManager.default.fileExists(atPath: tabUrl.path) {
let item = WorkspaceClient.FileItem(url: tabUrl)
self.openTab(item: item)
self.convertTemporaryTab()
if activeTabInState == openTab {
activeTabID = item.tabID
}
}
}

if activeTabID != nil {
selectionState.selectedId = activeTabID
}

self.openedTabsFromState = true
}

// MARK: Set Up Workspace
Expand All @@ -282,7 +369,7 @@ import CodeEditKit
self.searchState = .init(self)
self.quickOpenViewModel = .init(fileURL: url)
self.commandsPaletteState = .init()
self.statusBarModel = .init(workspaceURL: url)
self.statusBarModel = .init(workspace: self, workspaceURL: url)

NotificationCenter.default.addObserver(
self,
Expand All @@ -292,26 +379,10 @@ import CodeEditKit
)
}

/// Retrieves selection state from UserDefaults using SHA256 hash of project path as key
/// - Throws: `DecodingError.dataCorrupted` error if retrived data from UserDefaults is not decodable
/// - Returns: retrived state from UserDefaults or default state if not found
private func readSelectionState() throws -> WorkspaceSelectionState {
guard let path = fileURL?.path,
let data = UserDefaults.standard.value(forKey: path.sha256()) as? Data else { return selectionState }
let state = try PropertyListDecoder().decode(WorkspaceSelectionState.self, from: data)
return state
}

override func read(from url: URL, ofType typeName: String) throws {
try initWorkspaceState(url)

// Initialize Workspace
do {
selectionState = try readSelectionState()
} catch {
Swift.print("couldn't retrieve selection state from user defaults")
}

workspaceClient?
.getFiles
.sink { [weak self] files in
Expand Down Expand Up @@ -355,22 +426,7 @@ import CodeEditKit

// MARK: Close Workspace

/// Saves selection state to UserDefaults using SHA256 hash of project path as key
/// - Throws: `EncodingError.invalidValue` error if sellection state is not encodable
private func saveSelectionState() throws {
guard let path = fileURL?.path else { return }
let hash = path.sha256()
let data = try PropertyListEncoder().encode(selectionState)
UserDefaults.standard.set(data, forKey: hash)
}

override func close() {
do {
try saveSelectionState()
} catch {
Swift.print("couldn't save selection state from user defaults")
}

selectionState.selectedId = nil
selectionState.openedCodeFiles.removeAll()

Expand Down
28 changes: 27 additions & 1 deletion CodeEdit/Features/StatusBar/ViewModels/StatusBarViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import SwiftUI
/// A model class to host and manage data for the ``StatusBarView``
///
class StatusBarViewModel: ObservableObject {
private let isStatusBarDrawerCollapsedStateName: String
= "\(String(describing: StatusBarViewModel.self))-IsStatusBarDrawerCollapsed"
private let statusBarDrawerHeightStateName: String
= "\(String(describing: StatusBarViewModel.self))-StatusBarDrawerHeight"

// TODO: Implement logic for updating values
// TODO: Add @Published vars for indentation, encoding, linebreak
Expand Down Expand Up @@ -54,6 +58,8 @@ class StatusBarViewModel: ObservableObject {
/// Returns the font for status bar items to use
private(set) var toolbarFont: Font = .system(size: 11)

private(set) var workspace: WorkspaceDocument

/// The base URL of the workspace
private(set) var workspaceURL: URL

Expand All @@ -69,7 +75,27 @@ class StatusBarViewModel: ObservableObject {

/// Initialize with a GitClient
/// - Parameter workspaceURL: the current workspace URL
init(workspaceURL: URL) {
init(workspace: WorkspaceDocument, workspaceURL: URL) {
self.workspace = workspace
self.workspaceURL = workspaceURL

var currentHeight = workspace.getFromWorkspaceState(key: statusBarDrawerHeightStateName) as? Double
?? self.standardHeight
if currentHeight == 0 {
currentHeight = self.standardHeight
}

self.isExpanded = workspace.getFromWorkspaceState(key: isStatusBarDrawerCollapsedStateName) as? Bool ?? false
if self.isExpanded {
self.currentHeight = currentHeight
}
}

func saveIsExpandedToState() {
self.workspace.addToWorkspaceState(key: isStatusBarDrawerCollapsedStateName, value: self.isExpanded)
}

func saveHeightToState(height: Double) {
self.workspace.addToWorkspaceState(key: statusBarDrawerHeightStateName, value: height)
}
}
Loading