Changes to the Sidebar. And Fixing Errors in main window

This commit is contained in:
Rodric Krogh 2025-08-26 13:22:53 +02:00
parent d3bd921823
commit b353a09064

View file

@ -5,218 +5,6 @@ import Combine
import SwiftData
import AppKit // Explicitly import for NSColor
// MARK: - ContentView
// This is the main view that displays the editor interface with a sidebar and tabbed content area
struct ContentView: View {
@EnvironmentObject var viewModel: ContentViewModel // Injects the view model to manage state and data
@Environment(\.modelContext) private var modelContext // Provides access to the SwiftData context for persistence
@State private var isRenaming: [UUID: Bool] = [:] // Tracks which tab is being renamed
@FocusState private var isFocused: Bool // Manages focus state for renaming
var body: some View {
// Horizontal stack to layout sidebar and content side by side
HStack(spacing: 0) {
// Sidebar section to list and manage tabs
List(viewModel.tabs, id: \.id, selection: $viewModel.selectedTab) { item in
// Horizontal stack for each tab item in the list
HStack {
if isRenaming[item.id, default: false] {
TextField("Tab Name", text: Binding(
get: { item.name },
set: { viewModel.renameTab(item, to: $0) }
))
.textFieldStyle(RoundedBorderTextFieldStyle())
.foregroundColor(.black) // Changed to black for renaming text
.background(Color(nsColor: .windowBackgroundColor).opacity(0.85)) // Match sidebar background
.focused($isFocused) // Enable focus management
.onSubmit {
isRenaming[item.id] = false // Commit rename on Enter
isFocused = false
}
.onExitCommand {
isRenaming[item.id] = false // Commit on focus loss
isFocused = false
}
} else {
Text(item.name) // Displays the tab's name
.foregroundColor(.black) // Changed to black
.font(.system(size: 12)) // Smaller font size
}
Spacer() // Pushes the close button to the right
Button("x") {
viewModel.removeTab(item) // Calls method to remove the tab
}
.buttonStyle(.borderless) // Removes default button styling
.foregroundColor(.red) // Colors the close button red
}
.onTapGesture(count: 2) {
isRenaming[item.id] = true // Enable renaming on double-click
isFocused = true // Focus the text field
}
}
.frame(width: 200) // Fixed width for the sidebar
.background(Color(nsColor: .windowBackgroundColor).opacity(0.85)) // Applies a semi-transparent background
.listStyle(SidebarListStyle()) // Applies a sidebar-specific list style
.frame(minHeight: 0) // Ensure minimum height to prevent collapse
// Tabbed content area to display the selected tab's content with scroll
ScrollView {
VStack(spacing: 0) {
if let selectedItem = viewModel.selectedTab {
// Language picker for the selected tab
Picker("Language", selection: Binding(
get: { viewModel.selectedLanguage }, // Gets the current language
set: { viewModel.selectedLanguage = $0 } // Sets the new language
)) {
ForEach(viewModel.languages, id: \.self) { lang in
Text(lang).tag(lang.lowercased()) // Creates options for language selection
}
}
.pickerStyle(MenuPickerStyle()) // Uses a menu-style picker
.padding(.horizontal) // Adds horizontal padding
.frame(height: 30) // Fixed height for the picker
.background(Color(nsColor: .windowBackgroundColor).opacity(0.85)) // Semi-transparent background
// Custom text editor for the selected tab's content
CustomTextEditor(text: Binding(
get: { selectedItem.content }, // Gets the content of the selected item
set: { viewModel.updateContent($0) } // Updates content when changed
), language: Binding(
get: { selectedItem.language }, // Gets the language of the selected item
set: { viewModel.updateLanguage($0) } // Updates language when changed
), highlightr: HighlightrViewModel().highlightr)
.frame(maxWidth: .infinity, maxHeight: .infinity) // Expands to fill available space
} else {
Text("No tabs open") // Displayed when no tabs are selected
.foregroundColor(.primary) // Dynamic color for light/dark mode using SwiftUI's .primary
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity) // Expands to fill remaining space
}
.frame(minWidth: 1000, minHeight: 600) // Minimum dimensions for the window
.background(Color(nsColor: .textBackgroundColor).opacity(0.85)) // Semi-transparent background
// Temporarily removed overlay to test if it blocks interactions
// .overlay(
// Rectangle()
// .fill(Color(nsColor: .windowBackgroundColor).opacity(0.85))
// .edgesIgnoringSafeArea(.all)
// )
.onAppear {
// Load tabs when the view appears
print("ContentView appeared, loading tabs. Tabs count: \(viewModel.tabs.count)")
viewModel.loadTabs()
print("After load, tabs count: \(viewModel.tabs.count), selectedTab: \(String(describing: viewModel.selectedTab))")
}
}
}
// MARK: - CustomTextEditor
// A custom NSViewRepresentable to integrate NSTextView with syntax highlighting
struct CustomTextEditor: NSViewRepresentable {
@Binding var text: String // Binding to the text content
@Binding var language: String // Binding to the language for highlighting
let highlightr: Highlightr // Highlightr instance for syntax highlighting
func makeNSView(context: Context) -> NSTextView {
// Creates a text storage with Highlightr for syntax highlighting
let textStorage = CodeAttributedString(highlightr: highlightr)
let layoutManager = NSLayoutManager()
textStorage.addLayoutManager(layoutManager)
let textContainer = NSTextContainer()
textContainer.widthTracksTextView = true
textContainer.heightTracksTextView = true
textContainer.containerSize = CGSize(width: 0, height: CGFloat.greatestFiniteMagnitude) // Enable vertical scrolling
layoutManager.addTextContainer(textContainer)
// Configures the NSTextView
let textView = NSTextView(frame: .zero, textContainer: textContainer)
textView.isEditable = true // Allows editing
textView.isSelectable = true // Allows selection
textView.font = NSFont.monospacedSystemFont(ofSize: 14, weight: .regular) // Monospaced font
textView.backgroundColor = NSColor.textBackgroundColor.withAlphaComponent(0.85) // Semi-transparent background
textView.textColor = NSColor(white: 0.2, alpha: 0.85) // Adjusted to dark grey to match intended theme
textView.delegate = context.coordinator // Sets the coordinator as delegate
// Adds a scroll view to handle large content
let scrollView = NSScrollView(frame: textView.bounds)
scrollView.documentView = textView
scrollView.hasVerticalScroller = true // Enables vertical scrolling
scrollView.hasHorizontalScroller = true // Enables horizontal scrolling
scrollView.autoresizingMask = [.width, .height] // Resizes with the view
scrollView.autohidesScrollers = true // Hides scrollers when not needed
// Initializes the text view with the current text and language
DispatchQueue.main.async {
textView.string = text
textStorage.beginEditing()
if let highlightedText = highlightr.highlight(text, as: language) {
textStorage.setAttributedString(highlightedText)
}
textStorage.endEditing()
textStorage.language = language
}
return textView
}
func updateNSView(_ nsView: NSTextView, context: Context) {
// Updates the text view when the bound text or language changes
if nsView.string != text {
nsView.string = text
let textStorage = nsView.textStorage as! CodeAttributedString
textStorage.beginEditing()
if let highlightedText = highlightr.highlight(text, as: language) {
textStorage.setAttributedString(highlightedText)
}
textStorage.endEditing()
textStorage.language = language
}
}
func makeCoordinator() -> Coordinator {
// Creates a coordinator to handle text view delegate methods
Coordinator(self)
}
class Coordinator: NSObject, NSTextViewDelegate {
var parent: CustomTextEditor // Reference to the parent view
var language: String // Tracks the current language
init(_ parent: CustomTextEditor) {
self.parent = parent
self.language = parent.language
}
func textDidChange(_ notification: Notification) {
// Updates the bound text when the text view changes
if let textView = notification.object as? NSTextView {
parent.text = textView.string
}
}
}
}
// MARK: - HighlightrViewModel
// Manages the Highlightr instance for syntax highlighting
class HighlightrViewModel {
let highlightr: Highlightr = {
// Initializes Highlightr with the custom Vibrant Light theme
if let h = Highlightr() {
if let themePath = Bundle.main.path(forResource: "Vibrant Light", ofType: "xccolortheme") {
print("Loading theme from: \(themePath)") // Debug log for theme path
h.setTheme(to: "Vibrant Light")
} else {
print("Failed to find Vibrant Light theme. Falling back to vs2015.")
h.setTheme(to: "vs2015")
}
return h
} else {
fatalError("Highlightr initialization failed. Ensure the package is correctly added via Swift Package Manager.")
}
}()
}
// MARK: - ContentViewModel
// Manages the state and logic for the editor, including tabs and SwiftData persistence
class ContentViewModel: ObservableObject {
@ -239,9 +27,10 @@ class ContentViewModel: ObservableObject {
"json": "json"
] // Maps file extensions to languages
@Environment(\.modelContext) private var modelContext // Access to SwiftData context
private var modelContext: ModelContext // Store context passed from view
init() {
init(modelContext: ModelContext) {
self.modelContext = modelContext
// Initialize by loading existing tabs
loadTabs()
}
@ -281,7 +70,7 @@ class ContentViewModel: ObservableObject {
func saveFile() {
// Saves the content of the selected tab to a file
guard let selectedItem = selectedTab else { return }
let savePanel = NSSavePanel()
let savePanel = NSOpenPanel()
savePanel.allowedContentTypes = [
.text,
.sourceCode,
@ -304,7 +93,7 @@ class ContentViewModel: ObservableObject {
func addNewTab() {
// Adds a new empty tab
let newItem = Item(name: "Untitled-\(tabs.count + 1)", content: "", language: "swift")
let newItem = Item(name: "Note \(tabs.count + 1)", content: "", language: "swift")
tabs.append(newItem)
selectedTab = newItem
saveToSwiftData(newItem.name)
@ -384,3 +173,228 @@ class ContentViewModel: ObservableObject {
}
}
}
// MARK: - ContentView
// This is the main view that displays the editor interface with a sidebar and tabbed content area
struct ContentView: View {
@Environment(\.modelContext) private var modelContext // Provides access to the SwiftData context for persistence
@StateObject private var viewModel = ContentViewModel(modelContext: ModelContext(using: ModelContainer(for: Item.self)))
@State private var isRenaming: [UUID: Bool] = [:] // Tracks which tab is being renamed
@FocusState private var isFocused: Bool // Manages focus state for renaming
var body: some View {
// Horizontal stack to layout sidebar and content side by side
HStack(spacing: 0) {
// Sidebar section to list and manage tabs
List(viewModel.tabs, id: \Item.id, selection: $viewModel.selectedTab) { item in
// Horizontal stack for each tab item in the list
HStack {
if isRenaming[item.id] ?? false { // Use optional binding with nil coalescing
TextField("Tab Name", text: Binding(
get: { item.name },
set: { viewModel.renameTab(item, to: $0) }
))
.textFieldStyle(RoundedBorderTextFieldStyle())
.foregroundColor(.black) // Changed to black for renaming text
.background(Color(nsColor: .windowBackgroundColor).opacity(0.85)) // Match sidebar background
.focused($isFocused) // Enable focus management
.onSubmit {
isRenaming[item.id] = false // Commit rename on Enter
isFocused = false
}
.onExitCommand {
isRenaming[item.id] = false // Commit on focus loss
isFocused = false
}
} else {
Text(item.name) // Displays the tab's name
.foregroundColor(.black) // Changed to black
.font(.system(size: 12)) // Smaller font size
}
Spacer() // Pushes the close button to the right
Button("x") {
viewModel.removeTab(item) // Calls method to remove the tab
}
.buttonStyle(.borderless) // Removes default button styling
.foregroundColor(.red) // Colors the close button red
}
.listRowSeparator(.hidden) // Hide default separator
.padding(.vertical, 4) // Add vertical padding to separate tabs
.background(Color(nsColor: .windowBackgroundColor).opacity(0.7)) // Darker background to distinguish tabs
.cornerRadius(4) // Slight rounding for separation
.onTapGesture(count: 2) {
isRenaming[item.id] = true // Enable renaming on double-click
isFocused = true // Focus the text field
}
.onTapGesture(count: 1) {
if isRenaming[item.id] ?? false {
isRenaming[item.id] = false // Return to normal on single click away
isFocused = false
}
}
}
.frame(width: 200) // Fixed width for the sidebar
.background(Color(nsColor: .windowBackgroundColor).opacity(0.85)) // Applies a semi-transparent background
.listStyle(SidebarListStyle()) // Applies a sidebar-specific list style
.frame(minHeight: 0) // Ensure minimum height to prevent collapse
// Tabbed content area to display the selected tab's content with scroll
ScrollView {
VStack(spacing: 0) {
if let selectedItem = viewModel.selectedTab {
// Language picker for the selected tab
Picker("Language", selection: Binding(
get: { viewModel.selectedLanguage }, // Gets the current language
set: { viewModel.selectedLanguage = $0 } // Sets the new language
)) {
ForEach(viewModel.languages, id: \.self) { lang in
Text(lang).tag(lang.lowercased()) // Creates options for language selection
}
}
.pickerStyle(MenuPickerStyle()) // Uses a menu-style picker
.padding(.horizontal) // Adds horizontal padding
.frame(height: 30) // Fixed height for the picker
.background(Color(nsColor: .windowBackgroundColor).opacity(0.85)) // Semi-transparent background
// Custom text editor for the selected tab's content
CustomTextEditor(text: Binding(
get: { selectedItem.content }, // Gets the content of the selected item
set: { viewModel.updateContent($0); print("Content updated to: \($0)") } // Updates content when changed with debug
), language: Binding(
get: { selectedItem.language }, // Gets the language of the selected item
set: { viewModel.updateLanguage($0) } // Updates language when changed
), highlightr: HighlightrViewModel().highlightr)
.frame(maxWidth: .infinity, maxHeight: .infinity) // Expands to fill available space
} else {
Text("No tabs open") // Displayed when no tabs are selected
.foregroundColor(.primary) // Dynamic color for light/dark mode using SwiftUI's .primary
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity) // Expands to fill remaining space
}
.frame(minWidth: 1000, minHeight: 600) // Minimum dimensions for the window
.background(Color(nsColor: .textBackgroundColor).opacity(0.85)) // Semi-transparent background
// Temporarily removed overlay to test if it blocks interactions
// .overlay(
// Rectangle()
// .fill(Color(nsColor: .windowBackgroundColor).opacity(0.85))
// .edgesIgnoringSafeArea(.all)
// )
.onAppear {
// Load tabs when the view appears
print("ContentView appeared, loading tabs. Tabs count: \(viewModel.tabs.count)")
viewModel.loadTabs()
print("After load, tabs count: \(viewModel.tabs.count), selectedTab: \(String(describing: viewModel.selectedTab))")
}
}
}
// MARK: - CustomTextEditor
// A custom NSViewRepresentable to integrate NSTextView with syntax highlighting
struct CustomTextEditor: NSViewRepresentable {
@Binding var text: String // Binding to the text content
@Binding var language: String // Binding to the language for highlighting
let highlightr: Highlightr // Highlightr instance for syntax highlighting
func makeNSView(context: Context) -> NSTextView {
// Creates a text storage with Highlightr for syntax highlighting
let textStorage = CodeAttributedString(highlightr: highlightr)
let layoutManager = NSLayoutManager()
textStorage.addLayoutManager(layoutManager)
let textContainer = NSTextContainer()
textContainer.widthTracksTextView = true
textContainer.heightTracksTextView = true
textContainer.containerSize = CGSize(width: 0, height: CGFloat.greatestFiniteMagnitude) // Enable vertical scrolling
layoutManager.addTextContainer(textContainer)
// Configures the NSTextView
let textView = NSTextView(frame: .zero, textContainer: textContainer)
textView.isEditable = true // Allows editing
textView.isSelectable = true // Allows selection
textView.font = NSFont.monospacedSystemFont(ofSize: 14, weight: .regular) // Monospaced font
textView.backgroundColor = NSColor.textBackgroundColor.withAlphaComponent(0.85) // Semi-transparent background
textView.textColor = NSColor(white: 0.2, alpha: 0.85) // Adjusted to dark grey to match intended theme
textView.delegate = context.coordinator // Sets the coordinator as delegate
// Adds a scroll view to handle large content
let scrollView = NSScrollView(frame: textView.bounds)
scrollView.documentView = textView
scrollView.hasVerticalScroller = true // Enables vertical scrolling
scrollView.hasHorizontalScroller = true // Enables horizontal scrolling
scrollView.autoresizingMask = [.width, .height] // Resizes with the view
scrollView.autohidesScrollers = true // Hides scrollers when not needed
// Initializes the text view with the current text and language
DispatchQueue.main.async {
textView.string = self.text
textStorage.beginEditing()
if let highlightedText = self.highlightr.highlight(self.text, as: self.language) {
textStorage.setAttributedString(highlightedText)
}
textStorage.endEditing()
textStorage.language = self.language
print("TextEditor initialized with text: \(self.text)") // Debug log
}
return textView
}
func updateNSView(_ nsView: NSTextView, context: Context) {
// Updates the text view when the bound text or language changes
if nsView.string != text {
nsView.string = text
let textStorage = nsView.textStorage as! CodeAttributedString
textStorage.beginEditing()
if let highlightedText = highlightr.highlight(text, as: language) {
textStorage.setAttributedString(highlightedText)
}
textStorage.endEditing()
textStorage.language = language
print("TextEditor updated with text: \(text)") // Debug log
}
}
func makeCoordinator() -> Coordinator {
// Creates a coordinator to handle text view delegate methods
Coordinator(self)
}
class Coordinator: NSObject, NSTextViewDelegate {
var parent: CustomTextEditor // Reference to the parent view
var language: String // Tracks the current language
init(_ parent: CustomTextEditor) {
self.parent = parent
self.language = parent.language
}
func textDidChange(_ notification: Notification) {
// Updates the bound text when the text view changes
if let textView = notification.object as? NSTextView {
parent.text = textView.string
print("Text changed to: \(textView.string)") // Debug log
}
}
}
}
// MARK: - HighlightrViewModel
// Manages the Highlightr instance for syntax highlighting
class HighlightrViewModel {
let highlightr: Highlightr = {
// Initializes Highlightr with the custom Vibrant Light theme
if let h = Highlightr() {
if let themePath = Bundle.main.path(forResource: "Vibrant Light", ofType: "xccolortheme") {
print("Loading theme from: \(themePath)") // Debug log for theme path
h.setTheme(to: "Vibrant Light")
} else {
print("Failed to find Vibrant Light theme. Falling back to vs2015.")
h.setTheme(to: "vs2015")
}
return h
} else {
fatalError("Highlightr initialization failed. Ensure the package is correctly added via Swift Package Manager.")
}
}()
}