Skip to content

Commit ed69437

Browse files
committed
Added isBundled flag to Theme. Added ability to import, duplicate, and delete themes.
1 parent 1e44e5e commit ed69437

File tree

7 files changed

+178
-53
lines changed

7 files changed

+178
-53
lines changed

CodeEdit.xcodeproj/project.pbxproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -662,7 +662,7 @@
662662
5878DA81291863F900DD95A3 /* AcknowledgementsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AcknowledgementsView.swift; sourceTree = "<group>"; };
663663
5878DA832918642000DD95A3 /* ParsePackagesResolved.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParsePackagesResolved.swift; sourceTree = "<group>"; };
664664
5878DA862918642F00DD95A3 /* AcknowledgementsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AcknowledgementsViewModel.swift; sourceTree = "<group>"; };
665-
5878DAA1291AE76700DD95A3 /* QuickOpenView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QuickOpenView.swift; sourceTree = "<group>"; };
665+
5878DAA1291AE76700DD95A3 /* QuickOpenView.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = QuickOpenView.swift; sourceTree = "<group>"; wrapsLines = 1; };
666666
5878DAA2291AE76700DD95A3 /* QuickOpenPreviewView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QuickOpenPreviewView.swift; sourceTree = "<group>"; };
667667
5878DAA3291AE76700DD95A3 /* QuickOpenViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QuickOpenViewModel.swift; sourceTree = "<group>"; };
668668
5878DAA4291AE76700DD95A3 /* QuickOpenItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QuickOpenItem.swift; sourceTree = "<group>"; };

CodeEdit/Features/Settings/Pages/ThemeSettings/Models/Theme.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ struct Theme: Identifiable, Codable, Equatable, Hashable, Loopable {
3939
/// An URL for reference
4040
var distributionURL: String
4141

42+
/// If the theme is bundled with CodeEdit or not
43+
var isBundled: Bool = false
44+
45+
/// The URL for the theme file
46+
var fileURL: URL?
47+
4248
/// The `unique name` of the theme
4349
var name: String
4450

@@ -66,6 +72,7 @@ struct Theme: Identifiable, Codable, Equatable, Hashable, Loopable {
6672
license: String,
6773
metadataDescription: String,
6874
distributionURL: String,
75+
isBundled: Bool,
6976
name: String,
7077
displayName: String,
7178
appearance: ThemeType,
@@ -75,6 +82,7 @@ struct Theme: Identifiable, Codable, Equatable, Hashable, Loopable {
7582
self.license = license
7683
self.metadataDescription = metadataDescription
7784
self.distributionURL = distributionURL
85+
self.isBundled = isBundled
7886
self.name = name
7987
self.displayName = displayName
8088
self.appearance = appearance

CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel.swift

Lines changed: 73 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
//
77

88
import SwiftUI
9+
import UniformTypeIdentifiers
910

1011
/// The Theme View Model. Accessible via the singleton "``ThemeModel/shared``".
1112
///
@@ -199,6 +200,10 @@ final class ThemeModel: ObservableObject {
199200
}
200201
}
201202

203+
theme.isBundled = fileURL.path.contains(bundledThemesURL.path)
204+
205+
theme.fileURL = fileURL
206+
202207
// add the theme to themes array
203208
self.themes.append(theme)
204209

@@ -264,20 +269,19 @@ final class ThemeModel: ObservableObject {
264269
///
265270
/// - Parameter theme: The theme to delete
266271
func delete(_ theme: Theme) {
267-
let url = themesURL
268-
.appendingPathComponent(theme.name)
269-
.appendingPathExtension("cetheme")
270-
do {
271-
// remove the theme from the list
272-
try filemanager.removeItem(at: url)
272+
if let url = theme.fileURL {
273+
do {
274+
// remove the theme from the list
275+
try filemanager.removeItem(at: url)
273276

274-
// remove from overrides in `settings.json`
275-
Settings.shared.preferences.theme.overrides.removeValue(forKey: theme.name)
277+
// remove from overrides in `settings.json`
278+
Settings.shared.preferences.theme.overrides.removeValue(forKey: theme.name)
276279

277-
// reload themes
278-
try self.loadThemes()
279-
} catch {
280-
print(error)
280+
// reload themes
281+
try self.loadThemes()
282+
} catch {
283+
print(error)
284+
}
281285
}
282286
}
283287

@@ -325,4 +329,61 @@ final class ThemeModel: ObservableObject {
325329
}
326330
}
327331
}
332+
333+
func importTheme() {
334+
let openPanel = NSOpenPanel()
335+
let allowedTypes = [UTType(filenameExtension: "cetheme")!]
336+
337+
openPanel.prompt = "Import"
338+
openPanel.allowedContentTypes = allowedTypes
339+
openPanel.canChooseFiles = true
340+
openPanel.canChooseDirectories = false
341+
openPanel.allowsMultipleSelection = false
342+
343+
openPanel.begin { result in
344+
if result.rawValue == NSApplication.ModalResponse.OK.rawValue {
345+
if let url = openPanel.urls.first {
346+
self.duplicate(url)
347+
}
348+
}
349+
}
350+
}
351+
352+
func duplicate(_ url: URL) {
353+
do {
354+
// Construct the destination file URL
355+
var destinationFileURL = self.themesURL.appendingPathComponent(url.lastPathComponent)
356+
357+
// Check if the file already exists
358+
var iterator = 1
359+
while FileManager.default.fileExists(atPath: destinationFileURL.path) {
360+
// Extract the base filename and extension
361+
let fileExtension = destinationFileURL.pathExtension
362+
var fileName = destinationFileURL.deletingPathExtension().lastPathComponent
363+
364+
// Remove any existing iterator
365+
if let range = fileName.range(of: " \\d+$", options: .regularExpression) {
366+
fileName = String(fileName[..<range.lowerBound])
367+
}
368+
369+
// Generate a new filename with an iterator
370+
let newFileName = "\(fileName) \(iterator)"
371+
destinationFileURL = self.themesURL.appendingPathComponent(newFileName).appendingPathExtension(fileExtension)
372+
iterator += 1
373+
}
374+
375+
// Copy the file from selected URL to the destination
376+
try FileManager.default.copyItem(at: url, to: destinationFileURL)
377+
378+
try self.loadThemes()
379+
380+
if var newTheme = self.themes.first(where: { $0.fileURL == destinationFileURL }) {
381+
newTheme.name = newTheme.fileURL?.lastPathComponent ?? ""
382+
self.selectedTheme = newTheme
383+
}
384+
} catch {
385+
print("Error adding theme: \(error.localizedDescription)")
386+
}
387+
}
388+
328389
}

CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingThemeRow.swift

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ struct ThemeSettingsThemeRow: View {
1212
var active: Bool
1313
var action: (Theme) -> Void
1414

15+
@ObservedObject private var themeModel: ThemeModel = .shared
16+
1517
@State private var presentingDetails: Bool = false
1618

1719
@State private var isHovering = false
@@ -28,22 +30,40 @@ struct ThemeSettingsThemeRow: View {
2830
.font(.footnote)
2931
}
3032
.frame(maxWidth: .infinity, alignment: .leading)
31-
Button {
32-
presentingDetails = true
33-
} label: {
34-
Text("Details...")
33+
if !active {
34+
Button {
35+
action(theme)
36+
} label: {
37+
Text("Choose")
38+
}
39+
.buttonStyle(.bordered)
40+
.opacity(isHovering ? 1 : 0)
3541
}
36-
.buttonStyle(.bordered)
37-
.opacity(isHovering ? 1 : 0)
3842
ThemeSettingsColorPreview(theme)
43+
Menu {
44+
Button("Details...") {
45+
presentingDetails = true
46+
}
47+
Button("Duplicate") {
48+
if let fileURL = theme.fileURL {
49+
themeModel.duplicate(fileURL)
50+
}
51+
}
52+
Divider()
53+
Button("Delete") {
54+
themeModel.delete(theme)
55+
}
56+
.disabled(theme.isBundled)
57+
} label: {
58+
Image(systemName: "ellipsis.circle")
59+
.font(.system(size: 16))
60+
}
61+
.buttonStyle(.icon)
3962
}
4063
.padding(10)
4164
.onHover { hovering in
4265
isHovering = hovering
4366
}
44-
.onTapGesture {
45-
action(theme)
46-
}
4767
.sheet(isPresented: $presentingDetails) {
4868
ThemeSettingsThemeDetails($theme)
4969
}

CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeDetails.swift

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ struct ThemeSettingsThemeDetails: View {
1111
@Environment(\.dismiss)
1212
var dismiss
1313

14+
@Environment(\.colorScheme)
15+
var colorScheme
16+
1417
@Binding var theme: Theme
1518

1619
@State private var initialTheme: Theme
@@ -27,8 +30,15 @@ struct ThemeSettingsThemeDetails: View {
2730
Form {
2831
Section {
2932
TextField("Name", text: $theme.displayName)
33+
TextField("Author", text: $theme.author)
34+
Picker("Type", selection: $theme.appearance) {
35+
Text("Light")
36+
.tag(Theme.ThemeType.light)
37+
Text("Dark")
38+
.tag(Theme.ThemeType.dark)
39+
}
3040
}
31-
Section {
41+
Section("Text") {
3242
SettingsColorPicker(
3343
"Text",
3444
color: $theme.editor.text.swiftColor
@@ -42,7 +52,7 @@ struct ThemeSettingsThemeDetails: View {
4252
color: $theme.editor.invisibles.swiftColor
4353
)
4454
}
45-
Section {
55+
Section("Background") {
4656
SettingsColorPicker(
4757
"Background",
4858
color: $theme.editor.background.swiftColor
@@ -56,59 +66,66 @@ struct ThemeSettingsThemeDetails: View {
5666
color: $theme.editor.selection.swiftColor
5767
)
5868
}
59-
Section {
69+
Section("Tokens") {
6070
VStack(spacing: 0) {
6171
ThemeSettingsThemeToken(
6272
"Keywords",
6373
color: $theme.editor.keywords.swiftColor
6474
)
65-
Divider().padding(.leading, 10)
75+
Divider().padding(.horizontal, 10)
6676
ThemeSettingsThemeToken(
6777
"Commands",
6878
color: $theme.editor.commands.swiftColor
6979
)
70-
Divider().padding(.leading, 10)
80+
Divider().padding(.horizontal, 10)
7181
ThemeSettingsThemeToken(
7282
"Types",
7383
color: $theme.editor.types.swiftColor
7484
)
75-
Divider().padding(.leading, 10)
85+
Divider().padding(.horizontal, 10)
7686
ThemeSettingsThemeToken(
7787
"Attributes",
7888
color: $theme.editor.attributes.swiftColor
7989
)
80-
Divider().padding(.leading, 10)
90+
Divider().padding(.horizontal, 10)
8191
ThemeSettingsThemeToken(
8292
"Variables",
8393
color: $theme.editor.variables.swiftColor
8494
)
85-
Divider().padding(.leading, 10)
95+
Divider().padding(.horizontal, 10)
8696
ThemeSettingsThemeToken(
8797
"Values",
8898
color: $theme.editor.values.swiftColor
8999
)
90-
Divider().padding(.leading, 10)
100+
Divider().padding(.horizontal, 10)
91101
ThemeSettingsThemeToken(
92102
"Numbers",
93103
color: $theme.editor.numbers.swiftColor
94104
)
95-
Divider().padding(.leading, 10)
105+
Divider().padding(.horizontal, 10)
96106
ThemeSettingsThemeToken(
97107
"Strings",
98108
color: $theme.editor.strings.swiftColor
99109
)
100-
Divider().padding(.leading, 10)
110+
Divider().padding(.horizontal, 10)
101111
ThemeSettingsThemeToken(
102112
"Characters",
103113
color: $theme.editor.characters.swiftColor
104114
)
105-
Divider().padding(.leading, 10)
115+
Divider().padding(.horizontal, 10)
106116
ThemeSettingsThemeToken(
107117
"Comments",
108118
color: $theme.editor.comments.swiftColor
109119
)
110120
}
121+
.background(theme.editor.background.swiftColor)
111122
.padding(-10)
123+
.colorScheme(
124+
theme.appearance == .dark
125+
? .dark
126+
: theme.appearance == .light
127+
? .light : colorScheme
128+
)
112129
}
113130
}.formStyle(.grouped)
114131
Divider()

CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeToken.swift

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,38 +12,49 @@ struct ThemeSettingsThemeToken: View {
1212
@Binding var color: Color
1313

1414
@State private var isHovering = false
15+
@State private var selectedColor: Color
1516
@State private var isBold = false
1617
@State private var isItalic = false
1718

1819
init(_ label: String, color: Binding<Color>) {
1920
self.label = label
2021
self._color = color
22+
self._selectedColor = State(initialValue: color.wrappedValue)
2123
}
2224

2325
var body: some View {
24-
SettingsColorPicker(
25-
label,
26-
color: $color
27-
) {
28-
HStack(spacing: 8) {
29-
Toggle(isOn: $isBold) {
30-
Image(systemName: "bold")
26+
LabeledContent {
27+
HStack(spacing: 16) {
28+
HStack(spacing: 8) {
29+
Toggle(isOn: $isBold) {
30+
Image(systemName: "bold")
31+
}
32+
.toggleStyle(.icon)
33+
.help("Bold")
34+
Divider()
35+
.fixedSize()
36+
Toggle(isOn: $isItalic) {
37+
Image(systemName: "italic")
38+
}
39+
.toggleStyle(.icon)
40+
.help("Italic")
3141
}
32-
.toggleStyle(.icon)
33-
.help("Bold")
34-
Divider()
35-
.fixedSize()
36-
Toggle(isOn: $isItalic) {
37-
Image(systemName: "italic")
38-
}
39-
.toggleStyle(.icon)
40-
.help("Italic")
42+
.opacity(isHovering || isBold || isItalic ? 1 : 0)
43+
ColorPicker(selection: $selectedColor, supportsOpacity: false) { }
44+
.labelsHidden()
4145
}
42-
.opacity(isHovering || isBold || isItalic ? 1 : 0)
46+
} label: {
47+
Text(label)
48+
.font(.system(.body, design: .monospaced, weight: isBold ? .bold : .medium))
49+
.foregroundStyle(color)
50+
.italic(isItalic)
4351
}
4452
.padding(10)
4553
.onHover { hovering in
4654
isHovering = hovering
4755
}
56+
.onChange(of: selectedColor) { newValue in
57+
color = newValue
58+
}
4859
}
4960
}

0 commit comments

Comments
 (0)