Neon-Vision-Editor/Neon Vision Editor/ContentView+Toolbar.swift
h3p 6fc11927f2 Add New Tab toolbar action and stabilize detached window editor state
- Add a dedicated “New Tab” toolbar button across iOS/iPadOS/macOS
- Keep toolbar behavior aligned with existing menubar New Tab action
- Refactor “New Window” scene to use a per-window @StateObject EditorViewModel
- Prevent detached window state resets that could surface as sidebar toggles/text loss after paste
- Preserve existing shared error bindings and window sizing behavior
2026-02-07 19:46:08 +01:00

449 lines
16 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import SwiftUI
#if os(macOS)
import AppKit
#elseif os(iOS)
import UIKit
#endif
extension ContentView {
#if os(iOS)
private var isIPadToolbarLayout: Bool {
UIDevice.current.userInterfaceIdiom == .pad && horizontalSizeClass == .regular
}
private var iPadToolbarMaxWidth: CGFloat {
let screenWidth = UIApplication.shared.connectedScenes
.compactMap { $0 as? UIWindowScene }
.first(where: { $0.activationState == .foregroundActive })?
.screen.bounds.width ?? 1024
let target = screenWidth * 0.72
return min(max(target, 560), 980)
}
private var iPadPromotedActionsCount: Int {
switch iPadToolbarMaxWidth {
case 920...: return 7
case 840...: return 6
case 760...: return 5
case 680...: return 4
case 620...: return 3
default: return 2
}
}
@ViewBuilder
private var newTabControl: some View {
Button(action: { viewModel.addNewTab() }) {
Image(systemName: "plus.square.on.square")
}
.help("New Tab")
}
@ViewBuilder
private var languagePickerControl: some View {
Picker("Language", selection: currentLanguageBinding) {
ForEach(["swift", "python", "javascript", "typescript", "java", "kotlin", "go", "ruby", "rust", "sql", "html", "css", "cpp", "csharp", "objective-c", "json", "xml", "yaml", "toml", "ini", "markdown", "bash", "zsh", "powershell", "standard", "plain"], id: \.self) { lang in
let label: String = {
switch lang {
case "objective-c": return "Objective-C"
case "csharp": return "C#"
case "cpp": return "C++"
case "json": return "JSON"
case "xml": return "XML"
case "yaml": return "YAML"
case "toml": return "TOML"
case "ini": return "INI"
case "sql": return "SQL"
case "html": return "HTML"
case "css": return "CSS"
case "standard": return "Standard"
default: return lang.capitalized
}
}()
Text(label).tag(lang)
}
}
.labelsHidden()
.help("Language")
.frame(width: isIPadToolbarLayout ? 160 : 120)
}
@ViewBuilder
private var aiSelectorControl: some View {
Button(action: {
showAISelectorPopover.toggle()
}) {
Image(systemName: "brain.head.profile")
}
.help("AI Model & Settings")
.popover(isPresented: $showAISelectorPopover) {
VStack(alignment: .leading, spacing: 8) {
Text("AI Model").font(.headline)
Picker("AI Model", selection: $selectedModel) {
HStack(spacing: 6) {
Image(systemName: "brain.head.profile")
Text("Apple Intelligence")
}
.tag(AIModel.appleIntelligence)
Text("Grok").tag(AIModel.grok)
Text("OpenAI").tag(AIModel.openAI)
Text("Gemini").tag(AIModel.gemini)
Text("Anthropic").tag(AIModel.anthropic)
}
.labelsHidden()
.frame(width: 170)
.controlSize(.large)
Button("API Settings…") {
showAISelectorPopover = false
showAPISettings = true
}
.buttonStyle(.bordered)
}
.padding(12)
}
}
@ViewBuilder
private var activeProviderBadgeControl: some View {
Text(activeProviderName)
.font(.caption)
.foregroundColor(.secondary)
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(Color.secondary.opacity(0.12), in: Capsule())
.help("Active provider")
}
@ViewBuilder
private var clearEditorControl: some View {
Button(action: {
clearEditorContent()
}) {
Image(systemName: "trash")
}
.help("Clear Editor")
}
@ViewBuilder
private var openFileControl: some View {
Button(action: { openFileFromToolbar() }) {
Image(systemName: "folder")
}
.help("Open File…")
}
@ViewBuilder
private var saveFileControl: some View {
Button(action: { saveCurrentTabFromToolbar() }) {
Image(systemName: "square.and.arrow.down")
}
.disabled(viewModel.selectedTab == nil)
.help("Save File")
}
@ViewBuilder
private var toggleSidebarControl: some View {
Button(action: { toggleSidebarFromToolbar() }) {
Image(systemName: "sidebar.left")
}
.help("Toggle Sidebar")
}
@ViewBuilder
private var toggleProjectSidebarControl: some View {
Button(action: { showProjectStructureSidebar.toggle() }) {
Image(systemName: "sidebar.right")
}
.help("Toggle Project Structure Sidebar")
}
@ViewBuilder
private var findReplaceControl: some View {
Button(action: { showFindReplace = true }) {
Image(systemName: "magnifyingglass")
}
.help("Find & Replace")
}
@ViewBuilder
private var lineWrapControl: some View {
Button(action: { viewModel.isLineWrapEnabled.toggle() }) {
Image(systemName: viewModel.isLineWrapEnabled ? "text.justify" : "text.alignleft")
}
.help(viewModel.isLineWrapEnabled ? "Disable Wrap" : "Enable Wrap")
}
@ViewBuilder
private var autoCompletionControl: some View {
Button(action: { isAutoCompletionEnabled.toggle() }) {
Image(systemName: isAutoCompletionEnabled ? "bolt.horizontal.circle.fill" : "bolt.horizontal.circle")
}
.help(isAutoCompletionEnabled ? "Disable Code Completion" : "Enable Code Completion")
}
@ViewBuilder
private var iPadPromotedActions: some View {
if iPadPromotedActionsCount >= 1 { openFileControl }
if iPadPromotedActionsCount >= 2 { saveFileControl }
if iPadPromotedActionsCount >= 3 { toggleSidebarControl }
if iPadPromotedActionsCount >= 4 { toggleProjectSidebarControl }
if iPadPromotedActionsCount >= 5 { findReplaceControl }
if iPadPromotedActionsCount >= 6 { lineWrapControl }
if iPadPromotedActionsCount >= 7 { autoCompletionControl }
}
@ViewBuilder
private var moreActionsControl: some View {
Menu {
Button(action: { isAutoCompletionEnabled.toggle() }) {
Label(isAutoCompletionEnabled ? "Disable Code Completion" : "Enable Code Completion", systemImage: isAutoCompletionEnabled ? "bolt.horizontal.circle.fill" : "bolt.horizontal.circle")
}
Button(action: { openFileFromToolbar() }) {
Label("Open File…", systemImage: "folder")
}
Button(action: { saveCurrentTabFromToolbar() }) {
Label("Save File", systemImage: "square.and.arrow.down")
}
.disabled(viewModel.selectedTab == nil)
Button(action: { toggleSidebarFromToolbar() }) {
Label("Toggle Sidebar", systemImage: "sidebar.left")
}
Button(action: { showProjectStructureSidebar.toggle() }) {
Label("Toggle Project Structure Sidebar", systemImage: "sidebar.right")
}
Button(action: { showFindReplace = true }) {
Label("Find & Replace", systemImage: "magnifyingglass")
}
Button(action: {
viewModel.isBrainDumpMode.toggle()
UserDefaults.standard.set(viewModel.isBrainDumpMode, forKey: "BrainDumpModeEnabled")
}) {
Label("Brain Dump Mode", systemImage: "note.text")
}
Button(action: {
enableTranslucentWindow.toggle()
UserDefaults.standard.set(enableTranslucentWindow, forKey: "EnableTranslucentWindow")
NotificationCenter.default.post(name: .toggleTranslucencyRequested, object: enableTranslucentWindow)
}) {
Label("Translucent Window Background", systemImage: enableTranslucentWindow ? "rectangle.fill" : "rectangle")
}
Button(action: { viewModel.isLineWrapEnabled.toggle() }) {
Label(viewModel.isLineWrapEnabled ? "Disable Wrap" : "Enable Wrap", systemImage: viewModel.isLineWrapEnabled ? "text.justify" : "text.alignleft")
}
} label: {
Image(systemName: "ellipsis.circle")
}
.help("More Actions")
}
@ViewBuilder
private var iOSToolbarControls: some View {
languagePickerControl
newTabControl
aiSelectorControl
activeProviderBadgeControl
clearEditorControl
moreActionsControl
}
@ViewBuilder
private var iPadDistributedToolbarControls: some View {
languagePickerControl
newTabControl
Spacer(minLength: 18)
iPadPromotedActions
Spacer(minLength: 18)
aiSelectorControl
activeProviderBadgeControl
Spacer(minLength: 18)
clearEditorControl
moreActionsControl
}
#endif
@ToolbarContentBuilder
var editorToolbarContent: some ToolbarContent {
#if os(iOS)
ToolbarItemGroup(placement: .topBarTrailing) {
HStack(spacing: 14) {
if isIPadToolbarLayout {
iPadDistributedToolbarControls
} else {
iOSToolbarControls
}
}
.frame(maxWidth: isIPadToolbarLayout ? iPadToolbarMaxWidth : .infinity, alignment: .trailing)
}
#else
ToolbarItemGroup(placement: .automatic) {
Picker("Language", selection: currentLanguageBinding) {
ForEach(["swift", "python", "javascript", "typescript", "java", "kotlin", "go", "ruby", "rust", "sql", "html", "css", "cpp", "csharp", "objective-c", "json", "xml", "yaml", "toml", "ini", "markdown", "bash", "zsh", "powershell", "standard", "plain"], id: \.self) { lang in
let label: String = {
switch lang {
case "objective-c": return "ObjectiveC"
case "csharp": return "C#"
case "cpp": return "C++"
case "json": return "JSON"
case "xml": return "XML"
case "yaml": return "YAML"
case "toml": return "TOML"
case "ini": return "INI"
case "sql": return "SQL"
case "html": return "HTML"
case "css": return "CSS"
case "standard": return "Standard"
default: return lang.capitalized
}
}()
Text(label).tag(lang)
}
}
.labelsHidden()
.help("Language")
.controlSize(.large)
.frame(width: 140)
.padding(.vertical, 2)
Button(action: {
showAISelectorPopover.toggle()
}) {
Image(systemName: "brain.head.profile")
}
.help("AI Model & Settings")
.popover(isPresented: $showAISelectorPopover) {
VStack(alignment: .leading, spacing: 8) {
Text("AI Model").font(.headline)
Picker("AI Model", selection: $selectedModel) {
HStack(spacing: 6) {
Image(systemName: "brain.head.profile")
Text("Apple Intelligence")
}
.tag(AIModel.appleIntelligence)
Text("Grok").tag(AIModel.grok)
Text("OpenAI").tag(AIModel.openAI)
Text("Gemini").tag(AIModel.gemini)
Text("Anthropic").tag(AIModel.anthropic)
}
.labelsHidden()
.frame(width: 170)
.controlSize(.large)
Button("API Settings…") {
showAISelectorPopover = false
showAPISettings = true
}
.buttonStyle(.bordered)
}
.padding(12)
}
Text(activeProviderName)
.font(.caption)
.foregroundColor(.secondary)
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(Color.secondary.opacity(0.12), in: Capsule())
.help("Active provider")
Button(action: {
clearEditorContent()
}) {
Image(systemName: "trash")
}
.help("Clear Editor")
Button(action: {
isAutoCompletionEnabled.toggle()
}) {
Image(systemName: isAutoCompletionEnabled ? "bolt.horizontal.circle.fill" : "bolt.horizontal.circle")
}
.help(isAutoCompletionEnabled ? "Disable Code Completion" : "Enable Code Completion")
Button(action: { openFileFromToolbar() }) {
Image(systemName: "folder")
}
.help("Open File…")
Button(action: { viewModel.addNewTab() }) {
Image(systemName: "plus.square.on.square")
}
.help("New Tab")
#if os(macOS)
Button(action: {
openWindow(id: "blank-window")
}) {
Image(systemName: "macwindow.badge.plus")
}
.help("New Window")
#endif
Button(action: {
saveCurrentTabFromToolbar()
}) {
Image(systemName: "square.and.arrow.down")
}
.disabled(viewModel.selectedTab == nil)
.help("Save File")
Button(action: {
toggleSidebarFromToolbar()
}) {
Image(systemName: "sidebar.left")
.symbolVariant(viewModel.showSidebar ? .fill : .none)
}
.help("Toggle Sidebar")
Button(action: {
showProjectStructureSidebar.toggle()
}) {
Image(systemName: "sidebar.right")
.symbolVariant(showProjectStructureSidebar ? .fill : .none)
}
.help("Toggle Project Structure Sidebar")
Button(action: {
showFindReplace = true
}) {
Image(systemName: "magnifyingglass")
}
.keyboardShortcut("f", modifiers: .command)
.help("Find & Replace")
Button(action: {
viewModel.isBrainDumpMode.toggle()
UserDefaults.standard.set(viewModel.isBrainDumpMode, forKey: "BrainDumpModeEnabled")
}) {
Image(systemName: viewModel.isBrainDumpMode ? "note.text" : "note.text")
.symbolVariant(viewModel.isBrainDumpMode ? .fill : .none)
}
.help("Brain Dump Mode")
.accessibilityLabel("Brain Dump Mode")
Button(action: {
enableTranslucentWindow.toggle()
UserDefaults.standard.set(enableTranslucentWindow, forKey: "EnableTranslucentWindow")
NotificationCenter.default.post(name: .toggleTranslucencyRequested, object: enableTranslucentWindow)
}) {
Image(systemName: enableTranslucentWindow ? "rectangle.fill" : "rectangle")
}
.help("Toggle Translucent Window Background")
.accessibilityLabel("Translucent Window Background")
Button(action: { viewModel.isLineWrapEnabled.toggle() }) {
Image(systemName: viewModel.isLineWrapEnabled ? "text.justify" : "text.alignleft")
}
.help(viewModel.isLineWrapEnabled ? "Disable Wrap" : "Enable Wrap")
}
#endif
}
}