Created
May 7, 2025 04:38
-
-
Save kopyl/05ad15b240b58c7d2497399be987d377 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Cocoa | |
import SwiftUI | |
class WindowConfig { | |
static let width: CGFloat = 659 | |
static let height: CGFloat = 400 | |
static let sidebarFixedWidth: CGFloat = 215 | |
static let sideBarTopPadding: CGFloat = 43 | |
} | |
enum SidebarItem: String, CaseIterable { | |
case shortcut = "Shortcut" | |
case appearance = "Appearance" | |
case general = "General" | |
var icon: NSImageView { | |
let imageName = "\(self.rawValue)-icon" | |
let image = NSImage(named: NSImage.Name(imageName)) ?? NSImage() | |
return NSImageView(image: image) | |
} | |
var viewController: NSViewController { | |
switch self { | |
case .shortcut: | |
// return ShortcutViewController() | |
return NSHostingController(rootView: ShortcutSettingsView(appState: appState)) | |
case .appearance: | |
// return AppearanceViewController() | |
return NSHostingController(rootView: AppearanceSettingsView(appState: appState)) | |
case .general: | |
// return GeneralViewController() | |
return NSHostingController(rootView: GeneralSettingsView(appState: appState)) | |
} | |
} | |
} | |
protocol SidebarSelectionDelegate: AnyObject { | |
func didSelectSidebarItem(_ item: SidebarItem) | |
} | |
class DraggableView: NSView { | |
override public func mouseDown(with event: NSEvent) { | |
window?.performDrag(with: event) | |
} | |
} | |
class SidebarViewController: NSViewController, NSTableViewDataSource, NSTableViewDelegate { | |
weak var delegate: SidebarSelectionDelegate? | |
private let tableView = NSTableView() | |
private let items = SidebarItem.allCases | |
override func loadView() { | |
self.view = NSView() | |
let column = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("Column")) | |
tableView.addTableColumn(column) | |
tableView.delegate = self | |
tableView.dataSource = self | |
tableView.rowHeight = 28 | |
tableView.translatesAutoresizingMaskIntoConstraints = false | |
tableView.focusRingType = .none | |
DispatchQueue.main.async { | |
self.tableView.selectRowIndexes([0], byExtendingSelection: false) | |
} | |
view.addSubview(tableView) | |
NSLayoutConstraint.activate([ | |
tableView.topAnchor.constraint(equalTo: view.topAnchor, constant: WindowConfig.sideBarTopPadding), | |
]) | |
let draggableView = DraggableView() | |
draggableView.translatesAutoresizingMaskIntoConstraints = false | |
draggableView.wantsLayer = true | |
view.addSubview(draggableView) | |
NSLayoutConstraint.activate([ | |
draggableView.heightAnchor.constraint(equalToConstant: WindowConfig.sideBarTopPadding + 10), | |
draggableView.widthAnchor.constraint(equalToConstant: WindowConfig.sidebarFixedWidth), | |
draggableView.topAnchor.constraint(equalTo: view.topAnchor, constant: 0), | |
]) | |
} | |
func numberOfRows(in tableView: NSTableView) -> Int { | |
return items.count | |
} | |
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { | |
let textLabel = NSTextField(labelWithString: items[row].rawValue) | |
let imageView = items[row].icon | |
let stackView = NSStackView(views: [imageView, textLabel]) | |
stackView.spacing = 5 | |
stackView.heightAnchor.constraint(equalToConstant: tableView.rowHeight).isActive = true | |
return stackView | |
} | |
func tableViewSelectionDidChange(_ notification: Notification) { | |
let selectedIndex = tableView.selectedRow | |
guard selectedIndex >= 0 else { return } | |
delegate?.didSelectSidebarItem(items[selectedIndex]) | |
} | |
} | |
class SettingsView: NSView { | |
let rootView: AnyView | |
required init?(coder: NSCoder) { | |
fatalError("init(coder:) has not been implemented") | |
} | |
override init(frame frameRect: NSRect) { | |
fatalError("Use init(view:) instead") | |
} | |
init<V: View>(view: V) { | |
self.rootView = AnyView(view) | |
super.init(frame: .zero) | |
setupView() | |
} | |
private func setupView() { | |
let hostingView = NSHostingView(rootView: rootView) | |
addSubview(hostingView) | |
hostingView.translatesAutoresizingMaskIntoConstraints = false | |
NSLayoutConstraint.activate([ | |
hostingView.topAnchor.constraint(equalTo: topAnchor), | |
hostingView.leadingAnchor.constraint(equalTo: leadingAnchor), | |
hostingView.trailingAnchor.constraint(equalTo: trailingAnchor), | |
hostingView.bottomAnchor.constraint(equalTo: bottomAnchor) | |
]) | |
} | |
} | |
class ShortcutViewController: NSViewController { | |
override func loadView() { | |
self.view = SettingsView(view: ShortcutSettingsView(appState: appState)) | |
} | |
} | |
class AppearanceViewController: NSViewController { | |
override func loadView() { | |
self.view = SettingsView(view: AppearanceSettingsView(appState: appState)) | |
} | |
} | |
class GeneralViewController: NSViewController { | |
override func loadView() { | |
self.view = SettingsView(view: GeneralSettingsView(appState: appState)) | |
} | |
} | |
class SplitViewController: NSSplitViewController, SidebarSelectionDelegate { | |
override func loadView() { | |
view = NSView(frame: NSRect(x: 0, y: 0, width: WindowConfig.width, height: WindowConfig.height)) | |
super.loadView() | |
} | |
override func viewDidLoad() { | |
super.viewDidLoad() | |
let sidebarVC = SidebarViewController() | |
sidebarVC.delegate = self | |
let sidebarItem = NSSplitViewItem(sidebarWithViewController: sidebarVC) | |
sidebarItem.minimumThickness = WindowConfig.sidebarFixedWidth | |
sidebarItem.maximumThickness = WindowConfig.sidebarFixedWidth | |
sidebarItem.canCollapse = false | |
addSplitViewItem(sidebarItem) | |
let detailItem = NSSplitViewItem(viewController: SidebarItem.shortcut.viewController) | |
addSplitViewItem(detailItem) | |
} | |
func didSelectSidebarItem(_ item: SidebarItem) { | |
removeSplitViewItem(splitViewItems[1]) | |
let newDetailItem = NSSplitViewItem(viewController: item.viewController) | |
addSplitViewItem(newDetailItem) | |
} | |
} | |
func addPaddingToWindowButtons(leading: CGFloat, top: CGFloat) { | |
settingsWindow?.standardWindowButton(.miniaturizeButton)?.frame.origin.y -= top | |
settingsWindow?.standardWindowButton(.closeButton)?.frame.origin.y -= top | |
settingsWindow?.standardWindowButton(.zoomButton)?.frame.origin.y -= top | |
settingsWindow?.standardWindowButton(.miniaturizeButton)?.frame.origin.x += leading | |
settingsWindow?.standardWindowButton(.closeButton)?.frame.origin.x += leading | |
settingsWindow?.standardWindowButton(.zoomButton)?.frame.origin.x += leading | |
let buttonContainer = settingsWindow?.standardWindowButton(.closeButton)?.superview | |
for subview in buttonContainer?.subviews ?? [] where subview is NSTextField { | |
subview.frame.origin.y -= top | |
} | |
} | |
class SettingsWindowController: NSWindowController { | |
override init(window: NSWindow?) { | |
super.init(window: window) | |
NotificationCenter.default.addObserver( | |
self, | |
selector: #selector(windowDidResize(_:)), | |
name: NSWindow.didResizeNotification, | |
object: settingsWindow | |
) | |
} | |
required init?(coder: NSCoder) { | |
fatalError("init(coder:) has not been implemented") | |
} | |
@objc func windowDidResize(_ notification: Notification) { | |
addPaddingToWindowButtons(leading: 12, top: 12) | |
} | |
deinit { | |
NotificationCenter.default.removeObserver( | |
self, | |
name: NSWindow.didResizeNotification, | |
object: self.window | |
) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment