From b3f95e36765a5cc542555ec6822c1d43d9e6e136 Mon Sep 17 00:00:00 2001 From: Austin Condiff Date: Tue, 14 May 2024 17:33:08 -0500 Subject: [PATCH 01/12] Put UI in place to configure theme tokens to be bold and or or italic from within theme details in settings --- CodeEdit.xcodeproj/project.pbxproj | 4 + .../ThemeSettingsThemeDetails.swift | 92 +++++++++++-------- .../ThemeSettingsThemeToken.swift | 49 ++++++++++ .../Settings/Views/SettingsColorPicker.swift | 19 +++- 4 files changed, 120 insertions(+), 44 deletions(-) create mode 100644 CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeToken.swift diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index ae3931dd6..503ae0c92 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -519,6 +519,7 @@ B6F0517929D9E3C900D72287 /* SourceControlGitView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F0517829D9E3C900D72287 /* SourceControlGitView.swift */; }; B6F0517B29D9E46400D72287 /* SourceControlSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F0517A29D9E46400D72287 /* SourceControlSettingsView.swift */; }; B6F0517D29D9E4B100D72287 /* TerminalSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F0517C29D9E4B100D72287 /* TerminalSettingsView.swift */; }; + B6FA3F882BF41C940023DE9C /* ThemeSettingsThemeToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6FA3F872BF41C940023DE9C /* ThemeSettingsThemeToken.swift */; }; B6FF04782B6C08AC002C2C78 /* DefaultThemes in Resources */ = {isa = PBXBuildFile; fileRef = B6FF04772B6C08AC002C2C78 /* DefaultThemes */; }; D7012EE827E757850001E1EF /* FindNavigatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7012EE727E757850001E1EF /* FindNavigatorView.swift */; }; D7211D4327E066CE008F2ED7 /* Localized+Ex.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7211D4227E066CE008F2ED7 /* Localized+Ex.swift */; }; @@ -1096,6 +1097,7 @@ B6F0517829D9E3C900D72287 /* SourceControlGitView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceControlGitView.swift; sourceTree = ""; }; B6F0517A29D9E46400D72287 /* SourceControlSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceControlSettingsView.swift; sourceTree = ""; }; B6F0517C29D9E4B100D72287 /* TerminalSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalSettingsView.swift; sourceTree = ""; }; + B6FA3F872BF41C940023DE9C /* ThemeSettingsThemeToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeSettingsThemeToken.swift; sourceTree = ""; }; B6FF04772B6C08AC002C2C78 /* DefaultThemes */ = {isa = PBXFileReference; lastKnownFileType = folder; path = DefaultThemes; sourceTree = ""; }; D7012EE727E757850001E1EF /* FindNavigatorView.swift */ = {isa = PBXFileReference; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = FindNavigatorView.swift; sourceTree = ""; tabWidth = 4; }; D7211D4227E066CE008F2ED7 /* Localized+Ex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Localized+Ex.swift"; sourceTree = ""; }; @@ -2285,6 +2287,7 @@ B6EA1FF729DB78DB001BF195 /* ThemeSettingThemeRow.swift */, B6EA1FFA29DB78F6001BF195 /* ThemeSettingsThemeDetails.swift */, B6EA1FFC29DB792C001BF195 /* ThemeSettingsColorPreview.swift */, + B6FA3F872BF41C940023DE9C /* ThemeSettingsThemeToken.swift */, ); path = ThemeSettings; sourceTree = ""; @@ -3814,6 +3817,7 @@ 58798236292E30B90085B254 /* FeedbackType.swift in Sources */, 587B9E6D29301D8F00AC7927 /* GitLabEventNote.swift in Sources */, 587B9E9129301D8F00AC7927 /* BitBucketOAuthRouter.swift in Sources */, + B6FA3F882BF41C940023DE9C /* ThemeSettingsThemeToken.swift in Sources */, B6E41C7429DD40010088F9F4 /* View+HideSidebarToggle.swift in Sources */, 611191FA2B08CC9000D4459B /* SearchIndexer.swift in Sources */, 58822532292C280D00E83CDE /* UtilityAreaViewModel.swift in Sources */, diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeDetails.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeDetails.swift index 77ebfa777..f35d268fe 100644 --- a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeDetails.swift +++ b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeDetails.swift @@ -57,46 +57,58 @@ struct ThemeSettingsThemeDetails: View { ) } Section { - SettingsColorPicker( - "Keywords", - color: $theme.editor.keywords.swiftColor - ) - SettingsColorPicker( - "Commands", - color: $theme.editor.commands.swiftColor - ) - SettingsColorPicker( - "Types", - color: $theme.editor.types.swiftColor - ) - SettingsColorPicker( - "Attributes", - color: $theme.editor.attributes.swiftColor - ) - SettingsColorPicker( - "Variables", - color: $theme.editor.variables.swiftColor - ) - SettingsColorPicker( - "Values", - color: $theme.editor.values.swiftColor - ) - SettingsColorPicker( - "Numbers", - color: $theme.editor.numbers.swiftColor - ) - SettingsColorPicker( - "Strings", - color: $theme.editor.strings.swiftColor - ) - SettingsColorPicker( - "Characters", - color: $theme.editor.characters.swiftColor - ) - SettingsColorPicker( - "Comments", - color: $theme.editor.comments.swiftColor - ) + VStack(spacing: 0) { + ThemeSettingsThemeToken( + "Keywords", + color: $theme.editor.keywords.swiftColor + ) + Divider().padding(.leading, 10) + ThemeSettingsThemeToken( + "Commands", + color: $theme.editor.commands.swiftColor + ) + Divider().padding(.leading, 10) + ThemeSettingsThemeToken( + "Types", + color: $theme.editor.types.swiftColor + ) + Divider().padding(.leading, 10) + ThemeSettingsThemeToken( + "Attributes", + color: $theme.editor.attributes.swiftColor + ) + Divider().padding(.leading, 10) + ThemeSettingsThemeToken( + "Variables", + color: $theme.editor.variables.swiftColor + ) + Divider().padding(.leading, 10) + ThemeSettingsThemeToken( + "Values", + color: $theme.editor.values.swiftColor + ) + Divider().padding(.leading, 10) + ThemeSettingsThemeToken( + "Numbers", + color: $theme.editor.numbers.swiftColor + ) + Divider().padding(.leading, 10) + ThemeSettingsThemeToken( + "Strings", + color: $theme.editor.strings.swiftColor + ) + Divider().padding(.leading, 10) + ThemeSettingsThemeToken( + "Characters", + color: $theme.editor.characters.swiftColor + ) + Divider().padding(.leading, 10) + ThemeSettingsThemeToken( + "Comments", + color: $theme.editor.comments.swiftColor + ) + } + .padding(-10) } }.formStyle(.grouped) Divider() diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeToken.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeToken.swift new file mode 100644 index 000000000..bacf996ba --- /dev/null +++ b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeToken.swift @@ -0,0 +1,49 @@ +// +// ThemeSettingsThemeToken.swift +// CodeEdit +// +// Created by Austin Condiff on 5/14/24. +// + +import SwiftUI + +struct ThemeSettingsThemeToken: View { + var label: String + @Binding var color: Color + + @State private var isHovering = false + @State private var isBold = false + @State private var isItalic = false + + init(_ label: String, color: Binding) { + self.label = label + self._color = color + } + + var body: some View { + SettingsColorPicker( + label, + color: $color + ) { + HStack(spacing: 8) { + Toggle(isOn: $isBold) { + Image(systemName: "bold") + } + .toggleStyle(.icon) + .help("Bold") + Divider() + .fixedSize() + Toggle(isOn: $isItalic) { + Image(systemName: "italic") + } + .toggleStyle(.icon) + .help("Italic") + } + .opacity(isHovering || isBold || isItalic ? 1 : 0) + } + .padding(10) + .onHover { hovering in + isHovering = hovering + } + } +} diff --git a/CodeEdit/Features/Settings/Views/SettingsColorPicker.swift b/CodeEdit/Features/Settings/Views/SettingsColorPicker.swift index 13b751f6a..453d2347d 100644 --- a/CodeEdit/Features/Settings/Views/SettingsColorPicker.swift +++ b/CodeEdit/Features/Settings/Views/SettingsColorPicker.swift @@ -7,7 +7,7 @@ import SwiftUI -struct SettingsColorPicker: View { +struct SettingsColorPicker: View where Content: View { /// Color modified elsewhere in user theme @Binding var color: Color @@ -17,17 +17,28 @@ struct SettingsColorPicker: View { @State private var selectedColor: Color private let label: String + private let content: Content? - init(_ label: String, color: Binding) { + init(_ label: String, color: Binding, @ViewBuilder content: @escaping () -> Content) { self._color = color self.label = label self._selectedColor = State(initialValue: color.wrappedValue) + self.content = content() + } + + init(_ label: String, color: Binding) where Content == EmptyView { + self.init(label, color: color) { + EmptyView() + } } var body: some View { LabeledContent(label) { - ColorPicker(selection: $selectedColor, supportsOpacity: false) { } - .labelsHidden() + HStack(spacing: 16) { + content + ColorPicker(selection: $selectedColor, supportsOpacity: false) { } + .labelsHidden() + } } .onChange(of: selectedColor) { newValue in color = newValue From a66d3c69c56459e03e3693187f29af1a28460f4c Mon Sep 17 00:00:00 2001 From: Austin Condiff Date: Mon, 17 Jun 2024 13:12:57 -0500 Subject: [PATCH 02/12] Added isBundled flag to Theme. Added ability to import, duplicate, and delete themes. --- CodeEdit.xcodeproj/project.pbxproj | 2 +- .../Pages/ThemeSettings/Models/Theme.swift | 8 ++ .../ThemeSettings/Models/ThemeModel.swift | 85 ++++++++++++++++--- .../ThemeSettings/ThemeSettingThemeRow.swift | 38 +++++++-- .../ThemeSettingsThemeDetails.swift | 41 ++++++--- .../ThemeSettingsThemeToken.swift | 45 ++++++---- .../ThemeSettings/ThemeSettingsView.swift | 12 ++- 7 files changed, 178 insertions(+), 53 deletions(-) diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index 503ae0c92..0d4d5e24e 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -690,7 +690,7 @@ 5878DA81291863F900DD95A3 /* AcknowledgementsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AcknowledgementsView.swift; sourceTree = ""; }; 5878DA832918642000DD95A3 /* ParsePackagesResolved.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParsePackagesResolved.swift; sourceTree = ""; }; 5878DA862918642F00DD95A3 /* AcknowledgementsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AcknowledgementsViewModel.swift; sourceTree = ""; }; - 5878DAA1291AE76700DD95A3 /* QuickOpenView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QuickOpenView.swift; sourceTree = ""; }; + 5878DAA1291AE76700DD95A3 /* QuickOpenView.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = QuickOpenView.swift; sourceTree = ""; wrapsLines = 1; }; 5878DAA2291AE76700DD95A3 /* QuickOpenPreviewView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QuickOpenPreviewView.swift; sourceTree = ""; }; 5878DAA3291AE76700DD95A3 /* QuickOpenViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QuickOpenViewModel.swift; sourceTree = ""; }; 5878DAA4291AE76700DD95A3 /* QuickOpenItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QuickOpenItem.swift; sourceTree = ""; }; diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/Theme.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/Theme.swift index cbb131999..a71492015 100644 --- a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/Theme.swift +++ b/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/Theme.swift @@ -39,6 +39,12 @@ struct Theme: Identifiable, Codable, Equatable, Hashable, Loopable { /// An URL for reference var distributionURL: String + /// If the theme is bundled with CodeEdit or not + var isBundled: Bool = false + + /// The URL for the theme file + var fileURL: URL? + /// The `unique name` of the theme var name: String @@ -66,6 +72,7 @@ struct Theme: Identifiable, Codable, Equatable, Hashable, Loopable { license: String, metadataDescription: String, distributionURL: String, + isBundled: Bool, name: String, displayName: String, appearance: ThemeType, @@ -75,6 +82,7 @@ struct Theme: Identifiable, Codable, Equatable, Hashable, Loopable { self.license = license self.metadataDescription = metadataDescription self.distributionURL = distributionURL + self.isBundled = isBundled self.name = name self.displayName = displayName self.appearance = appearance diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel.swift index 2c0586a54..a02296823 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``". /// @@ -199,6 +200,10 @@ final class ThemeModel: ObservableObject { } } + theme.isBundled = fileURL.path.contains(bundledThemesURL.path) + + theme.fileURL = fileURL + // add the theme to themes array self.themes.append(theme) @@ -264,20 +269,19 @@ final class ThemeModel: ObservableObject { /// /// - Parameter theme: The theme to delete func delete(_ theme: Theme) { - let url = themesURL - .appendingPathComponent(theme.name) - .appendingPathExtension("cetheme") - do { - // remove the theme from the list - try filemanager.removeItem(at: url) + if let url = theme.fileURL { + do { + // remove the theme from the list + try filemanager.removeItem(at: url) - // remove from overrides in `settings.json` - Settings.shared.preferences.theme.overrides.removeValue(forKey: theme.name) + // remove from overrides in `settings.json` + Settings.shared.preferences.theme.overrides.removeValue(forKey: theme.name) - // reload themes - try self.loadThemes() - } catch { - print(error) + // reload themes + try self.loadThemes() + } catch { + print(error) + } } } @@ -325,4 +329,61 @@ final class ThemeModel: ObservableObject { } } } + + func importTheme() { + let openPanel = NSOpenPanel() + let allowedTypes = [UTType(filenameExtension: "cetheme")!] + + openPanel.prompt = "Import" + openPanel.allowedContentTypes = allowedTypes + openPanel.canChooseFiles = true + openPanel.canChooseDirectories = false + openPanel.allowsMultipleSelection = false + + openPanel.begin { result in + if result.rawValue == NSApplication.ModalResponse.OK.rawValue { + if let url = openPanel.urls.first { + self.duplicate(url) + } + } + } + } + + func duplicate(_ url: URL) { + do { + // Construct the destination file URL + var destinationFileURL = self.themesURL.appendingPathComponent(url.lastPathComponent) + + // Check if the file already exists + var iterator = 1 + while FileManager.default.fileExists(atPath: destinationFileURL.path) { + // Extract the base filename and extension + let fileExtension = destinationFileURL.pathExtension + var fileName = destinationFileURL.deletingPathExtension().lastPathComponent + + // Remove any existing iterator + if let range = fileName.range(of: " \\d+$", options: .regularExpression) { + fileName = String(fileName[.. Void + @ObservedObject private var themeModel: ThemeModel = .shared + @State private var presentingDetails: Bool = false @State private var isHovering = false @@ -28,22 +30,40 @@ struct ThemeSettingsThemeRow: View { .font(.footnote) } .frame(maxWidth: .infinity, alignment: .leading) - Button { - presentingDetails = true - } label: { - Text("Details...") + if !active { + Button { + action(theme) + } label: { + Text("Choose") + } + .buttonStyle(.bordered) + .opacity(isHovering ? 1 : 0) } - .buttonStyle(.bordered) - .opacity(isHovering ? 1 : 0) ThemeSettingsColorPreview(theme) + Menu { + Button("Details...") { + presentingDetails = true + } + Button("Duplicate") { + if let fileURL = theme.fileURL { + themeModel.duplicate(fileURL) + } + } + Divider() + Button("Delete") { + themeModel.delete(theme) + } + .disabled(theme.isBundled) + } label: { + Image(systemName: "ellipsis.circle") + .font(.system(size: 16)) + } + .buttonStyle(.icon) } .padding(10) .onHover { hovering in isHovering = hovering } - .onTapGesture { - action(theme) - } .sheet(isPresented: $presentingDetails) { ThemeSettingsThemeDetails($theme) } diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeDetails.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeDetails.swift index f35d268fe..a9aa65088 100644 --- a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeDetails.swift +++ b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeDetails.swift @@ -11,6 +11,9 @@ struct ThemeSettingsThemeDetails: View { @Environment(\.dismiss) var dismiss + @Environment(\.colorScheme) + var colorScheme + @Binding var theme: Theme @State private var initialTheme: Theme @@ -27,8 +30,15 @@ struct ThemeSettingsThemeDetails: View { Form { Section { TextField("Name", text: $theme.displayName) + TextField("Author", text: $theme.author) + Picker("Type", selection: $theme.appearance) { + Text("Light") + .tag(Theme.ThemeType.light) + Text("Dark") + .tag(Theme.ThemeType.dark) + } } - Section { + Section("Text") { SettingsColorPicker( "Text", color: $theme.editor.text.swiftColor @@ -42,7 +52,7 @@ struct ThemeSettingsThemeDetails: View { color: $theme.editor.invisibles.swiftColor ) } - Section { + Section("Background") { SettingsColorPicker( "Background", color: $theme.editor.background.swiftColor @@ -56,59 +66,66 @@ struct ThemeSettingsThemeDetails: View { color: $theme.editor.selection.swiftColor ) } - Section { + Section("Tokens") { VStack(spacing: 0) { ThemeSettingsThemeToken( "Keywords", color: $theme.editor.keywords.swiftColor ) - Divider().padding(.leading, 10) + Divider().padding(.horizontal, 10) ThemeSettingsThemeToken( "Commands", color: $theme.editor.commands.swiftColor ) - Divider().padding(.leading, 10) + Divider().padding(.horizontal, 10) ThemeSettingsThemeToken( "Types", color: $theme.editor.types.swiftColor ) - Divider().padding(.leading, 10) + Divider().padding(.horizontal, 10) ThemeSettingsThemeToken( "Attributes", color: $theme.editor.attributes.swiftColor ) - Divider().padding(.leading, 10) + Divider().padding(.horizontal, 10) ThemeSettingsThemeToken( "Variables", color: $theme.editor.variables.swiftColor ) - Divider().padding(.leading, 10) + Divider().padding(.horizontal, 10) ThemeSettingsThemeToken( "Values", color: $theme.editor.values.swiftColor ) - Divider().padding(.leading, 10) + Divider().padding(.horizontal, 10) ThemeSettingsThemeToken( "Numbers", color: $theme.editor.numbers.swiftColor ) - Divider().padding(.leading, 10) + Divider().padding(.horizontal, 10) ThemeSettingsThemeToken( "Strings", color: $theme.editor.strings.swiftColor ) - Divider().padding(.leading, 10) + Divider().padding(.horizontal, 10) ThemeSettingsThemeToken( "Characters", color: $theme.editor.characters.swiftColor ) - Divider().padding(.leading, 10) + Divider().padding(.horizontal, 10) ThemeSettingsThemeToken( "Comments", color: $theme.editor.comments.swiftColor ) } + .background(theme.editor.background.swiftColor) .padding(-10) + .colorScheme( + theme.appearance == .dark + ? .dark + : theme.appearance == .light + ? .light : colorScheme + ) } }.formStyle(.grouped) Divider() diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeToken.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeToken.swift index bacf996ba..072720e7b 100644 --- a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeToken.swift +++ b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeToken.swift @@ -12,38 +12,49 @@ struct ThemeSettingsThemeToken: View { @Binding var color: Color @State private var isHovering = false + @State private var selectedColor: Color @State private var isBold = false @State private var isItalic = false init(_ label: String, color: Binding) { self.label = label self._color = color + self._selectedColor = State(initialValue: color.wrappedValue) } var body: some View { - SettingsColorPicker( - label, - color: $color - ) { - HStack(spacing: 8) { - Toggle(isOn: $isBold) { - Image(systemName: "bold") + LabeledContent { + HStack(spacing: 16) { + HStack(spacing: 8) { + Toggle(isOn: $isBold) { + Image(systemName: "bold") + } + .toggleStyle(.icon) + .help("Bold") + Divider() + .fixedSize() + Toggle(isOn: $isItalic) { + Image(systemName: "italic") + } + .toggleStyle(.icon) + .help("Italic") } - .toggleStyle(.icon) - .help("Bold") - Divider() - .fixedSize() - Toggle(isOn: $isItalic) { - Image(systemName: "italic") - } - .toggleStyle(.icon) - .help("Italic") + .opacity(isHovering || isBold || isItalic ? 1 : 0) + ColorPicker(selection: $selectedColor, supportsOpacity: false) { } + .labelsHidden() } - .opacity(isHovering || isBold || isItalic ? 1 : 0) + } label: { + Text(label) + .font(.system(.body, design: .monospaced, weight: isBold ? .bold : .medium)) + .foregroundStyle(color) + .italic(isItalic) } .padding(10) .onHover { hovering in isHovering = hovering } + .onChange(of: selectedColor) { newValue in + color = newValue + } } } diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsView.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsView.swift index b3e44793c..7155f5a11 100644 --- a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsView.swift +++ b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsView.swift @@ -82,7 +82,7 @@ struct ThemeSettingsView: View { } VStack(spacing: 0) { ForEach(selectedAppearance == .dark ? themeModel.darkThemes : themeModel.lightThemes) { theme in - Divider() + Divider().padding(.horizontal, 10) ThemeSettingsThemeRow( theme: $themeModel.themes[themeModel.themes.firstIndex(of: theme)!], active: getThemeActive(theme), @@ -90,7 +90,7 @@ struct ThemeSettingsView: View { ).id(theme) } ForEach(selectedAppearance == .dark ? themeModel.lightThemes : themeModel.darkThemes) { theme in - Divider() + Divider().padding(.horizontal, 10) ThemeSettingsThemeRow( theme: $themeModel.themes[themeModel.themes.firstIndex(of: theme)!], active: getThemeActive(theme), @@ -100,6 +100,14 @@ struct ThemeSettingsView: View { } } .padding(-10) + } footer: { + HStack { + Spacer() + Button("Import...") { + themeModel.importTheme() + } + } + .padding(.top, 10) } } } From 64f32b8042c8fa4b14496f333597017e4228ca96 Mon Sep 17 00:00:00 2001 From: Austin Condiff Date: Mon, 17 Jun 2024 23:39:12 -0500 Subject: [PATCH 03/12] Renaming theme file when displayName is changed. Persisting changes to theme files. --- .../ThemeSettings/Models/ThemeModel.swift | 168 ++++++++------- .../ThemeSettings/ThemeSettingThemeRow.swift | 5 +- .../ThemeSettingsThemeDetails.swift | 199 +++++++++--------- .../ThemeSettings/ThemeSettingsView.swift | 12 ++ 4 files changed, 205 insertions(+), 179 deletions(-) diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel.swift index a02296823..f263e8286 100644 --- a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel.swift +++ b/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel.swift @@ -67,23 +67,17 @@ final class ThemeModel: ObservableObject { } } + @Published var presentingDetails: Bool = false + + @Published var detailsTheme: Theme? + /// The selected appearance in the sidebar. /// - **0**: dark mode themes /// - **1**: light mode themes @Published var selectedAppearance: Int = 0 - /// The selected tab in the main section. - /// - **0**: Preview - /// - **1**: Editor - /// - **2**: Terminal - @Published var selectedTab: Int = 1 - /// An array of loaded ``Theme``. - @Published var themes: [Theme] = [] { - didSet { - saveThemes() - } - } + @Published var themes: [Theme] = [] /// The currently selected ``Theme``. @Published var selectedTheme: Theme? { @@ -244,23 +238,6 @@ final class ThemeModel: ObservableObject { } } - /// Removes all overrides of the given theme in - /// `~/Library/Application Support/CodeEdit/settings.json` - /// - /// After removing overrides, themes are reloaded - /// from `~/Library/Application Support/CodeEdit/Themes`. See ``loadThemes()`` - /// for more information. - /// - /// - Parameter theme: The theme to reset - func reset(_ theme: Theme) { - Settings.shared.preferences.theme.overrides[theme.name] = [:] - do { - try self.loadThemes() - } catch { - print(error) - } - } - /// Removes the given theme from `–/Library/Application Support/CodeEdit/themes` /// /// After removing the theme, themes are reloaded @@ -285,51 +262,6 @@ final class ThemeModel: ObservableObject { } } - /// Saves changes on theme properties to `overrides` - /// in `~/Library/Application Support/CodeEdit/settings.json`. - private func saveThemes() { - let url = themesURL - themes.forEach { theme in - do { - // load the original theme from `~/Library/Application Support/CodeEdit/Themes/` - let originalUrl = url.appendingPathComponent(theme.name).appendingPathExtension("cetheme") - let originalData = try Data(contentsOf: originalUrl) - let originalTheme = try JSONDecoder().decode(Theme.self, from: originalData) - - // get properties of the current theme as well as the original - guard let terminalColors = try theme.terminal.allProperties() as? [String: Theme.Attributes], - let editorColors = try theme.editor.allProperties() as? [String: Theme.Attributes], - let oTermColors = try originalTheme.terminal.allProperties() as? [String: Theme.Attributes], - let oEditColors = try originalTheme.editor.allProperties() as? [String: Theme.Attributes] - else { - // TODO: Throw a proper error - throw NSError() // swiftlint:disable:this discouraged_direct_init - } - - // compare the properties and if there are differences, save to overrides - // in `settings.json - var newAttr: [String: [String: Theme.Attributes]] = ["terminal": [:], "editor": [:]] - terminalColors.forEach { (key, value) in - if value != oTermColors[key] { - newAttr["terminal"]?[key] = value - } - } - - editorColors.forEach { (key, value) in - if value != oEditColors[key] { - newAttr["editor"]?[key] = value - } - } - DispatchQueue.main.async { - Settings.shared.preferences.theme.overrides[theme.name] = newAttr - } - - } catch { - print(error) - } - } - } - func importTheme() { let openPanel = NSOpenPanel() let allowedTypes = [UTType(filenameExtension: "cetheme")!] @@ -349,17 +281,70 @@ final class ThemeModel: ObservableObject { } } + func rename(to newName: String, theme: Theme) { + do { + guard let oldURL = theme.fileURL else { + throw NSError( + domain: "ThemeModel", + code: 1, + userInfo: [NSLocalizedDescriptionKey: "Theme file URL not found"] + ) + } + + var iterator = 1 + var finalName = newName + var finalURL = themesURL.appendingPathComponent(finalName).appendingPathExtension("cetheme") + + // Check for existing display names in themes + while themes.contains(where: { theme != $0 && $0.displayName == finalName }) { + finalName = "\(newName) \(iterator)" + finalURL = themesURL.appendingPathComponent(finalName).appendingPathExtension("cetheme") + iterator += 1 + } + + 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)") + } + } + func duplicate(_ url: URL) { do { // Construct the destination file URL var destinationFileURL = self.themesURL.appendingPathComponent(url.lastPathComponent) + // Extract the base filename and extension + let fileExtension = destinationFileURL.pathExtension + + var fileName = destinationFileURL.deletingPathExtension().lastPathComponent + var newFileName = fileName + // Check if the file already exists var iterator = 1 + + let isBundled = url.absoluteString.hasPrefix(bundledThemesURL?.absoluteString ?? "") + let isImporting = + !url.absoluteString.hasPrefix(bundledThemesURL?.absoluteString ?? "") + && !url.absoluteString.hasPrefix(themesURL.absoluteString) + + if isBundled { + newFileName = "\(fileName) \(iterator)" + destinationFileURL = self.themesURL + .appendingPathComponent(newFileName) + .appendingPathExtension(fileExtension) + } + while FileManager.default.fileExists(atPath: destinationFileURL.path) { - // Extract the base filename and extension - let fileExtension = destinationFileURL.pathExtension - var fileName = destinationFileURL.deletingPathExtension().lastPathComponent + fileName = destinationFileURL.deletingPathExtension().lastPathComponent // Remove any existing iterator if let range = fileName.range(of: " \\d+$", options: .regularExpression) { @@ -367,8 +352,11 @@ final class ThemeModel: ObservableObject { } // Generate a new filename with an iterator - let newFileName = "\(fileName) \(iterator)" - destinationFileURL = self.themesURL.appendingPathComponent(newFileName).appendingPathExtension(fileExtension) + newFileName = "\(fileName) \(iterator)" + destinationFileURL = self.themesURL + .appendingPathComponent(newFileName) + .appendingPathExtension(fileExtension) + iterator += 1 } @@ -377,13 +365,31 @@ final class ThemeModel: ObservableObject { try self.loadThemes() - if var newTheme = self.themes.first(where: { $0.fileURL == destinationFileURL }) { - newTheme.name = newTheme.fileURL?.lastPathComponent ?? "" - self.selectedTheme = newTheme + if var index = self.themes.firstIndex(where: { $0.fileURL == destinationFileURL }) { + self.themes[index].displayName = newFileName + self.themes[index].name = newFileName.lowercased().replacingOccurrences(of: " ", with: "-") + if isImporting != true { + self.themes[index].author = NSFullUserName() + } + self.selectedTheme = self.themes[index] + self.detailsTheme = self.themes[index] } } catch { print("Error adding theme: \(error.localizedDescription)") } } + /// Save theme to file + func save(_ theme: Theme) { + do { + if let fileURL = theme.fileURL { + let data = try JSONEncoder().encode(theme) + let json = try JSONSerialization.jsonObject(with: data) + let prettyJSON = try JSONSerialization.data(withJSONObject: json, options: [.prettyPrinted]) + try prettyJSON.write(to: fileURL, options: .atomic) + } + } catch { + print("Error saving theme: \(error.localizedDescription)") + } + } } diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingThemeRow.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingThemeRow.swift index 237e1df54..fac6ed4c5 100644 --- a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingThemeRow.swift +++ b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingThemeRow.swift @@ -42,7 +42,7 @@ struct ThemeSettingsThemeRow: View { ThemeSettingsColorPreview(theme) Menu { Button("Details...") { - presentingDetails = true + themeModel.detailsTheme = theme } Button("Duplicate") { if let fileURL = theme.fileURL { @@ -64,8 +64,5 @@ struct ThemeSettingsThemeRow: View { .onHover { hovering in isHovering = hovering } - .sheet(isPresented: $presentingDetails) { - ThemeSettingsThemeDetails($theme) - } } } diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeDetails.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeDetails.swift index a9aa65088..3d6c715da 100644 --- a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeDetails.swift +++ b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeDetails.swift @@ -16,129 +16,140 @@ struct ThemeSettingsThemeDetails: View { @Binding var theme: Theme - @State private var initialTheme: Theme + var originalTheme: Theme @StateObject private var themeModel: ThemeModel = .shared - init(_ theme: Binding) { + init(theme: Binding) { _theme = theme - _initialTheme = State(initialValue: theme.wrappedValue) + originalTheme = theme.wrappedValue } var body: some View { VStack(spacing: 0) { Form { - Section { - TextField("Name", text: $theme.displayName) - TextField("Author", text: $theme.author) - Picker("Type", selection: $theme.appearance) { - Text("Light") - .tag(Theme.ThemeType.light) - Text("Dark") - .tag(Theme.ThemeType.dark) + Text(theme.fileURL?.absoluteString ?? "") + Group { + Section { + TextField("Name", text: $theme.displayName) + TextField("Author", text: $theme.author) + Picker("Type", selection: $theme.appearance) { + Text("Light") + .tag(Theme.ThemeType.light) + Text("Dark") + .tag(Theme.ThemeType.dark) + } } - } - Section("Text") { - SettingsColorPicker( - "Text", - color: $theme.editor.text.swiftColor - ) - SettingsColorPicker( - "Cursor", - color: $theme.editor.insertionPoint.swiftColor - ) - SettingsColorPicker( - "Invisibles", - color: $theme.editor.invisibles.swiftColor - ) - } - Section("Background") { - SettingsColorPicker( - "Background", - color: $theme.editor.background.swiftColor - ) - SettingsColorPicker( - "Current Line", - color: $theme.editor.lineHighlight.swiftColor - ) - SettingsColorPicker( - "Selection", - color: $theme.editor.selection.swiftColor - ) - } - Section("Tokens") { - VStack(spacing: 0) { - ThemeSettingsThemeToken( - "Keywords", - color: $theme.editor.keywords.swiftColor - ) - Divider().padding(.horizontal, 10) - ThemeSettingsThemeToken( - "Commands", - color: $theme.editor.commands.swiftColor - ) - Divider().padding(.horizontal, 10) - ThemeSettingsThemeToken( - "Types", - color: $theme.editor.types.swiftColor - ) - Divider().padding(.horizontal, 10) - ThemeSettingsThemeToken( - "Attributes", - color: $theme.editor.attributes.swiftColor + Section("Text") { + SettingsColorPicker( + "Text", + color: $theme.editor.text.swiftColor ) - Divider().padding(.horizontal, 10) - ThemeSettingsThemeToken( - "Variables", - color: $theme.editor.variables.swiftColor + SettingsColorPicker( + "Cursor", + color: $theme.editor.insertionPoint.swiftColor ) - Divider().padding(.horizontal, 10) - ThemeSettingsThemeToken( - "Values", - color: $theme.editor.values.swiftColor + SettingsColorPicker( + "Invisibles", + color: $theme.editor.invisibles.swiftColor ) - Divider().padding(.horizontal, 10) - ThemeSettingsThemeToken( - "Numbers", - color: $theme.editor.numbers.swiftColor + } + Section("Background") { + SettingsColorPicker( + "Background", + color: $theme.editor.background.swiftColor ) - Divider().padding(.horizontal, 10) - ThemeSettingsThemeToken( - "Strings", - color: $theme.editor.strings.swiftColor + SettingsColorPicker( + "Current Line", + color: $theme.editor.lineHighlight.swiftColor ) - Divider().padding(.horizontal, 10) - ThemeSettingsThemeToken( - "Characters", - color: $theme.editor.characters.swiftColor + SettingsColorPicker( + "Selection", + color: $theme.editor.selection.swiftColor ) - Divider().padding(.horizontal, 10) - ThemeSettingsThemeToken( - "Comments", - color: $theme.editor.comments.swiftColor + } + Section("Tokens") { + VStack(spacing: 0) { + ThemeSettingsThemeToken( + "Keywords", + color: $theme.editor.keywords.swiftColor + ) + Divider().padding(.horizontal, 10) + ThemeSettingsThemeToken( + "Commands", + color: $theme.editor.commands.swiftColor + ) + Divider().padding(.horizontal, 10) + ThemeSettingsThemeToken( + "Types", + color: $theme.editor.types.swiftColor + ) + Divider().padding(.horizontal, 10) + ThemeSettingsThemeToken( + "Attributes", + color: $theme.editor.attributes.swiftColor + ) + Divider().padding(.horizontal, 10) + ThemeSettingsThemeToken( + "Variables", + color: $theme.editor.variables.swiftColor + ) + Divider().padding(.horizontal, 10) + ThemeSettingsThemeToken( + "Values", + color: $theme.editor.values.swiftColor + ) + Divider().padding(.horizontal, 10) + ThemeSettingsThemeToken( + "Numbers", + color: $theme.editor.numbers.swiftColor + ) + Divider().padding(.horizontal, 10) + ThemeSettingsThemeToken( + "Strings", + color: $theme.editor.strings.swiftColor + ) + Divider().padding(.horizontal, 10) + ThemeSettingsThemeToken( + "Characters", + color: $theme.editor.characters.swiftColor + ) + Divider().padding(.horizontal, 10) + ThemeSettingsThemeToken( + "Comments", + color: $theme.editor.comments.swiftColor + ) + } + .background(theme.editor.background.swiftColor) + .padding(-10) + .colorScheme( + theme.appearance == .dark + ? .dark + : theme.appearance == .light + ? .light : colorScheme ) } - .background(theme.editor.background.swiftColor) - .padding(-10) - .colorScheme( - theme.appearance == .dark - ? .dark - : theme.appearance == .light - ? .light : colorScheme - ) } - }.formStyle(.grouped) + .disabled(theme.isBundled) + } + .formStyle(.grouped) Divider() HStack { + Button("Duplicate...") { + if let fileURL = theme.fileURL { + themeModel.duplicate(fileURL) + } + } Spacer() Button { - theme = initialTheme + theme = originalTheme dismiss() } label: { Text("Cancel") } .buttonStyle(.bordered) Button { + themeModel.rename(to: theme.displayName, theme: theme) dismiss() } label: { Text("Done") diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsView.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsView.swift index 7155f5a11..d024f2adc 100644 --- a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsView.swift +++ b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsView.swift @@ -110,6 +110,18 @@ struct ThemeSettingsView: View { .padding(.top, 10) } } + .sheet(item: $themeModel.detailsTheme) { theme in + if 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) + } + )) + } + + } } } From 5d7861438a3f0e087765d78f987ca535aceda1b9 Mon Sep 17 00:00:00 2001 From: Austin Condiff Date: Tue, 18 Jun 2024 11:43:06 -0500 Subject: [PATCH 04/12] Changed theme details UI when adding theme, cancel deletes new theme when adding. --- CodeEdit.xcodeproj/project.pbxproj | 4 + .../Models/ThemeModel+CRUD.swift | 290 +++++++++++++++++ .../ThemeSettings/Models/ThemeModel.swift | 294 +----------------- .../ThemeSettingsThemeDetails.swift | 16 +- .../ThemeSettingsThemeToken.swift | 31 +- .../ThemeSettings/ThemeSettingsView.swift | 4 +- 6 files changed, 329 insertions(+), 310 deletions(-) create mode 100644 CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel+CRUD.swift diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index 0d4d5e24e..09c467fd5 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -440,6 +440,7 @@ B61A606129F188AB009B43F9 /* ExternalLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = B61A606029F188AB009B43F9 /* ExternalLink.swift */; }; B61A606929F4481A009B43F9 /* MonospacedFontPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = B61A606829F4481A009B43F9 /* MonospacedFontPicker.swift */; }; B61DA9DF29D929E100BF4A43 /* GeneralSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B61DA9DE29D929E100BF4A43 /* GeneralSettingsView.swift */; }; + B62423302C21EE280096668B /* ThemeModel+CRUD.swift in Sources */ = {isa = PBXBuildFile; fileRef = B624232F2C21EE280096668B /* ThemeModel+CRUD.swift */; }; B628B7932B18369800F9775A /* GitClient+Validate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B628B7922B18369800F9775A /* GitClient+Validate.swift */; }; B628B7B72B223BAD00F9775A /* FindModePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = B628B7B62B223BAD00F9775A /* FindModePicker.swift */; }; B62AEDAA2A1FCBE5009A9F52 /* AreaTabBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = B62AEDA92A1FCBE5009A9F52 /* AreaTabBar.swift */; }; @@ -1013,6 +1014,7 @@ B61A606029F188AB009B43F9 /* ExternalLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExternalLink.swift; sourceTree = ""; }; B61A606829F4481A009B43F9 /* MonospacedFontPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MonospacedFontPicker.swift; sourceTree = ""; }; B61DA9DE29D929E100BF4A43 /* GeneralSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralSettingsView.swift; sourceTree = ""; }; + B624232F2C21EE280096668B /* ThemeModel+CRUD.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ThemeModel+CRUD.swift"; sourceTree = ""; }; B628B7922B18369800F9775A /* GitClient+Validate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GitClient+Validate.swift"; sourceTree = ""; }; B628B7B62B223BAD00F9775A /* FindModePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FindModePicker.swift; sourceTree = ""; }; B62AEDA92A1FCBE5009A9F52 /* AreaTabBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AreaTabBar.swift; sourceTree = ""; }; @@ -3103,6 +3105,7 @@ children = ( 58F2EAE0292FB2B0004A9BDE /* ThemeSettings.swift */, B6EA1FE429DA33DB001BF195 /* ThemeModel.swift */, + B624232F2C21EE280096668B /* ThemeModel+CRUD.swift */, B6EA1FE629DA341D001BF195 /* Theme.swift */, ); path = Models; @@ -3571,6 +3574,7 @@ 58798219292D92370085B254 /* SearchModeModel.swift in Sources */, 6C5C891B2A3F736500A94FE1 /* FocusedValues.swift in Sources */, 611192062B08CCF600D4459B /* SearchIndexer+Add.swift in Sources */, + B62423302C21EE280096668B /* ThemeModel+CRUD.swift in Sources */, B62AEDD72A27B3D0009A9F52 /* UtilityAreaTabViewModel.swift in Sources */, 85773E1E2A3E0A1F00C5D926 /* SettingsSearchResult.swift in Sources */, B66A4E4F29C917B8004573B4 /* WelcomeWindow.swift in Sources */, diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel+CRUD.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel+CRUD.swift new file mode 100644 index 000000000..ee01c103f --- /dev/null +++ b/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel+CRUD.swift @@ -0,0 +1,290 @@ +// +// ThemeModel+CRUD.swift +// CodeEdit +// +// Created by Austin Condiff on 6/18/24. +// + +import SwiftUI +import UniformTypeIdentifiers + +extension ThemeModel { + /// Loads a theme from a given url and appends it to ``themes``. + /// - Parameter url: The URL of the theme + /// - Returns: A ``Theme`` + private func load(from url: URL) throws -> Theme? { + do { + // get the data from the provided file + let json = try Data(contentsOf: url) + // decode the json into ``Theme`` + let theme = try JSONDecoder().decode(Theme.self, from: json) + return theme + } catch { + print(error) + return nil + } + } + + /// Loads all available themes from `~/Library/Application Support/CodeEdit/Themes/` + /// + /// If no themes are available, it will create a default theme and save + /// it to the location mentioned above. + /// + /// When overrides are found in `~/Library/Application Support/CodeEdit/settings.json` + /// they are applied to the loaded themes without altering the original + /// the files in `~/Library/Application Support/CodeEdit/Themes/`. + func loadThemes() throws { // swiftlint:disable:this function_body_length + if let bundledThemesURL = bundledThemesURL { + // remove all themes from memory + themes.removeAll() + + var isDir: ObjCBool = false + + // check if a themes directory exists, otherwise create one + if !filemanager.fileExists(atPath: themesURL.path, isDirectory: &isDir) { + try filemanager.createDirectory(at: themesURL, withIntermediateDirectories: true) + } + + // get all URLs in users themes folder that end with `.cetheme` + let userDefinedThemeFilenames = try filemanager.contentsOfDirectory(atPath: themesURL.path).filter { + $0.contains(".cetheme") + } + let userDefinedThemeURLs = userDefinedThemeFilenames.map { + themesURL.appendingPathComponent($0) + } + + // get all bundled theme URLs + let bundledThemeFilenames = try filemanager.contentsOfDirectory(atPath: bundledThemesURL.path).filter { + $0.contains(".cetheme") + } + let bundledThemeURLs = bundledThemeFilenames.map { + bundledThemesURL.appendingPathComponent($0) + } + + // combine user theme URLs with bundled theme URLs + let themeURLs = userDefinedThemeURLs + bundledThemeURLs + + let prefs = Settings.shared.preferences + + // load each theme from disk and store in memory + try themeURLs.forEach { fileURL in + if var theme = try load(from: fileURL) { + + // get all properties of terminal and editor colors + guard let terminalColors = try theme.terminal.allProperties() as? [String: Theme.Attributes], + let editorColors = try theme.editor.allProperties() as? [String: Theme.Attributes] + else { + print("error") + // TODO: Throw a proper error + throw NSError() // swiftlint:disable:this discouraged_direct_init + } + + // check if there are any overrides in `settings.json` + if let overrides = prefs.theme.overrides[theme.name]?["terminal"] { + terminalColors.forEach { (key, _) in + if let attributes = overrides[key] { + theme.terminal[key] = attributes + } + } + } + + if let overrides = prefs.theme.overrides[theme.name]?["editor"] { + editorColors.forEach { (key, _) in + if let attributes = overrides[key] { + theme.editor[key] = attributes + } + } + } + + theme.isBundled = fileURL.path.contains(bundledThemesURL.path) + + theme.fileURL = fileURL + + // add the theme to themes array + self.themes.append(theme) + + // if there already is a selected theme in `settings.json` select this theme + // otherwise take the first in the list + self.selectedDarkTheme = self.darkThemes.first { + $0.name == prefs.theme.selectedDarkTheme + } ?? self.darkThemes.first + + self.selectedLightTheme = self.lightThemes.first { + $0.name == prefs.theme.selectedLightTheme + } ?? self.lightThemes.first + + // For selecting the default theme, doing it correctly on startup requires some more logic + let userSelectedTheme = self.themes.first { $0.name == prefs.theme.selectedTheme } + let systemAppearance = NSAppearance.currentDrawing().name + + if userSelectedTheme != nil { + self.selectedTheme = userSelectedTheme + } else { + if systemAppearance == .darkAqua { + self.selectedTheme = self.selectedDarkTheme + } else { + self.selectedTheme = self.selectedLightTheme + } + } + } + } + } + } + + func importTheme() { + let openPanel = NSOpenPanel() + let allowedTypes = [UTType(filenameExtension: "cetheme")!] + + openPanel.prompt = "Import" + openPanel.allowedContentTypes = allowedTypes + openPanel.canChooseFiles = true + openPanel.canChooseDirectories = false + openPanel.allowsMultipleSelection = false + + openPanel.begin { result in + if result.rawValue == NSApplication.ModalResponse.OK.rawValue { + if let url = openPanel.urls.first { + self.duplicate(url) + } + } + } + } + + func duplicate(_ url: URL) { + do { + self.isAdding = true + // Construct the destination file URL + var destinationFileURL = self.themesURL.appendingPathComponent(url.lastPathComponent) + + // Extract the base filename and extension + let fileExtension = destinationFileURL.pathExtension + + var fileName = destinationFileURL.deletingPathExtension().lastPathComponent + var newFileName = fileName + + // Check if the file already exists + var iterator = 1 + + let isBundled = url.absoluteString.hasPrefix(bundledThemesURL?.absoluteString ?? "") + let isImporting = + !url.absoluteString.hasPrefix(bundledThemesURL?.absoluteString ?? "") + && !url.absoluteString.hasPrefix(themesURL.absoluteString) + + if isBundled { + newFileName = "\(fileName) \(iterator)" + destinationFileURL = self.themesURL + .appendingPathComponent(newFileName) + .appendingPathExtension(fileExtension) + } + + while FileManager.default.fileExists(atPath: destinationFileURL.path) { + fileName = destinationFileURL.deletingPathExtension().lastPathComponent + + // Remove any existing iterator + if let range = fileName.range(of: " \\d+$", options: .regularExpression) { + fileName = String(fileName[.. Theme? { - do { - // get the data from the provided file - let json = try Data(contentsOf: url) - // decode the json into ``Theme`` - let theme = try JSONDecoder().decode(Theme.self, from: json) - return theme - } catch { - print(error) - return nil - } - } - - /// Loads all available themes from `~/Library/Application Support/CodeEdit/Themes/` - /// - /// If no themes are available, it will create a default theme and save - /// it to the location mentioned above. - /// - /// When overrides are found in `~/Library/Application Support/CodeEdit/settings.json` - /// they are applied to the loaded themes without altering the original - /// the files in `~/Library/Application Support/CodeEdit/Themes/`. - func loadThemes() throws { // swiftlint:disable:this function_body_length - if let bundledThemesURL = bundledThemesURL { - // remove all themes from memory - themes.removeAll() - - var isDir: ObjCBool = false - - // check if a themes directory exists, otherwise create one - if !filemanager.fileExists(atPath: themesURL.path, isDirectory: &isDir) { - try filemanager.createDirectory(at: themesURL, withIntermediateDirectories: true) - } - - // get all URLs in users themes folder that end with `.cetheme` - let userDefinedThemeFilenames = try filemanager.contentsOfDirectory(atPath: themesURL.path).filter { - $0.contains(".cetheme") - } - let userDefinedThemeURLs = userDefinedThemeFilenames.map { - themesURL.appendingPathComponent($0) - } - - // get all bundled theme URLs - let bundledThemeFilenames = try filemanager.contentsOfDirectory(atPath: bundledThemesURL.path).filter { - $0.contains(".cetheme") - } - let bundledThemeURLs = bundledThemeFilenames.map { - bundledThemesURL.appendingPathComponent($0) - } - - // combine user theme URLs with bundled theme URLs - let themeURLs = userDefinedThemeURLs + bundledThemeURLs - - let prefs = Settings.shared.preferences - - // load each theme from disk and store in memory - try themeURLs.forEach { fileURL in - if var theme = try load(from: fileURL) { - - // get all properties of terminal and editor colors - guard let terminalColors = try theme.terminal.allProperties() as? [String: Theme.Attributes], - let editorColors = try theme.editor.allProperties() as? [String: Theme.Attributes] - else { - print("error") - // TODO: Throw a proper error - throw NSError() // swiftlint:disable:this discouraged_direct_init - } - - // check if there are any overrides in `settings.json` - if let overrides = prefs.theme.overrides[theme.name]?["terminal"] { - terminalColors.forEach { (key, _) in - if let attributes = overrides[key] { - theme.terminal[key] = attributes - } - } - } - - if let overrides = prefs.theme.overrides[theme.name]?["editor"] { - editorColors.forEach { (key, _) in - if let attributes = overrides[key] { - theme.editor[key] = attributes - } - } - } - - theme.isBundled = fileURL.path.contains(bundledThemesURL.path) - - theme.fileURL = fileURL - - // add the theme to themes array - self.themes.append(theme) - - // if there already is a selected theme in `settings.json` select this theme - // otherwise take the first in the list - self.selectedDarkTheme = self.darkThemes.first { - $0.name == prefs.theme.selectedDarkTheme - } ?? self.darkThemes.first - - self.selectedLightTheme = self.lightThemes.first { - $0.name == prefs.theme.selectedLightTheme - } ?? self.lightThemes.first - - // For selecting the default theme, doing it correctly on startup requires some more logic - let userSelectedTheme = self.themes.first { $0.name == prefs.theme.selectedTheme } - let systemAppearance = NSAppearance.currentDrawing().name - - if userSelectedTheme != nil { - self.selectedTheme = userSelectedTheme - } else { - if systemAppearance == .darkAqua { - self.selectedTheme = self.selectedDarkTheme - } else { - self.selectedTheme = self.selectedLightTheme - } - } - } - } - } - } - /// This function stores 'dark' and 'light' themes into `ThemePreferences` if user happens to select a theme - private func updateAppearanceTheme() { + func updateAppearanceTheme() { if self.selectedTheme?.appearance == .dark { self.selectedDarkTheme = self.selectedTheme } else if self.selectedTheme?.appearance == .light { @@ -238,158 +123,11 @@ final class ThemeModel: ObservableObject { } } - /// Removes the given theme from `–/Library/Application Support/CodeEdit/themes` - /// - /// After removing the theme, themes are reloaded - /// from `~/Library/Application Support/CodeEdit/Themes`. See ``loadThemes()`` - /// for more information. - /// - /// - Parameter theme: The theme to delete - func delete(_ theme: Theme) { - if let url = theme.fileURL { - do { - // remove the theme from the list - try filemanager.removeItem(at: url) - - // remove from overrides in `settings.json` - Settings.shared.preferences.theme.overrides.removeValue(forKey: theme.name) - - // reload themes - try self.loadThemes() - } catch { - print(error) - } - } - } - - func importTheme() { - let openPanel = NSOpenPanel() - let allowedTypes = [UTType(filenameExtension: "cetheme")!] - - openPanel.prompt = "Import" - openPanel.allowedContentTypes = allowedTypes - openPanel.canChooseFiles = true - openPanel.canChooseDirectories = false - openPanel.allowsMultipleSelection = false - - openPanel.begin { result in - if result.rawValue == NSApplication.ModalResponse.OK.rawValue { - if let url = openPanel.urls.first { - self.duplicate(url) - } - } - } - } - - func rename(to newName: String, theme: Theme) { - do { - guard let oldURL = theme.fileURL else { - throw NSError( - domain: "ThemeModel", - code: 1, - userInfo: [NSLocalizedDescriptionKey: "Theme file URL not found"] - ) - } - - var iterator = 1 - var finalName = newName - var finalURL = themesURL.appendingPathComponent(finalName).appendingPathExtension("cetheme") - - // Check for existing display names in themes - while themes.contains(where: { theme != $0 && $0.displayName == finalName }) { - finalName = "\(newName) \(iterator)" - finalURL = themesURL.appendingPathComponent(finalName).appendingPathExtension("cetheme") - iterator += 1 - } - - 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)") - } - } - - func duplicate(_ url: URL) { - do { - // Construct the destination file URL - var destinationFileURL = self.themesURL.appendingPathComponent(url.lastPathComponent) - - // Extract the base filename and extension - let fileExtension = destinationFileURL.pathExtension - - var fileName = destinationFileURL.deletingPathExtension().lastPathComponent - var newFileName = fileName - - // Check if the file already exists - var iterator = 1 - - let isBundled = url.absoluteString.hasPrefix(bundledThemesURL?.absoluteString ?? "") - let isImporting = - !url.absoluteString.hasPrefix(bundledThemesURL?.absoluteString ?? "") - && !url.absoluteString.hasPrefix(themesURL.absoluteString) - - if isBundled { - newFileName = "\(fileName) \(iterator)" - destinationFileURL = self.themesURL - .appendingPathComponent(newFileName) - .appendingPathExtension(fileExtension) - } - - while FileManager.default.fileExists(atPath: destinationFileURL.path) { - fileName = destinationFileURL.deletingPathExtension().lastPathComponent - - // Remove any existing iterator - if let range = fileName.range(of: " \\d+$", options: .regularExpression) { - fileName = String(fileName[..) { self.label = label @@ -24,35 +22,14 @@ struct ThemeSettingsThemeToken: View { var body: some View { LabeledContent { - HStack(spacing: 16) { - HStack(spacing: 8) { - Toggle(isOn: $isBold) { - Image(systemName: "bold") - } - .toggleStyle(.icon) - .help("Bold") - Divider() - .fixedSize() - Toggle(isOn: $isItalic) { - Image(systemName: "italic") - } - .toggleStyle(.icon) - .help("Italic") - } - .opacity(isHovering || isBold || isItalic ? 1 : 0) - ColorPicker(selection: $selectedColor, supportsOpacity: false) { } - .labelsHidden() - } + ColorPicker(selection: $selectedColor, supportsOpacity: false) { } + .labelsHidden() } label: { Text(label) - .font(.system(.body, design: .monospaced, weight: isBold ? .bold : .medium)) + .font(.system(.body, design: .monospaced)) .foregroundStyle(color) - .italic(isItalic) } .padding(10) - .onHover { hovering in - isHovering = hovering - } .onChange(of: selectedColor) { newValue in color = newValue } diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsView.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsView.swift index d024f2adc..fbc2ed8fb 100644 --- a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsView.swift +++ b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsView.swift @@ -111,7 +111,9 @@ struct ThemeSettingsView: View { } } .sheet(item: $themeModel.detailsTheme) { theme in - if let index = themeModel.themes.firstIndex(where: { $0.fileURL?.absoluteString == theme.fileURL?.absoluteString }) { + if let index = themeModel.themes.firstIndex(where: { + $0.fileURL?.absoluteString == theme.fileURL?.absoluteString + }) { ThemeSettingsThemeDetails(theme: Binding( get: { themeModel.themes[index] }, set: { newValue in From f8addd606fe9eb5cb411a498d9359084372061d4 Mon Sep 17 00:00:00 2001 From: Austin Condiff Date: Tue, 18 Jun 2024 13:01:20 -0500 Subject: [PATCH 05/12] Removed dev text in theme details --- .../Pages/ThemeSettings/ThemeSettingsThemeDetails.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeDetails.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeDetails.swift index 3cb508b1c..bcb94869d 100644 --- a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeDetails.swift +++ b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeDetails.swift @@ -28,8 +28,6 @@ struct ThemeSettingsThemeDetails: View { var body: some View { VStack(spacing: 0) { Form { - Text(theme.fileURL?.absoluteString ?? "") - Text(originalTheme.author) Group { Section { TextField("Name", text: $theme.displayName) From 9f45dced1b63cf9979bcd4d461a2aae4ae044029 Mon Sep 17 00:00:00 2001 From: Austin Condiff Date: Tue, 18 Jun 2024 13:43:04 -0500 Subject: [PATCH 06/12] Sorting the keys in the JSON output when modifying a theme --- .../Settings/Pages/ThemeSettings/Models/ThemeModel+CRUD.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel+CRUD.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel+CRUD.swift index ee01c103f..9713098d8 100644 --- a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel+CRUD.swift +++ b/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel+CRUD.swift @@ -254,7 +254,9 @@ extension ThemeModel { func save(_ theme: Theme) { do { if let fileURL = theme.fileURL { - let data = try JSONEncoder().encode(theme) + let encoder = JSONEncoder() + encoder.outputFormatting = [.sortedKeys] + let data = try encoder.encode(theme) let json = try JSONSerialization.jsonObject(with: data) let prettyJSON = try JSONSerialization.data(withJSONObject: json, options: [.prettyPrinted]) try prettyJSON.write(to: fileURL, options: .atomic) From 7598adb4e06ac95dcc71ac44ef1eaf1cfe01d817 Mon Sep 17 00:00:00 2001 From: Austin Condiff Date: Tue, 18 Jun 2024 15:01:17 -0500 Subject: [PATCH 07/12] Added delete button to theme details sheet --- .../ThemeSettingsThemeDetails.swift | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeDetails.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeDetails.swift index bcb94869d..642852709 100644 --- a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeDetails.swift +++ b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeDetails.swift @@ -135,10 +135,23 @@ struct ThemeSettingsThemeDetails: View { Divider() HStack { if !themeModel.isAdding { - Button("Duplicate...") { + Button { if let fileURL = theme.fileURL { themeModel.duplicate(fileURL) } + } label: { + Text("Duplicate") + .frame(minWidth: 56) + } + if !theme.isBundled { + Button(role: .destructive) { + themeModel.delete(theme) + dismiss() + } label: { + Text("Delete") + .foregroundStyle(.red) + .frame(minWidth: 56) + } } } Spacer() @@ -152,6 +165,7 @@ struct ThemeSettingsThemeDetails: View { dismiss() } label: { Text("Cancel") + .frame(minWidth: 56) } .buttonStyle(.bordered) Button { @@ -159,6 +173,7 @@ struct ThemeSettingsThemeDetails: View { dismiss() } label: { Text("Done") + .frame(minWidth: 56) } .buttonStyle(.borderedProminent) } From 1416830b8b299135369476105d21d5a7be1b4ffc Mon Sep 17 00:00:00 2001 From: Austin Condiff Date: Tue, 18 Jun 2024 20:57:47 -0500 Subject: [PATCH 08/12] PR Issues --- .../Pages/ThemeSettings/Models/ThemeModel+CRUD.swift | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel+CRUD.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel+CRUD.swift index 9713098d8..740cdf3fd 100644 --- a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel+CRUD.swift +++ b/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel+CRUD.swift @@ -224,9 +224,9 @@ extension ThemeModel { ) } - var iterator = 1 var finalName = newName var finalURL = themesURL.appendingPathComponent(finalName).appendingPathExtension("cetheme") + var iterator = 1 // Check for existing display names in themes while themes.contains(where: { theme != $0 && $0.displayName == finalName }) { @@ -276,13 +276,10 @@ extension ThemeModel { func delete(_ theme: Theme) { if let url = theme.fileURL { do { - // remove the theme from the list try filemanager.removeItem(at: url) - // remove from overrides in `settings.json` Settings.shared.preferences.theme.overrides.removeValue(forKey: theme.name) - // reload themes try self.loadThemes() } catch { print(error) From 450b1cccd2431bea99b1c0f4fb4e72522bfe97ce Mon Sep 17 00:00:00 2001 From: Austin Condiff Date: Tue, 18 Jun 2024 21:04:49 -0500 Subject: [PATCH 09/12] PR Issues --- .../Settings/Pages/ThemeSettings/Models/ThemeModel+CRUD.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel+CRUD.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel+CRUD.swift index 740cdf3fd..83b7ecdd8 100644 --- a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel+CRUD.swift +++ b/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel+CRUD.swift @@ -162,7 +162,6 @@ extension ThemeModel { var fileName = destinationFileURL.deletingPathExtension().lastPathComponent var newFileName = fileName - // Check if the file already exists var iterator = 1 let isBundled = url.absoluteString.hasPrefix(bundledThemesURL?.absoluteString ?? "") @@ -177,6 +176,7 @@ extension ThemeModel { .appendingPathExtension(fileExtension) } + // Check if the file already exists while FileManager.default.fileExists(atPath: destinationFileURL.path) { fileName = destinationFileURL.deletingPathExtension().lastPathComponent From f88a81a0fd3e2922ac5cab3316ab224e8a3ed773 Mon Sep 17 00:00:00 2001 From: Austin Condiff Date: Sun, 23 Jun 2024 22:26:47 -0500 Subject: [PATCH 10/12] Moved view functions to ThemeModel --- .../Models/ThemeModel+CRUD.swift | 5 +- .../ThemeSettings/Models/ThemeModel.swift | 59 ++++++++++++---- .../ThemeSettings/ThemeSettingThemeRow.swift | 3 +- .../ThemeSettingsThemeDetails.swift | 67 +++++++++++++------ .../ThemeSettings/ThemeSettingsView.swift | 65 +++++------------- 5 files changed, 114 insertions(+), 85 deletions(-) diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel+CRUD.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel+CRUD.swift index 83b7ecdd8..6eb627f08 100644 --- a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel+CRUD.swift +++ b/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel+CRUD.swift @@ -202,11 +202,14 @@ extension ThemeModel { if var index = self.themes.firstIndex(where: { $0.fileURL == destinationFileURL }) { self.themes[index].displayName = newFileName self.themes[index].name = newFileName.lowercased().replacingOccurrences(of: " ", with: "-") + if isImporting != true { self.themes[index].author = NSFullUserName() self.save(self.themes[index]) } - self.selectedTheme = self.themes[index] + + activateTheme(self.themes[index]) + self.detailsTheme = self.themes[index] } } catch { diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel.swift index 4b99c2a16..aed8d2f56 100644 --- a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel.swift +++ b/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel.swift @@ -17,6 +17,12 @@ import SwiftUI final class ThemeModel: ObservableObject { static let shared: ThemeModel = .init() + @Environment(\.colorScheme) + var colorScheme + + @AppSettings(\.theme) + var settings + /// Default instance of the `FileManager` let filemanager = FileManager.default @@ -70,18 +76,7 @@ final class ThemeModel: ObservableObject { @Published var isAdding: Bool = false - @Published var detailsTheme: Theme? { - didSet { - if detailsTheme == nil { - isAdding = false - } - } - } - - /// The selected appearance in the sidebar. - /// - **0**: dark mode themes - /// - **1**: light mode themes - @Published var selectedAppearance: Int = 0 + @Published var detailsTheme: Theme? /// An array of loaded ``Theme``. @Published var themes: [Theme] = [] @@ -130,4 +125,44 @@ final class ThemeModel: ObservableObject { self.save(self.themes[index]) } } + + @Published var selectedAppearance: ThemeSettingsAppearances = .dark + + enum ThemeSettingsAppearances: String, CaseIterable { + case light = "Light Appearance" + case dark = "Dark Appearance" + } + + func getThemeActive (_ theme: Theme) -> Bool { + if settings.matchAppearance { + return selectedAppearance == .dark + ? selectedDarkTheme == theme + : selectedAppearance == .light + ? selectedLightTheme == theme + : selectedTheme == theme + } + return selectedTheme == theme + } + + 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 + } + } + } } diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingThemeRow.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingThemeRow.swift index fac6ed4c5..54a2f3217 100644 --- a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingThemeRow.swift +++ b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingThemeRow.swift @@ -10,7 +10,6 @@ import SwiftUI struct ThemeSettingsThemeRow: View { @Binding var theme: Theme var active: Bool - var action: (Theme) -> Void @ObservedObject private var themeModel: ThemeModel = .shared @@ -32,7 +31,7 @@ struct ThemeSettingsThemeRow: View { .frame(maxWidth: .infinity, alignment: .leading) if !active { Button { - action(theme) + themeModel.activateTheme(theme) } label: { Text("Choose") } diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeDetails.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeDetails.swift index 642852709..9ec3bb5f4 100644 --- a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeDetails.swift +++ b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeDetails.swift @@ -134,7 +134,27 @@ struct ThemeSettingsThemeDetails: View { .formStyle(.grouped) Divider() HStack { - if !themeModel.isAdding { + if theme.isBundled { + HStack { + Image(systemName: "exclamationmark.triangle.fill") + .font(.body) + .foregroundStyle(Color.yellow) + Text("Duplicate this theme to make changes.") + .font(.subheadline) + .lineLimit(2) + } + .help("Bundled themes must be duplicated to make changes.") + .accessibilityElement(children: .combine) + .accessibilityLabel("Warning: Duplicate this theme to make changes.") + } else if !themeModel.isAdding { + Button(role: .destructive) { + themeModel.delete(theme) + dismiss() + } label: { + Text("Delete") + .foregroundStyle(.red) + .frame(minWidth: 56) + } Button { if let fileURL = theme.fileURL { themeModel.duplicate(fileURL) @@ -143,33 +163,36 @@ struct ThemeSettingsThemeDetails: View { Text("Duplicate") .frame(minWidth: 56) } - if !theme.isBundled { - Button(role: .destructive) { - themeModel.delete(theme) - dismiss() - } label: { - Text("Delete") - .foregroundStyle(.red) - .frame(minWidth: 56) - } - } } Spacer() - Button { - if themeModel.isAdding { - themeModel.delete(theme) - } else { - themeModel.cancelDetails(theme) + if !themeModel.isAdding && theme.isBundled { + Button { + if let fileURL = theme.fileURL { + themeModel.duplicate(fileURL) + } + } label: { + Text("Duplicate") + .frame(minWidth: 56) } + } else { + Button { + if themeModel.isAdding { + themeModel.delete(theme) + } else { + themeModel.cancelDetails(theme) + } - dismiss() - } label: { - Text("Cancel") - .frame(minWidth: 56) + dismiss() + } label: { + Text("Cancel") + .frame(minWidth: 56) + } + .buttonStyle(.bordered) } - .buttonStyle(.bordered) Button { - themeModel.rename(to: theme.displayName, theme: theme) + if !theme.isBundled { + themeModel.rename(to: theme.displayName, theme: theme) + } dismiss() } label: { Text("Done") diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsView.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsView.swift index fbc2ed8fb..4e471b614 100644 --- a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsView.swift +++ b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsView.swift @@ -18,45 +18,6 @@ struct ThemeSettingsView: View { var useDarkTerminalAppearance @State private var listView: Bool = false - @State private var selectedAppearance: ThemeSettingsAppearances = .dark - - enum ThemeSettingsAppearances: String, CaseIterable { - case light = "Light Appearance" - case dark = "Dark Appearance" - } - - func getThemeActive (_ theme: Theme) -> Bool { - if settings.matchAppearance { - return selectedAppearance == .dark - ? themeModel.selectedDarkTheme == theme - : selectedAppearance == .light - ? themeModel.selectedLightTheme == theme - : themeModel.selectedTheme == theme - } - return themeModel.selectedTheme == theme - } - - func activateTheme (_ theme: Theme) { - if settings.matchAppearance { - if selectedAppearance == .dark { - themeModel.selectedDarkTheme = theme - } else if selectedAppearance == .light { - themeModel.selectedLightTheme = theme - } - if (selectedAppearance == .dark && colorScheme == .dark) - || (selectedAppearance == .light && colorScheme == .light) { - themeModel.selectedTheme = theme - } - } else { - themeModel.selectedTheme = theme - if colorScheme == .light { - themeModel.selectedLightTheme = theme - } - if colorScheme == .dark { - themeModel.selectedDarkTheme = theme - } - } - } var body: some View { SettingsForm { @@ -70,8 +31,8 @@ struct ThemeSettingsView: View { Section { VStack(spacing: 0) { if settings.matchAppearance { - Picker("", selection: $selectedAppearance) { - ForEach(ThemeSettingsAppearances.allCases, id: \.self) { tab in + Picker("", selection: $themeModel.selectedAppearance) { + ForEach(ThemeModel.ThemeSettingsAppearances.allCases, id: \.self) { tab in Text(tab.rawValue) .tag(tab) } @@ -81,20 +42,26 @@ struct ThemeSettingsView: View { .padding(10) } VStack(spacing: 0) { - ForEach(selectedAppearance == .dark ? themeModel.darkThemes : themeModel.lightThemes) { theme in + ForEach( + themeModel.selectedAppearance == .dark + ? themeModel.darkThemes + : themeModel.lightThemes + ) { theme in Divider().padding(.horizontal, 10) ThemeSettingsThemeRow( theme: $themeModel.themes[themeModel.themes.firstIndex(of: theme)!], - active: getThemeActive(theme), - action: activateTheme + active: themeModel.getThemeActive(theme) ).id(theme) } - ForEach(selectedAppearance == .dark ? themeModel.lightThemes : themeModel.darkThemes) { theme in + ForEach( + themeModel.selectedAppearance == .dark + ? themeModel.lightThemes + : themeModel.darkThemes + ) { theme in Divider().padding(.horizontal, 10) ThemeSettingsThemeRow( theme: $themeModel.themes[themeModel.themes.firstIndex(of: theme)!], - active: getThemeActive(theme), - action: activateTheme + active: themeModel.getThemeActive(theme) ).id(theme) } } @@ -110,7 +77,9 @@ struct ThemeSettingsView: View { .padding(.top, 10) } } - .sheet(item: $themeModel.detailsTheme) { theme in + .sheet(item: $themeModel.detailsTheme) { + themeModel.isAdding = false + } content: { theme in if let index = themeModel.themes.firstIndex(where: { $0.fileURL?.absoluteString == theme.fileURL?.absoluteString }) { From 225521a94b16df76f01c271b98117795b97dd0e2 Mon Sep 17 00:00:00 2001 From: Austin Condiff Date: Mon, 24 Jun 2024 15:02:40 -0500 Subject: [PATCH 11/12] Storing and updating colorScheme in ThemeModel because otherwise it would always be set to light --- .../Settings/Pages/ThemeSettings/Models/ThemeModel.swift | 6 +++--- CodeEdit/WorkspaceView.swift | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel.swift index aed8d2f56..0fcf1f43e 100644 --- a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel.swift +++ b/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel.swift @@ -17,9 +17,6 @@ import SwiftUI final class ThemeModel: ObservableObject { static let shared: ThemeModel = .init() - @Environment(\.colorScheme) - var colorScheme - @AppSettings(\.theme) var settings @@ -50,6 +47,9 @@ final class ThemeModel: ObservableObject { baseURL.appendingPathComponent("settings.json", isDirectory: true) } + /// System color scheme + @Published var colorScheme: ColorScheme = .light + /// Selected 'light' theme /// Used for auto-switching theme to match macOS system appearance @Published var selectedLightTheme: Theme? { diff --git a/CodeEdit/WorkspaceView.swift b/CodeEdit/WorkspaceView.swift index b2649facc..8c4dcb35d 100644 --- a/CodeEdit/WorkspaceView.swift +++ b/CodeEdit/WorkspaceView.swift @@ -69,7 +69,11 @@ struct WorkspaceView: View { focusedEditor = newValue } } + .task { + themeModel.colorScheme = colorScheme + } .onChange(of: colorScheme) { newValue in + themeModel.colorScheme = newValue if matchAppearance { themeModel.selectedTheme = newValue == .dark ? themeModel.selectedDarkTheme From e85a8efce8c9b956d80fc79b4874461b5d901e23 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Wed, 26 Jun 2024 10:01:45 -0500 Subject: [PATCH 12/12] Update Theme While Editing, Warnings --- .../Features/Editor/Views/CodeFileView.swift | 28 ++++++++----------- .../Models/ThemeModel+CRUD.swift | 2 +- .../ThemeSettings/Models/ThemeModel.swift | 12 ++++++-- .../ThemeSettings/ThemeSettingsView.swift | 3 ++ 4 files changed, 25 insertions(+), 20 deletions(-) diff --git a/CodeEdit/Features/Editor/Views/CodeFileView.swift b/CodeEdit/Features/Editor/Views/CodeFileView.swift index 6cd441d62..3f3003d58 100644 --- a/CodeEdit/Features/Editor/Views/CodeFileView.swift +++ b/CodeEdit/Features/Editor/Views/CodeFileView.swift @@ -46,7 +46,7 @@ struct CodeFileView: View { @EnvironmentObject private var editorManager: EditorManager - @StateObject private var themeModel: ThemeModel = .shared + @ObservedObject private var themeModel: ThemeModel = .shared private var cancellables = Set() @@ -77,7 +77,9 @@ struct CodeFileView: View { codeFile.undoManager = self.undoManager.manager } - @State private var selectedTheme = ThemeModel.shared.selectedTheme ?? ThemeModel.shared.themes.first! + private var currentTheme: Theme { + themeModel.selectedTheme ?? themeModel.themes.first! + } @State private var font: NSFont = Settings[\.textEditing].font.current @@ -107,7 +109,7 @@ struct CodeFileView: View { CodeEditSourceEditor( codeFile.content ?? NSTextStorage(), language: getLanguage(), - theme: selectedTheme.editor.editorTheme, + theme: currentTheme.editor.editorTheme, font: font, tabWidth: codeFile.defaultTabWidth ?? defaultTabWidth, indentOption: (codeFile.indentOption ?? indentOption).textViewOption(), @@ -131,17 +133,9 @@ struct CodeFileView: View { EffectView(.contentBackground) } } - .colorScheme( - selectedTheme.appearance == .dark - ? .dark - : .light - ) + .colorScheme(currentTheme.appearance == .dark ? .dark : .light) // minHeight zero fixes a bug where the app would freeze if the contents of the file are empty. .frame(minHeight: .zero, maxHeight: .infinity) - .onChange(of: themeModel.selectedTheme) { newValue in - guard let theme = newValue else { return } - self.selectedTheme = theme - } .onChange(of: settingsFont) { newFontSetting in font = newFontSetting.current } @@ -162,10 +156,12 @@ struct CodeFileView: View { } private func getBracketPairHighlight() -> BracketPairHighlight? { - let theme = ThemeModel.shared.selectedTheme ?? ThemeModel.shared.themes.first! - let color = Settings[\.textEditing].bracketHighlight.useCustomColor - ? Settings[\.textEditing].bracketHighlight.color.nsColor - : theme.editor.text.nsColor.withAlphaComponent(0.8) + let color = if Settings[\.textEditing].bracketHighlight.useCustomColor { + Settings[\.textEditing].bracketHighlight.color.nsColor + } else { + currentTheme.editor.text.nsColor.withAlphaComponent(0.8) + } + switch Settings[\.textEditing].bracketHighlight.highlightType { case .disabled: return nil diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel+CRUD.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel+CRUD.swift index 6eb627f08..74090f65b 100644 --- a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel+CRUD.swift +++ b/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel+CRUD.swift @@ -199,7 +199,7 @@ extension ThemeModel { try self.loadThemes() - if var index = self.themes.firstIndex(where: { $0.fileURL == destinationFileURL }) { + if let index = self.themes.firstIndex(where: { $0.fileURL == destinationFileURL }) { self.themes[index].displayName = newFileName self.themes[index].name = newFileName.lowercased().replacingOccurrences(of: " ", with: "-") diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel.swift index 0fcf1f43e..c3663f3e6 100644 --- a/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel.swift +++ b/CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel.swift @@ -126,14 +126,17 @@ final class ThemeModel: ObservableObject { } } - @Published var selectedAppearance: ThemeSettingsAppearances = .dark + /// Initialize to the app's current appearance. + @Published var selectedAppearance: ThemeSettingsAppearances = { + NSApp.effectiveAppearance.name == .darkAqua ? .dark : .light + }() enum ThemeSettingsAppearances: String, CaseIterable { case light = "Light Appearance" case dark = "Dark Appearance" } - func getThemeActive (_ theme: Theme) -> Bool { + func getThemeActive(_ theme: Theme) -> Bool { if settings.matchAppearance { return selectedAppearance == .dark ? selectedDarkTheme == theme @@ -144,7 +147,10 @@ final class ThemeModel: ObservableObject { return selectedTheme == theme } - func activateTheme (_ theme: Theme) { + /// Activates the current theme, setting ``selectedTheme`` and ``selectedLightTheme``/``selectedDarkTheme`` as + /// necessary. + /// - Parameter theme: The theme to activate. + func activateTheme(_ theme: Theme) { if settings.matchAppearance { if selectedAppearance == .dark { selectedDarkTheme = theme diff --git a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsView.swift b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsView.swift index 4e471b614..3f327b43e 100644 --- a/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsView.swift +++ b/CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsView.swift @@ -88,6 +88,9 @@ struct ThemeSettingsView: View { set: { newValue in themeModel.themes[index] = newValue themeModel.save(newValue) + if settings.selectedTheme == theme.name { + themeModel.activateTheme(newValue) + } } )) }