diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel+CRUD.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel+CRUD.swift index 74090f65b..07b03dd14 100644 --- a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel+CRUD.swift +++ b/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel+CRUD.swift @@ -208,9 +208,12 @@ extension ThemeModel { self.save(self.themes[index]) } + self.previousTheme = self.selectedTheme + activateTheme(self.themes[index]) self.detailsTheme = self.themes[index] + self.detailsIsPresented = true } } catch { print("Error adding theme: \(error.localizedDescription)") @@ -238,16 +241,11 @@ extension ThemeModel { iterator += 1 } + let isActive = self.getThemeActive(theme) + try filemanager.moveItem(at: oldURL, to: finalURL) try self.loadThemes() - - if let index = themes.firstIndex(where: { $0.fileURL == finalURL }) { - themes[index].displayName = finalName - themes[index].fileURL = finalURL - themes[index].name = finalName.lowercased().replacingOccurrences(of: " ", with: "-") - } - } catch { print("Error renaming theme: \(error.localizedDescription)") } diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel.swift index c3663f3e6..55bac77d5 100644 --- a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel.swift +++ b/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel.swift @@ -6,6 +6,7 @@ // import SwiftUI +import UniformTypeIdentifiers /// The Theme View Model. Accessible via the singleton "``ThemeModel/shared``". /// @@ -72,7 +73,7 @@ final class ThemeModel: ObservableObject { } } - @Published var presentingDetails: Bool = false + @Published var detailsIsPresented: Bool = false @Published var isAdding: Bool = false @@ -87,10 +88,11 @@ final class ThemeModel: ObservableObject { DispatchQueue.main.async { Settings[\.theme].selectedTheme = self.selectedTheme?.name } - updateAppearanceTheme() } } + @Published var previousTheme: Theme? + /// Only themes where ``Theme/appearance`` == ``Theme/ThemeType/dark`` var darkThemes: [Theme] { themes.filter { $0.appearance == .dark } @@ -127,9 +129,9 @@ final class ThemeModel: ObservableObject { } /// Initialize to the app's current appearance. - @Published var selectedAppearance: ThemeSettingsAppearances = { + var selectedAppearance: ThemeSettingsAppearances { NSApp.effectiveAppearance.name == .darkAqua ? .dark : .light - }() + } enum ThemeSettingsAppearances: String, CaseIterable { case light = "Light Appearance" @@ -137,13 +139,6 @@ final class ThemeModel: ObservableObject { } func getThemeActive(_ theme: Theme) -> Bool { - if settings.matchAppearance { - return selectedAppearance == .dark - ? selectedDarkTheme == theme - : selectedAppearance == .light - ? selectedLightTheme == theme - : selectedTheme == theme - } return selectedTheme == theme } @@ -151,24 +146,63 @@ final class ThemeModel: ObservableObject { /// necessary. /// - Parameter theme: The theme to activate. func activateTheme(_ theme: Theme) { - if settings.matchAppearance { - if selectedAppearance == .dark { - selectedDarkTheme = theme - } else if selectedAppearance == .light { - selectedLightTheme = theme - } - if (selectedAppearance == .dark && colorScheme == .dark) - || (selectedAppearance == .light && colorScheme == .light) { - selectedTheme = theme - } - } else { - selectedTheme = theme - if colorScheme == .light { - selectedLightTheme = theme - } - if colorScheme == .dark { - selectedDarkTheme = theme + selectedTheme = theme + if colorScheme == .light { + selectedLightTheme = theme + } + if colorScheme == .dark { + selectedDarkTheme = theme + } + } + + func exportTheme(_ theme: Theme) { + guard let themeFileURL = theme.fileURL else { + print("Theme file URL not found.") + return + } + + let savePanel = NSSavePanel() + savePanel.allowedContentTypes = [UTType(filenameExtension: "cetheme")!] + savePanel.nameFieldStringValue = theme.displayName + savePanel.prompt = "Export" + savePanel.canCreateDirectories = true + + savePanel.begin { response in + if response == .OK, let destinationURL = savePanel.url { + do { + try FileManager.default.copyItem(at: themeFileURL, to: destinationURL) + print("Theme exported successfully to \(destinationURL.path)") + } catch { + print("Failed to export theme: \(error.localizedDescription)") + } } } } + + func exportAllCustomThemes() { + let openPanel = NSOpenPanel() + openPanel.prompt = "Export" + openPanel.canChooseFiles = false + openPanel.canChooseDirectories = true + openPanel.allowsMultipleSelection = false + + openPanel.begin { result in + if result == .OK, let exportDirectory = openPanel.url { + let customThemes = self.themes.filter { !$0.isBundled } + + for theme in customThemes { + guard let sourceURL = theme.fileURL else { continue } + + let destinationURL = exportDirectory.appendingPathComponent("\(theme.displayName).cetheme") + + do { + try FileManager.default.copyItem(at: sourceURL, to: destinationURL) + print("Exported \(theme.displayName) to \(destinationURL.path)") + } catch { + print("Failed to export \(theme.displayName): \(error.localizedDescription)") + } + } + } + } + } } diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingThemeRow.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingThemeRow.swift index 54a2f3217..58f2403de 100644 --- a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingThemeRow.swift +++ b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingThemeRow.swift @@ -13,10 +13,10 @@ struct ThemeSettingsThemeRow: View { @ObservedObject private var themeModel: ThemeModel = .shared - @State private var presentingDetails: Bool = false - @State private var isHovering = false + @State private var deleteConfirmationIsPresented = false + var body: some View { HStack { Image(systemName: "checkmark") @@ -42,15 +42,20 @@ struct ThemeSettingsThemeRow: View { Menu { Button("Details...") { themeModel.detailsTheme = theme + themeModel.detailsIsPresented = true } - Button("Duplicate") { + Button("Duplicate...") { if let fileURL = theme.fileURL { themeModel.duplicate(fileURL) } } + Button("Export...") { + themeModel.exportTheme(theme) + } + .disabled(theme.isBundled) Divider() - Button("Delete") { - themeModel.delete(theme) + Button("Delete...") { + deleteConfirmationIsPresented = true } .disabled(theme.isBundled) } label: { @@ -63,5 +68,18 @@ struct ThemeSettingsThemeRow: View { .onHover { hovering in isHovering = hovering } + .alert( + Text("Are you sure you want to delete the theme “\(theme.displayName)”?"), + isPresented: $deleteConfirmationIsPresented + ) { + Button("Delete Theme") { + themeModel.delete(theme) + } + Button("Cancel") { + deleteConfirmationIsPresented = false + } + } message: { + Text("This action cannot be undone.") + } } } diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeDetails.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeDetails.swift index cd51bb756..d6fc59657 100644 --- a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeDetails.swift +++ b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeDetails.swift @@ -20,6 +20,14 @@ struct ThemeSettingsThemeDetails: View { @StateObject private var themeModel: ThemeModel = .shared + @State private var duplicatingTheme: Theme? + + @State private var deleteConfirmationIsPresented = false + + var isActive: Bool { + themeModel.getThemeActive(theme) + } + init(theme: Binding) { _theme = theme originalTheme = theme.wrappedValue @@ -168,19 +176,19 @@ struct ThemeSettingsThemeDetails: View { .accessibilityLabel("Warning: Duplicate this theme to make changes.") } else if !themeModel.isAdding { Button(role: .destructive) { - themeModel.delete(theme) - dismiss() + deleteConfirmationIsPresented = true } label: { - Text("Delete") + Text("Delete...") .foregroundStyle(.red) .frame(minWidth: 56) } Button { if let fileURL = theme.fileURL { + duplicatingTheme = theme themeModel.duplicate(fileURL) } } label: { - Text("Duplicate") + Text("Duplicate...") .frame(minWidth: 56) } } @@ -188,6 +196,7 @@ struct ThemeSettingsThemeDetails: View { if !themeModel.isAdding && theme.isBundled { Button { if let fileURL = theme.fileURL { + duplicatingTheme = theme themeModel.duplicate(fileURL) } } label: { @@ -197,12 +206,28 @@ struct ThemeSettingsThemeDetails: View { } else { Button { if themeModel.isAdding { - themeModel.delete(theme) + if let previousTheme = themeModel.previousTheme { + themeModel.activateTheme(previousTheme) + } + if let duplicatingWithinDetails = duplicatingTheme { + let duplicateTheme = theme + themeModel.detailsTheme = duplicatingWithinDetails + themeModel.delete(duplicateTheme) + } else { + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + themeModel.delete(theme) + } + } } else { themeModel.cancelDetails(theme) } - dismiss() + if duplicatingTheme == nil { + dismiss() + } else { + duplicatingTheme = nil + themeModel.isAdding = false + } } label: { Text("Cancel") .frame(minWidth: 56) @@ -223,5 +248,19 @@ struct ThemeSettingsThemeDetails: View { .padding() } .constrainHeightToWindow() + .alert( + Text("Are you sure you want to delete the theme “\(theme.displayName)”?"), + isPresented: $deleteConfirmationIsPresented + ) { + Button("Delete Theme") { + themeModel.delete(theme) + dismiss() + } + Button("Cancel") { + deleteConfirmationIsPresented = false + } + } message: { + Text("This action cannot be undone.") + } } } diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsView.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsView.swift index 49371070a..813479cdc 100644 --- a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsView.swift +++ b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsView.swift @@ -49,10 +49,10 @@ struct ThemeSettingsView: View { Text("Import Theme...") } Button { - // TODO: #1874 + themeModel.exportAllCustomThemes() } label: { Text("Export All Custom Themes...") - }.disabled(true) + } } }) .padding(.horizontal, 5) @@ -90,30 +90,37 @@ struct ThemeSettingsView: View { } .padding(.top, 10) } - .sheet(item: $themeModel.detailsTheme) { - themeModel.isAdding = false - } content: { theme in - if let index = themeModel.themes.firstIndex(where: { + .sheet(isPresented: $themeModel.detailsIsPresented, onDismiss: { + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + themeModel.isAdding = false + } + }, content: { + if let theme = themeModel.detailsTheme, let index = themeModel.themes.firstIndex(where: { $0.fileURL?.absoluteString == theme.fileURL?.absoluteString }) { ThemeSettingsThemeDetails(theme: Binding( get: { themeModel.themes[index] }, set: { newValue in - themeModel.themes[index] = newValue - themeModel.save(newValue) - if settings.selectedTheme == theme.name { - themeModel.activateTheme(newValue) + if themeModel.detailsIsPresented { + themeModel.themes[index] = newValue + themeModel.save(newValue) + if settings.selectedTheme == theme.name { + themeModel.activateTheme(newValue) + } } } )) } - } + }) .onAppear { updateFilteredThemes() } .onChange(of: themeSearchQuery) { _ in updateFilteredThemes() } + .onChange(of: themeModel.themes) { _ in + updateFilteredThemes() + } .onChange(of: colorScheme) { newColorScheme in updateFilteredThemes(overrideColorScheme: newColorScheme) }