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.makeKeyAndOrderFront(nil)
|
||||
|
||||
applyThemeAppearance()
|
||||
setupMenuBar()
|
||||
observeDocumentTitle()
|
||||
|
||||
NotificationCenter.default.addObserver(
|
||||
self, selector: #selector(settingsDidChange),
|
||||
name: .settingsChanged, object: nil
|
||||
)
|
||||
}
|
||||
|
||||
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||
|
|
@ -59,6 +65,10 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||
let menu = NSMenu()
|
||||
menu.addItem(withTitle: "About Swiftly", action: #selector(NSApplication.orderFrontStandardAboutPanel(_:)), keyEquivalent: "")
|
||||
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")
|
||||
item.submenu = menu
|
||||
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() {
|
||||
NotificationCenter.default.post(name: .toggleSidebar, object: nil)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@ import SwiftUI
|
|||
struct ContentView: View {
|
||||
@ObservedObject var state: AppState
|
||||
@State private var sidebarVisible: Bool = false
|
||||
@AppStorage("themeMode") private var themeMode: String = "auto"
|
||||
|
||||
var body: some View {
|
||||
let _ = themeMode
|
||||
HSplitView {
|
||||
if sidebarVisible {
|
||||
SidebarView(state: state)
|
||||
|
|
|
|||
|
|
@ -818,6 +818,7 @@ struct EditorTextView: NSViewRepresentable {
|
|||
private var isUpdatingTables = false
|
||||
private var embeddedTableViews: [MarkdownTableView] = []
|
||||
private var focusObserver: NSObjectProtocol?
|
||||
private var settingsObserver: NSObjectProtocol?
|
||||
|
||||
init(_ parent: EditorTextView) {
|
||||
self.parent = parent
|
||||
|
|
@ -829,12 +830,32 @@ struct EditorTextView: NSViewRepresentable {
|
|||
tv.window?.makeFirstResponder(tv)
|
||||
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 {
|
||||
if let obs = focusObserver {
|
||||
NotificationCenter.default.removeObserver(obs)
|
||||
}
|
||||
if let obs = settingsObserver {
|
||||
NotificationCenter.default.removeObserver(obs)
|
||||
}
|
||||
}
|
||||
|
||||
func textDidChange(_ notification: Notification) {
|
||||
|
|
@ -1982,6 +2003,8 @@ class LineNumberTextView: NSTextView {
|
|||
let text = string as NSString
|
||||
guard text.length > 0 else { return }
|
||||
|
||||
let lineMode = UserDefaults.standard.string(forKey: "lineIndicatorMode") ?? "on"
|
||||
|
||||
var containerVisible = visibleRect
|
||||
containerVisible.origin.x -= origin.x
|
||||
containerVisible.origin.y -= origin.y
|
||||
|
|
@ -1995,10 +2018,24 @@ class LineNumberTextView: NSTextView {
|
|||
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] = [
|
||||
.font: Theme.gutterFont,
|
||||
.foregroundColor: palette.overlay0
|
||||
]
|
||||
let currentLineAttrs: [NSAttributedString.Key: Any] = [
|
||||
.font: Theme.gutterFont,
|
||||
.foregroundColor: palette.text
|
||||
]
|
||||
let resultAttrs: [NSAttributedString.Key: Any] = [
|
||||
.font: Theme.gutterFont,
|
||||
.foregroundColor: palette.teal
|
||||
|
|
@ -2011,9 +2048,25 @@ class LineNumberTextView: NSTextView {
|
|||
let lineRect = lm.boundingRect(forGlyphRange: lineGlyphRange, in: tc)
|
||||
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()
|
||||
numStr.draw(at: NSPoint(x: LineNumberTextView.gutterWidth - numSize.width - 8, y: y))
|
||||
}
|
||||
|
||||
if let result = evalResults[lineNumber - 1] {
|
||||
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 {
|
||||
let mode = UserDefaults.standard.string(forKey: "themeMode") ?? "auto"
|
||||
switch mode {
|
||||
case "dark": return mocha
|
||||
case "light": return latte
|
||||
default:
|
||||
let appearance = NSApp.effectiveAppearance
|
||||
let isDark = appearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua
|
||||
return isDark ? mocha : latte
|
||||
}
|
||||
}
|
||||
|
||||
static let editorFont = NSFont.monospacedSystemFont(ofSize: 13, weight: .regular)
|
||||
static let gutterFont = NSFont.monospacedSystemFont(ofSize: 11, weight: .regular)
|
||||
|
|
|
|||
Loading…
Reference in New Issue