From 77efa3988be12d8f735821f5b31c0ff1ba92411f Mon Sep 17 00:00:00 2001 From: Austin Condiff Date: Fri, 25 Oct 2024 09:30:54 -0500 Subject: [PATCH 1/6] Fixed theme details flicker when duplicating and then canceling. --- .../Models/ThemeModel+CRUD.swift | 8 ++++ .../ThemeSettings/Models/ThemeModel.swift | 40 +++++-------------- .../ThemeSettings/ThemeSettingThemeRow.swift | 3 +- .../ThemeSettingsThemeDetails.swift | 29 +++++++++++++- .../ThemeSettings/ThemeSettingsView.swift | 25 +++++++----- 5 files changed, 63 insertions(+), 42 deletions(-) diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel+CRUD.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel+CRUD.swift index 74090f65b..757df4895 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,6 +241,8 @@ extension ThemeModel { iterator += 1 } + let isActive = self.getThemeActive(theme) + try filemanager.moveItem(at: oldURL, to: finalURL) try self.loadThemes() @@ -246,6 +251,9 @@ extension ThemeModel { themes[index].displayName = finalName themes[index].fileURL = finalURL themes[index].name = finalName.lowercased().replacingOccurrences(of: " ", with: "-") + if isActive { + self.activateTheme(themes[index]) + } } } catch { diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel.swift index c3663f3e6..0627b81d9 100644 --- a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel.swift +++ b/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel.swift @@ -72,7 +72,7 @@ final class ThemeModel: ObservableObject { } } - @Published var presentingDetails: Bool = false + @Published var detailsIsPresented: Bool = false @Published var isAdding: Bool = false @@ -87,10 +87,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 +128,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 +138,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 +145,12 @@ 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 } } } diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingThemeRow.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingThemeRow.swift index 54a2f3217..2f6a0b2c8 100644 --- a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingThemeRow.swift +++ b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingThemeRow.swift @@ -13,8 +13,6 @@ struct ThemeSettingsThemeRow: View { @ObservedObject private var themeModel: ThemeModel = .shared - @State private var presentingDetails: Bool = false - @State private var isHovering = false var body: some View { @@ -42,6 +40,7 @@ struct ThemeSettingsThemeRow: View { Menu { Button("Details...") { themeModel.detailsTheme = theme + themeModel.detailsIsPresented = true } Button("Duplicate") { if let fileURL = theme.fileURL { diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeDetails.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeDetails.swift index cd51bb756..000f92e9b 100644 --- a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeDetails.swift +++ b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeDetails.swift @@ -20,6 +20,12 @@ struct ThemeSettingsThemeDetails: View { @StateObject private var themeModel: ThemeModel = .shared + @State private var duplicatingTheme: Theme? + + var isActive: Bool { + themeModel.getThemeActive(theme) + } + init(theme: Binding) { _theme = theme originalTheme = theme.wrappedValue @@ -29,6 +35,7 @@ struct ThemeSettingsThemeDetails: View { VStack(spacing: 0) { Form { Group { + Text("Theme is\(isActive ? "" : " not") active") Section { TextField("Name", text: $theme.displayName) TextField("Author", text: $theme.author) @@ -177,6 +184,7 @@ struct ThemeSettingsThemeDetails: View { } Button { if let fileURL = theme.fileURL { + duplicatingTheme = theme themeModel.duplicate(fileURL) } } label: { @@ -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) diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsView.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsView.swift index 49371070a..71ffeb8b7 100644 --- a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsView.swift +++ b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsView.swift @@ -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) } From 986514fe0539b4a9f70f7e263934bbfed82523f7 Mon Sep 17 00:00:00 2001 From: Austin Condiff Date: Sat, 26 Oct 2024 09:55:10 -0500 Subject: [PATCH 2/6] Fixed bug when renaming a theme where the theme disappeared briefly and then reappeared shortly after with the new name. --- .../Pages/ThemeSettings/Models/ThemeModel+CRUD.swift | 10 ---------- .../ThemeSettings/ThemeSettingsThemeDetails.swift | 1 - 2 files changed, 11 deletions(-) diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel+CRUD.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel+CRUD.swift index 757df4895..07b03dd14 100644 --- a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel+CRUD.swift +++ b/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel+CRUD.swift @@ -246,16 +246,6 @@ extension ThemeModel { 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: "-") - if isActive { - self.activateTheme(themes[index]) - } - } - } catch { print("Error renaming theme: \(error.localizedDescription)") } diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeDetails.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeDetails.swift index 000f92e9b..e420954f6 100644 --- a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeDetails.swift +++ b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeDetails.swift @@ -35,7 +35,6 @@ struct ThemeSettingsThemeDetails: View { VStack(spacing: 0) { Form { Group { - Text("Theme is\(isActive ? "" : " not") active") Section { TextField("Name", text: $theme.displayName) TextField("Author", text: $theme.author) From bef313cb9b6bfef5da867589cd1c8d264ed9dd30 Mon Sep 17 00:00:00 2001 From: Austin Condiff Date: Sat, 26 Oct 2024 17:05:29 -0500 Subject: [PATCH 3/6] Added the ability to export individual custom themes and export all custom themes at once. --- .../ThemeSettings/Models/ThemeModel.swift | 52 +++++++++++++++++++ .../ThemeSettings/ThemeSettingThemeRow.swift | 5 ++ .../ThemeSettings/ThemeSettingsView.swift | 4 +- 3 files changed, 59 insertions(+), 2 deletions(-) diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel.swift index 0627b81d9..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``". /// @@ -153,4 +154,55 @@ final class ThemeModel: ObservableObject { 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 2f6a0b2c8..c60f11ea2 100644 --- a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingThemeRow.swift +++ b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingThemeRow.swift @@ -47,6 +47,11 @@ struct ThemeSettingsThemeRow: View { themeModel.duplicate(fileURL) } } + Divider() + Button("Export...") { + themeModel.exportTheme(theme) + } + .disabled(theme.isBundled) Divider() Button("Delete") { themeModel.delete(theme) diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsView.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsView.swift index 71ffeb8b7..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) From acc2d35c99c89d19b7383ea40d821b7ccf69b4e3 Mon Sep 17 00:00:00 2001 From: Austin Condiff Date: Sat, 26 Oct 2024 17:07:48 -0500 Subject: [PATCH 4/6] Removed divider --- .../Pages/ThemeSettings/ThemeSettingThemeRow.swift | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingThemeRow.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingThemeRow.swift index c60f11ea2..bab152fab 100644 --- a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingThemeRow.swift +++ b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingThemeRow.swift @@ -42,16 +42,15 @@ struct ThemeSettingsThemeRow: View { themeModel.detailsTheme = theme themeModel.detailsIsPresented = true } - Button("Duplicate") { + Button("Duplicate...") { if let fileURL = theme.fileURL { themeModel.duplicate(fileURL) } } - Divider() - Button("Export...") { - themeModel.exportTheme(theme) - } - .disabled(theme.isBundled) + Button("Export...") { + themeModel.exportTheme(theme) + } + .disabled(theme.isBundled) Divider() Button("Delete") { themeModel.delete(theme) From 7fccb9b1645f746acca8196110f3e3290cfae608 Mon Sep 17 00:00:00 2001 From: Austin Condiff Date: Sat, 26 Oct 2024 17:28:26 -0500 Subject: [PATCH 5/6] Added confermation alert to the delete theme action. --- .../ThemeSettings/ThemeSettingThemeRow.swift | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingThemeRow.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingThemeRow.swift index bab152fab..58f2403de 100644 --- a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingThemeRow.swift +++ b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingThemeRow.swift @@ -15,6 +15,8 @@ struct ThemeSettingsThemeRow: View { @State private var isHovering = false + @State private var deleteConfirmationIsPresented = false + var body: some View { HStack { Image(systemName: "checkmark") @@ -52,8 +54,8 @@ struct ThemeSettingsThemeRow: View { } .disabled(theme.isBundled) Divider() - Button("Delete") { - themeModel.delete(theme) + Button("Delete...") { + deleteConfirmationIsPresented = true } .disabled(theme.isBundled) } label: { @@ -66,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.") + } } } From 25c010712f47573890186ca3e349941f29cfeb11 Mon Sep 17 00:00:00 2001 From: Austin Condiff Date: Sat, 26 Oct 2024 17:36:54 -0500 Subject: [PATCH 6/6] Added delete alert to theme details sheet. --- .../ThemeSettingsThemeDetails.swift | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeDetails.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeDetails.swift index e420954f6..d6fc59657 100644 --- a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeDetails.swift +++ b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeDetails.swift @@ -22,6 +22,8 @@ struct ThemeSettingsThemeDetails: View { @State private var duplicatingTheme: Theme? + @State private var deleteConfirmationIsPresented = false + var isActive: Bool { themeModel.getThemeActive(theme) } @@ -174,10 +176,9 @@ 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) } @@ -187,7 +188,7 @@ struct ThemeSettingsThemeDetails: View { themeModel.duplicate(fileURL) } } label: { - Text("Duplicate") + Text("Duplicate...") .frame(minWidth: 56) } } @@ -247,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.") + } } }