// // PreviewViewController.swift // QuickLook // // Created by Ruben Beltran del Rio on 2025-07-11. // import Cocoa import Quartz import SwiftUI class PreviewViewController: NSViewController, QLPreviewingController { override var nibName: NSNib.Name? { return NSNib.Name("PreviewViewController") } override func loadView() { super.loadView() } func preparePreviewOfFile(at url: URL) async throws { // Read the .wmap file content let content = try String(contentsOf: url, encoding: .utf8) // Get the user's preview style preference from shared UserDefaults let sharedDefaults = UserDefaults(suiteName: "group.systems.tranquil.Map") ?? UserDefaults.standard let previewStyleString = sharedDefaults.string(forKey: "quickLookPreviewStyle") ?? QuickLookPreviewStyle.map.rawValue let previewStyle = QuickLookPreviewStyle(rawValue: previewStyleString) ?? .map await MainActor.run { // Clear any existing subviews and force cleanup view.subviews.forEach { subview in if let hostingView = subview as? NSHostingView { hostingView.rootView = AnyView(EmptyView()) } subview.removeFromSuperview() } // Force memory cleanup autoreleasepool { let previewContent: AnyView switch previewStyle { case .plainText: previewContent = AnyView(createPlainTextView(content: content)) case .highlightedText: // Parse for syntax highlighting let lexer = Wmap.Lexer(content) let parser = Wmap.Parser(lexer: lexer) let parsedMap = parser.parse() previewContent = AnyView( createHighlightedTextView(content: content, parsedMap: parsedMap)) case .map: // Parse the content to create a ParsedMap let lexer = Wmap.Lexer(content) let parser = Wmap.Parser(lexer: lexer) let parsedMap = parser.parse() if parsedMap.entities.isEmpty { // Fallback view for empty or invalid maps previewContent = AnyView( VStack { Image(systemName: "map") .font(.system(size: 48)) .foregroundColor(.gray) Text("quick_look.empty_map.title") .font(.Theme.Title.emphasized) .foregroundColor(.Theme.UI.foreground) Text("quick_look.empty_map.description") .font(.Theme.Body.regular) .foregroundColor(.Theme.UI.foreground) } .frame(maxWidth: .infinity, maxHeight: .infinity) .foregroundColor(.Theme.UI.background) ) } else { // Limit entities for memory efficiency in QuickLook let limitedEntities = Array(parsedMap.entities.prefix(100)) // Limit to first 100 entities let limitedMap = Wmap.ParsedMap( entities: limitedEntities, vertexLabels: parsedMap.vertexLabels) previewContent = AnyView(MapPreviewView(parsedMap: limitedMap)) } } let hostingView = NSHostingView(rootView: previewContent) hostingView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(hostingView) NSLayoutConstraint.activate([ hostingView.topAnchor.constraint(equalTo: view.topAnchor), hostingView.leadingAnchor.constraint(equalTo: view.leadingAnchor), hostingView.trailingAnchor.constraint(equalTo: view.trailingAnchor), hostingView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) view.needsLayout = true view.layoutSubtreeIfNeeded() } // autoreleasepool } } private func createPlainTextView(content: String) -> some View { ScrollView([.horizontal, .vertical]) { Text(content) .font(.monospaced(.body)()) .multilineTextAlignment(.leading) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) .padding() } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color.white) } private func createHighlightedTextView(content: String, parsedMap: Wmap.ParsedMap) -> some View { QuickLookTextEditor(content: content, parsedMap: parsedMap) .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color.white) } deinit { // Cleanup when view controller is deallocated view.subviews.forEach { subview in if let hostingView = subview as? NSHostingView { hostingView.rootView = AnyView(EmptyView()) } subview.removeFromSuperview() } } } // A SwiftUI view specifically for QuickLook preview struct MapPreviewView: View { let parsedMap: Wmap.ParsedMap @State private var selectedEvolution: StageType = .general var body: some View { GeometryReader { geometry in let mapSize = Dimensions.Map.size let padding = Dimensions.Map.padding let totalWidth = mapSize.width + 2 * padding let totalHeight = mapSize.height + 2 * padding // Calculate scale to fit the available space let scaleX = geometry.size.width / totalWidth let scaleY = geometry.size.height / totalHeight let scale = min(scaleX, scaleY, 1.0) // Don't scale up, only down ScrollView([.horizontal, .vertical]) { MapRenderView( entities: parsedMap.entities, evolution: $selectedEvolution ) .drawingGroup(opaque: false) .scaleEffect(scale) .frame( width: totalWidth * scale, height: totalHeight * scale ) } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color.white) } } } // A read-only text editor for QuickLook that shows syntax highlighting struct QuickLookTextEditor: NSViewRepresentable { let content: String let parsedMap: Wmap.ParsedMap func makeNSView(context: Context) -> NSScrollView { let scrollView = NSTextView.scrollableTextView() let textView = scrollView.documentView as! NSTextView // Configure the scroll view scrollView.hasVerticalScroller = true scrollView.hasHorizontalScroller = true scrollView.autohidesScrollers = false // Configure the text view for read-only display textView.isEditable = false textView.isSelectable = true textView.backgroundColor = NSColor.textBackgroundColor textView.font = NSFont.monospacedSystemFont(ofSize: 13, weight: .regular) textView.string = content textView.isVerticallyResizable = true textView.isHorizontallyResizable = true textView.textContainer?.widthTracksTextView = false textView.textContainer?.containerSize = NSSize( width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude) // Apply syntax highlighting if let textStorage = textView.textStorage { let syntaxHighlighter = Wmap.SyntaxHighlighter() syntaxHighlighter.updateSyntaxElements(from: parsedMap) // Apply highlighting to the entire text let fullRange = NSRange(location: 0, length: textStorage.length) syntaxHighlighter.applySyntaxHighlighting(textStorage: textStorage, range: fullRange) } return scrollView } func updateNSView(_ nsView: NSScrollView, context: Context) { // No updates needed for read-only view } }