// Copyright (C) 2024 Rubén Beltrán del Río // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // You should have received a copy of the GNU General Public License // along with this program. If not, see https://map.tranquil.systems. import SwiftUI import UniformTypeIdentifiers struct MapCommands: Commands { @AppStorage("viewStyle") var viewStyle: ViewStyle = .horizontal @AppStorage("zoom") var zoom = 1.0 @AppStorage("mapTemplates") private var templatesData: Data = Data() @FocusedBinding(\.document) var document: MapDocument? @FocusedValue(\.fileURL) var url: URL? @FocusedBinding(\.selectedEvolution) var selectedEvolution: StageType? @FocusedBinding(\.isSearching) var isSearching private var templates: [Template] { if let decoded = try? JSONDecoder().decode([Template].self, from: templatesData) { return decoded } return [] } private var defaultTemplate: Template? { templates.first { $0.isDefault } } var body: some Commands { // File CommandGroup(after: CommandGroupPlacement.newItem) { Menu(String(localized: "commands.file.new_from_template")) { if templates.isEmpty { Text("commands.file.new_from_template.empty") .disabled(true) } else { ForEach(templates) { template in Button(template.name) { createNewDocument(from: template) } } } } } CommandGroup(after: CommandGroupPlacement.saveItem) { Divider() Button("commands.file.export") { if let selectedEvolution, let document { if let image = document.exportAsImage(withEvolution: selectedEvolution) { let filename = url?.deletingPathExtension().lastPathComponent ?? "Untitled" // Create format selection accessory view controller @MainActor class ExportAccessoryViewController: NSViewController { var formatPopup: NSPopUpButton! let savePanel: NSSavePanel let formats: [(String, NSBitmapImageRep.FileType, UTType, String)] let baseFilename: String init( savePanel: NSSavePanel, formats: [(String, NSBitmapImageRep.FileType, UTType, String)], baseFilename: String ) { self.savePanel = savePanel self.formats = formats self.baseFilename = baseFilename super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func loadView() { let view = NSView(frame: NSRect(x: 0, y: 0, width: 300, height: 30)) let label = NSTextField( labelWithString: String(localized: "commands.file.export.format")) label.frame = NSRect(x: 10, y: 8, width: 45, height: 17) formatPopup = NSPopUpButton( frame: NSRect(x: 60, y: 5, width: 120, height: 20), pullsDown: false) formatPopup.autoenablesItems = false for (name, _, _, _) in formats { formatPopup.addItem(withTitle: name) } for item in formatPopup.itemArray { item.isEnabled = true } // Restore last selected format let lastSelectedFormat = UserDefaults.standard.integer(forKey: "lastExportFormat") let validIndex = min(max(lastSelectedFormat, 0), formats.count - 1) formatPopup.selectItem(at: validIndex) formatPopup.target = self formatPopup.action = #selector(formatChanged(_:)) view.addSubview(label) view.addSubview(formatPopup) self.view = view } @objc func formatChanged(_ sender: NSPopUpButton) { let selectedIndex = sender.indexOfSelectedItem let selectedFormat = formats[selectedIndex] // Update allowed content types savePanel.allowedContentTypes = [selectedFormat.2] // Save the selection for next time UserDefaults.standard.set(selectedIndex, forKey: "lastExportFormat") } } // Add format options let formats: [(String, NSBitmapImageRep.FileType, UTType, String)] = [ ("PNG", .png, .png, "png"), ("JPEG", .jpeg, .jpeg, "jpg"), ("TIFF", .tiff, .tiff, "tiff"), ("BMP", .bmp, .bmp, "bmp"), ("GIF", .gif, .gif, "gif"), ] let savePanel = NSSavePanel() // Set initial content type based on last selection let lastSelectedFormat = UserDefaults.standard.integer(forKey: "lastExportFormat") let validIndex = min(max(lastSelectedFormat, 0), formats.count - 1) savePanel.allowedContentTypes = [formats[validIndex].2] savePanel.canCreateDirectories = true savePanel.isExtensionHidden = false savePanel.title = String(localized: "commands.file.export.panel.title \(filename)") savePanel.message = String(localized: "commands.file.export.panel.content") savePanel.nameFieldStringValue = "\(filename)" let accessoryController = ExportAccessoryViewController( savePanel: savePanel, formats: formats, baseFilename: filename) savePanel.accessoryView = accessoryController.view savePanel.begin { result in if result == .OK, let url = savePanel.url { if let tiffRepresentation = image.tiffRepresentation { let bitmapImage = NSBitmapImageRep(data: tiffRepresentation) let selectedFormat = formats[accessoryController.formatPopup.indexOfSelectedItem] // Generate data based on selected format let imageData = bitmapImage?.representation( using: selectedFormat.1, properties: [:]) do { try imageData?.write(to: url) } catch { return } } } } } } }.keyboardShortcut( "e", modifiers: EventModifiers([.command]) ).disabled(document == nil) } // Edit CommandGroup(after: CommandGroupPlacement.pasteboard) { Divider() Button("commands.edit.find") { isSearching = isSearching != nil ? !isSearching! : true }.keyboardShortcut( "f", modifiers: EventModifiers([.command]) ).disabled(document == nil) } // View CommandGroup(after: CommandGroupPlacement.toolbar) { if viewStyle == .horizontal { Button("commands.view.layout.vertical") { viewStyle = .vertical }.keyboardShortcut( "l", modifiers: EventModifiers([.command]) ).disabled(document == nil) } else { Button("commands.view.layout.horizontal") { viewStyle = .horizontal }.keyboardShortcut( "l", modifiers: EventModifiers([.command]) ).disabled(document == nil) } Divider() Button("commands.view.zoom_in") { zoom = min(Constants.kMaxZoom, zoom + 0.1) }.keyboardShortcut( "+", modifiers: EventModifiers([.command]) ).disabled(document == nil) Button("commands.view.zoom_out") { zoom = max(Constants.kMinZoom, zoom - 0.1) }.keyboardShortcut( "-", modifiers: EventModifiers([.command]) ).disabled(document == nil) Divider() } CommandGroup(replacing: CommandGroupPlacement.help) { Button("commands.help.help") { NSWorkspace.shared.open(URL(string: "https://map.tranquil.systems")!) } } } @MainActor private func createNewDocument(from template: Template) { // Store the template content in UserDefaults temporarily UserDefaults.standard.set(template.content, forKey: "pendingTemplateContent") // Create a new document using the standard mechanism NSApplication.shared.sendAction( #selector(NSDocumentController.newDocument(_:)), to: nil, from: nil) // Clear the pending template content after a short delay DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { UserDefaults.standard.removeObject(forKey: "pendingTemplateContent") } } }