add Settings window with theme mode and line indicator mode controls
This commit is contained in:
parent
4ae9409eff
commit
0bf2d1b344
|
|
@ -28,8 +28,14 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
window.setFrameAutosaveName("SwiftlyMainWindow")
|
window.setFrameAutosaveName("SwiftlyMainWindow")
|
||||||
window.makeKeyAndOrderFront(nil)
|
window.makeKeyAndOrderFront(nil)
|
||||||
|
|
||||||
|
applyThemeAppearance()
|
||||||
setupMenuBar()
|
setupMenuBar()
|
||||||
observeDocumentTitle()
|
observeDocumentTitle()
|
||||||
|
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
self, selector: #selector(settingsDidChange),
|
||||||
|
name: .settingsChanged, object: nil
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||||
|
|
@ -59,6 +65,10 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
let menu = NSMenu()
|
let menu = NSMenu()
|
||||||
menu.addItem(withTitle: "About Swiftly", action: #selector(NSApplication.orderFrontStandardAboutPanel(_:)), keyEquivalent: "")
|
menu.addItem(withTitle: "About Swiftly", action: #selector(NSApplication.orderFrontStandardAboutPanel(_:)), keyEquivalent: "")
|
||||||
menu.addItem(.separator())
|
menu.addItem(.separator())
|
||||||
|
let settingsItem = NSMenuItem(title: "Settings...", action: #selector(openSettings), keyEquivalent: ",")
|
||||||
|
settingsItem.target = self
|
||||||
|
menu.addItem(settingsItem)
|
||||||
|
menu.addItem(.separator())
|
||||||
menu.addItem(withTitle: "Quit Swiftly", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")
|
menu.addItem(withTitle: "Quit Swiftly", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")
|
||||||
item.submenu = menu
|
item.submenu = menu
|
||||||
return item
|
return item
|
||||||
|
|
@ -163,6 +173,15 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc private func openSettings() {
|
||||||
|
SettingsWindowController.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func settingsDidChange() {
|
||||||
|
window.backgroundColor = Theme.current.base
|
||||||
|
window.contentView?.needsDisplay = true
|
||||||
|
}
|
||||||
|
|
||||||
@objc private func toggleSidebar() {
|
@objc private func toggleSidebar() {
|
||||||
NotificationCenter.default.post(name: .toggleSidebar, object: nil)
|
NotificationCenter.default.post(name: .toggleSidebar, object: nil)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,10 @@ import SwiftUI
|
||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
@ObservedObject var state: AppState
|
@ObservedObject var state: AppState
|
||||||
@State private var sidebarVisible: Bool = false
|
@State private var sidebarVisible: Bool = false
|
||||||
|
@AppStorage("themeMode") private var themeMode: String = "auto"
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
let _ = themeMode
|
||||||
HSplitView {
|
HSplitView {
|
||||||
if sidebarVisible {
|
if sidebarVisible {
|
||||||
SidebarView(state: state)
|
SidebarView(state: state)
|
||||||
|
|
|
||||||
|
|
@ -818,6 +818,7 @@ struct EditorTextView: NSViewRepresentable {
|
||||||
private var isUpdatingTables = false
|
private var isUpdatingTables = false
|
||||||
private var embeddedTableViews: [MarkdownTableView] = []
|
private var embeddedTableViews: [MarkdownTableView] = []
|
||||||
private var focusObserver: NSObjectProtocol?
|
private var focusObserver: NSObjectProtocol?
|
||||||
|
private var settingsObserver: NSObjectProtocol?
|
||||||
|
|
||||||
init(_ parent: EditorTextView) {
|
init(_ parent: EditorTextView) {
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
|
|
@ -829,12 +830,32 @@ struct EditorTextView: NSViewRepresentable {
|
||||||
tv.window?.makeFirstResponder(tv)
|
tv.window?.makeFirstResponder(tv)
|
||||||
tv.setSelectedRange(NSRange(location: 0, length: 0))
|
tv.setSelectedRange(NSRange(location: 0, length: 0))
|
||||||
}
|
}
|
||||||
|
settingsObserver = NotificationCenter.default.addObserver(
|
||||||
|
forName: .settingsChanged, object: nil, queue: .main
|
||||||
|
) { [weak self] _ in
|
||||||
|
guard let tv = self?.textView, let ts = tv.textStorage else { return }
|
||||||
|
let palette = Theme.current
|
||||||
|
tv.backgroundColor = palette.base
|
||||||
|
tv.insertionPointColor = palette.text
|
||||||
|
tv.selectedTextAttributes = [.backgroundColor: palette.surface1]
|
||||||
|
tv.typingAttributes = [
|
||||||
|
.font: Theme.editorFont,
|
||||||
|
.foregroundColor: palette.text
|
||||||
|
]
|
||||||
|
ts.beginEditing()
|
||||||
|
applySyntaxHighlighting(to: ts)
|
||||||
|
ts.endEditing()
|
||||||
|
tv.needsDisplay = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
if let obs = focusObserver {
|
if let obs = focusObserver {
|
||||||
NotificationCenter.default.removeObserver(obs)
|
NotificationCenter.default.removeObserver(obs)
|
||||||
}
|
}
|
||||||
|
if let obs = settingsObserver {
|
||||||
|
NotificationCenter.default.removeObserver(obs)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func textDidChange(_ notification: Notification) {
|
func textDidChange(_ notification: Notification) {
|
||||||
|
|
@ -1982,6 +2003,8 @@ class LineNumberTextView: NSTextView {
|
||||||
let text = string as NSString
|
let text = string as NSString
|
||||||
guard text.length > 0 else { return }
|
guard text.length > 0 else { return }
|
||||||
|
|
||||||
|
let lineMode = UserDefaults.standard.string(forKey: "lineIndicatorMode") ?? "on"
|
||||||
|
|
||||||
var containerVisible = visibleRect
|
var containerVisible = visibleRect
|
||||||
containerVisible.origin.x -= origin.x
|
containerVisible.origin.x -= origin.x
|
||||||
containerVisible.origin.y -= origin.y
|
containerVisible.origin.y -= origin.y
|
||||||
|
|
@ -1995,10 +2018,24 @@ class LineNumberTextView: NSTextView {
|
||||||
idx += 1
|
idx += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var cursorLine = 1
|
||||||
|
if lineMode == "vim" {
|
||||||
|
let cursorPos = selectedRange().location
|
||||||
|
var ci = 0
|
||||||
|
while ci < min(cursorPos, text.length) {
|
||||||
|
if text.character(at: ci) == 0x0A { cursorLine += 1 }
|
||||||
|
ci += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let lineAttrs: [NSAttributedString.Key: Any] = [
|
let lineAttrs: [NSAttributedString.Key: Any] = [
|
||||||
.font: Theme.gutterFont,
|
.font: Theme.gutterFont,
|
||||||
.foregroundColor: palette.overlay0
|
.foregroundColor: palette.overlay0
|
||||||
]
|
]
|
||||||
|
let currentLineAttrs: [NSAttributedString.Key: Any] = [
|
||||||
|
.font: Theme.gutterFont,
|
||||||
|
.foregroundColor: palette.text
|
||||||
|
]
|
||||||
let resultAttrs: [NSAttributedString.Key: Any] = [
|
let resultAttrs: [NSAttributedString.Key: Any] = [
|
||||||
.font: Theme.gutterFont,
|
.font: Theme.gutterFont,
|
||||||
.foregroundColor: palette.teal
|
.foregroundColor: palette.teal
|
||||||
|
|
@ -2011,9 +2048,25 @@ class LineNumberTextView: NSTextView {
|
||||||
let lineRect = lm.boundingRect(forGlyphRange: lineGlyphRange, in: tc)
|
let lineRect = lm.boundingRect(forGlyphRange: lineGlyphRange, in: tc)
|
||||||
let y = lineRect.origin.y + origin.y
|
let y = lineRect.origin.y + origin.y
|
||||||
|
|
||||||
let numStr = NSAttributedString(string: "\(lineNumber)", attributes: lineAttrs)
|
if lineMode != "off" {
|
||||||
|
let displayNum: Int
|
||||||
|
let attrs: [NSAttributedString.Key: Any]
|
||||||
|
if lineMode == "vim" {
|
||||||
|
if lineNumber == cursorLine {
|
||||||
|
displayNum = lineNumber
|
||||||
|
attrs = currentLineAttrs
|
||||||
|
} else {
|
||||||
|
displayNum = abs(lineNumber - cursorLine)
|
||||||
|
attrs = lineAttrs
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
displayNum = lineNumber
|
||||||
|
attrs = lineAttrs
|
||||||
|
}
|
||||||
|
let numStr = NSAttributedString(string: "\(displayNum)", attributes: attrs)
|
||||||
let numSize = numStr.size()
|
let numSize = numStr.size()
|
||||||
numStr.draw(at: NSPoint(x: LineNumberTextView.gutterWidth - numSize.width - 8, y: y))
|
numStr.draw(at: NSPoint(x: LineNumberTextView.gutterWidth - numSize.width - 8, y: y))
|
||||||
|
}
|
||||||
|
|
||||||
if let result = evalResults[lineNumber - 1] {
|
if let result = evalResults[lineNumber - 1] {
|
||||||
let resultStr = NSAttributedString(string: "\u{2192} \(result)", attributes: resultAttrs)
|
let resultStr = NSAttributedString(string: "\u{2192} \(result)", attributes: resultAttrs)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,116 @@
|
||||||
|
import SwiftUI
|
||||||
|
import Cocoa
|
||||||
|
|
||||||
|
enum ThemeMode: String, CaseIterable {
|
||||||
|
case auto = "auto"
|
||||||
|
case dark = "dark"
|
||||||
|
case light = "light"
|
||||||
|
|
||||||
|
var label: String {
|
||||||
|
switch self {
|
||||||
|
case .auto: return "Auto"
|
||||||
|
case .dark: return "Dark"
|
||||||
|
case .light: return "Light"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum LineIndicatorMode: String, CaseIterable {
|
||||||
|
case on = "on"
|
||||||
|
case off = "off"
|
||||||
|
case vim = "vim"
|
||||||
|
|
||||||
|
var label: String {
|
||||||
|
switch self {
|
||||||
|
case .on: return "On"
|
||||||
|
case .off: return "Off"
|
||||||
|
case .vim: return "Vim"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SettingsView: View {
|
||||||
|
@AppStorage("themeMode") private var themeMode: String = "auto"
|
||||||
|
@AppStorage("lineIndicatorMode") private var lineIndicatorMode: String = "on"
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
let palette = Theme.current
|
||||||
|
Form {
|
||||||
|
Section("Theme") {
|
||||||
|
Picker("Mode", selection: $themeMode) {
|
||||||
|
ForEach(ThemeMode.allCases, id: \.rawValue) { mode in
|
||||||
|
Text(mode.label).tag(mode.rawValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pickerStyle(.segmented)
|
||||||
|
}
|
||||||
|
|
||||||
|
Section("Line Numbers") {
|
||||||
|
Picker("Mode", selection: $lineIndicatorMode) {
|
||||||
|
ForEach(LineIndicatorMode.allCases, id: \.rawValue) { mode in
|
||||||
|
Text(mode.label).tag(mode.rawValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pickerStyle(.segmented)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.formStyle(.grouped)
|
||||||
|
.frame(width: 320, height: 180)
|
||||||
|
.background(Color(ns: palette.base))
|
||||||
|
.onChange(of: themeMode) {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
applyThemeAppearance()
|
||||||
|
NotificationCenter.default.post(name: .settingsChanged, object: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onChange(of: lineIndicatorMode) {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
NotificationCenter.default.post(name: .settingsChanged, object: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyThemeAppearance() {
|
||||||
|
let mode = UserDefaults.standard.string(forKey: "themeMode") ?? "auto"
|
||||||
|
switch mode {
|
||||||
|
case "dark":
|
||||||
|
NSApp.appearance = NSAppearance(named: .darkAqua)
|
||||||
|
case "light":
|
||||||
|
NSApp.appearance = NSAppearance(named: .aqua)
|
||||||
|
default:
|
||||||
|
NSApp.appearance = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Notification.Name {
|
||||||
|
static let settingsChanged = Notification.Name("settingsChanged")
|
||||||
|
}
|
||||||
|
|
||||||
|
class SettingsWindowController {
|
||||||
|
private static var window: NSWindow?
|
||||||
|
|
||||||
|
static func show() {
|
||||||
|
if let existing = window, existing.isVisible {
|
||||||
|
existing.makeKeyAndOrderFront(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let settingsView = SettingsView()
|
||||||
|
let hostingView = NSHostingView(rootView: settingsView)
|
||||||
|
hostingView.frame = NSRect(x: 0, y: 0, width: 320, height: 200)
|
||||||
|
|
||||||
|
let w = NSWindow(
|
||||||
|
contentRect: NSRect(x: 0, y: 0, width: 320, height: 200),
|
||||||
|
styleMask: [.titled, .closable],
|
||||||
|
backing: .buffered,
|
||||||
|
defer: false
|
||||||
|
)
|
||||||
|
w.title = "Settings"
|
||||||
|
w.contentView = hostingView
|
||||||
|
w.center()
|
||||||
|
w.isReleasedWhenClosed = false
|
||||||
|
w.makeKeyAndOrderFront(nil)
|
||||||
|
window = w
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -90,10 +90,16 @@ struct Theme {
|
||||||
)
|
)
|
||||||
|
|
||||||
static var current: CatppuccinPalette {
|
static var current: CatppuccinPalette {
|
||||||
|
let mode = UserDefaults.standard.string(forKey: "themeMode") ?? "auto"
|
||||||
|
switch mode {
|
||||||
|
case "dark": return mocha
|
||||||
|
case "light": return latte
|
||||||
|
default:
|
||||||
let appearance = NSApp.effectiveAppearance
|
let appearance = NSApp.effectiveAppearance
|
||||||
let isDark = appearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua
|
let isDark = appearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua
|
||||||
return isDark ? mocha : latte
|
return isDark ? mocha : latte
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static let editorFont = NSFont.monospacedSystemFont(ofSize: 13, weight: .regular)
|
static let editorFont = NSFont.monospacedSystemFont(ofSize: 13, weight: .regular)
|
||||||
static let gutterFont = NSFont.monospacedSystemFont(ofSize: 11, weight: .regular)
|
static let gutterFont = NSFont.monospacedSystemFont(ofSize: 11, weight: .regular)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue