Skip to content

Commit 63776a1

Browse files
ItemBox implementation
1 parent e5369da commit 63776a1

File tree

1 file changed

+291
-0
lines changed

1 file changed

+291
-0
lines changed
Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
//
2+
// TextView+ItemBox.swift
3+
// CodeEditTextView
4+
//
5+
// Created by Abe Malla on 6/18/24.
6+
//
7+
8+
import AppKit
9+
import SwiftUI
10+
11+
public protocol ItemBoxEntry {
12+
var view: NSView { get }
13+
}
14+
15+
public final class ItemBoxWindowController: NSWindowController {
16+
17+
public static let DEFAULT_SIZE = NSSize(width: 300, height: 212)
18+
19+
public var items: [any ItemBoxEntry] = [] {
20+
didSet {
21+
updateItems()
22+
}
23+
}
24+
25+
private let tableView = NSTableView()
26+
private let scrollView = NSScrollView()
27+
private var localEventMonitor: Any?
28+
29+
public var isVisible: Bool {
30+
return window?.isVisible ?? false
31+
}
32+
33+
public init() {
34+
let window = NSWindow(
35+
contentRect: NSRect(origin: CGPoint.zero, size: ItemBoxWindowController.DEFAULT_SIZE),
36+
styleMask: [.resizable, .fullSizeContentView, .nonactivatingPanel],
37+
backing: .buffered,
38+
defer: false
39+
)
40+
41+
// Style window
42+
window.titleVisibility = .hidden
43+
window.titlebarAppearsTransparent = true
44+
window.isExcludedFromWindowsMenu = true
45+
window.isReleasedWhenClosed = false
46+
window.level = .popUpMenu
47+
window.hasShadow = true
48+
window.isOpaque = false
49+
window.tabbingMode = .disallowed
50+
window.hidesOnDeactivate = true
51+
window.backgroundColor = .clear
52+
window.minSize = ItemBoxWindowController.DEFAULT_SIZE
53+
54+
// Style the content with custom borders and colors
55+
window.contentView?.wantsLayer = true
56+
window.contentView?.layer?.backgroundColor = CGColor(
57+
srgbRed: 31.0 / 255.0, green: 31.0 / 255.0, blue: 36.0 / 255.0, alpha: 1.0
58+
)
59+
window.contentView?.layer?.cornerRadius = 8
60+
window.contentView?.layer?.borderWidth = 1
61+
window.contentView?.layer?.borderColor = NSColor.gray.withAlphaComponent(0.4).cgColor
62+
let innerShadow = NSShadow()
63+
innerShadow.shadowColor = NSColor.black.withAlphaComponent(0.1)
64+
innerShadow.shadowOffset = NSSize(width: 0, height: -1)
65+
innerShadow.shadowBlurRadius = 2
66+
window.contentView?.shadow = innerShadow
67+
68+
super.init(window: window)
69+
70+
setupTableView()
71+
}
72+
73+
required init?(coder: NSCoder) {
74+
fatalError("init(coder:) has not been implemented")
75+
}
76+
77+
/// Opens the window of items
78+
public func show() {
79+
super.showWindow(nil)
80+
setupEventMonitor()
81+
}
82+
83+
public func showWindow(attachedTo parentWindow: NSWindow) {
84+
guard let window = self.window else { return }
85+
parentWindow.addChildWindow(window, ordered: .above)
86+
window.orderFront(nil)
87+
88+
// Close on window switch
89+
NotificationCenter.default.addObserver(
90+
forName: NSWindow.didResignKeyNotification,
91+
object: parentWindow,
92+
queue: .current
93+
) { [weak self] _ in
94+
self?.close()
95+
}
96+
97+
self.show()
98+
}
99+
100+
public override func close() {
101+
guard isVisible else { return }
102+
removeEventMonitor()
103+
super.close()
104+
}
105+
106+
private func setupTableView() {
107+
tableView.delegate = self
108+
tableView.dataSource = self
109+
tableView.headerView = nil
110+
tableView.backgroundColor = .clear
111+
tableView.intercellSpacing = .zero
112+
tableView.selectionHighlightStyle = .none
113+
tableView.backgroundColor = .clear
114+
tableView.enclosingScrollView?.drawsBackground = false
115+
tableView.rowHeight = 24
116+
117+
let column = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("ItemsCell"))
118+
column.width = ItemBoxWindowController.DEFAULT_SIZE.width
119+
tableView.addTableColumn(column)
120+
121+
scrollView.documentView = tableView
122+
scrollView.hasVerticalScroller = true
123+
scrollView.verticalScroller?.controlSize = .large
124+
scrollView.autohidesScrollers = true
125+
scrollView.automaticallyAdjustsContentInsets = false
126+
scrollView.contentInsets = NSEdgeInsetsZero
127+
window?.contentView?.addSubview(scrollView)
128+
129+
scrollView.translatesAutoresizingMaskIntoConstraints = false
130+
NSLayoutConstraint.activate([
131+
scrollView.topAnchor.constraint(equalTo: window!.contentView!.topAnchor),
132+
scrollView.leadingAnchor.constraint(equalTo: window!.contentView!.leadingAnchor),
133+
scrollView.trailingAnchor.constraint(equalTo: window!.contentView!.trailingAnchor),
134+
scrollView.bottomAnchor.constraint(equalTo: window!.contentView!.bottomAnchor)
135+
])
136+
}
137+
138+
private func updateItems() {
139+
tableView.reloadData()
140+
}
141+
142+
public func tableViewSelectionDidChange(_ notification: Notification) {
143+
tableView.enumerateAvailableRowViews { (rowView, row) in
144+
if let cellView = rowView.view(atColumn: 0) as? CustomTableCellView {
145+
cellView.backgroundStyle = tableView.selectedRow == row ? .emphasized : .normal
146+
}
147+
}
148+
}
149+
150+
private func setupEventMonitor() {
151+
localEventMonitor = NSEvent.addLocalMonitorForEvents(
152+
matching: [.keyDown, .leftMouseDown, .rightMouseDown]
153+
) { [weak self] event in
154+
guard let self = self else { return event }
155+
156+
switch event.type {
157+
case .keyDown:
158+
switch event.keyCode {
159+
case 53: // Escape key
160+
self.close()
161+
case 125: // Down arrow
162+
self.selectNextItemInTable()
163+
return nil
164+
case 126: // Up arrow
165+
self.selectPreviousItemInTable()
166+
return nil
167+
case 36: // Return key
168+
return nil
169+
default:
170+
break
171+
}
172+
case .leftMouseDown, .rightMouseDown:
173+
// If we click outside the window, close the window
174+
if !NSMouseInRect(NSEvent.mouseLocation, self.window!.frame, false) {
175+
self.close()
176+
}
177+
default:
178+
break
179+
}
180+
181+
return event
182+
}
183+
}
184+
185+
private func removeEventMonitor() {
186+
if let monitor = localEventMonitor {
187+
NSEvent.removeMonitor(monitor)
188+
localEventMonitor = nil
189+
}
190+
}
191+
192+
private func selectNextItemInTable() {
193+
let nextIndex = min(tableView.selectedRow + 1, items.count - 1)
194+
tableView.selectRowIndexes(IndexSet(integer: nextIndex), byExtendingSelection: false)
195+
tableView.scrollRowToVisible(nextIndex)
196+
}
197+
198+
private func selectPreviousItemInTable() {
199+
let previousIndex = max(tableView.selectedRow - 1, 0)
200+
tableView.selectRowIndexes(IndexSet(integer: previousIndex), byExtendingSelection: false)
201+
tableView.scrollRowToVisible(previousIndex)
202+
}
203+
204+
deinit {
205+
removeEventMonitor()
206+
}
207+
}
208+
209+
extension ItemBoxWindowController: NSTableViewDataSource {
210+
public func numberOfRows(in tableView: NSTableView) -> Int {
211+
return items.count
212+
}
213+
}
214+
215+
extension ItemBoxWindowController: NSTableViewDelegate {
216+
// public func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
217+
// items[row].view
218+
// }
219+
220+
public func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
221+
let cellIdentifier = NSUserInterfaceItemIdentifier("CustomCell")
222+
var cell = tableView.makeView(withIdentifier: cellIdentifier, owner: nil) as? CustomTableCellView
223+
224+
if cell == nil {
225+
cell = CustomTableCellView(frame: .zero)
226+
cell?.identifier = cellIdentifier
227+
}
228+
229+
// Remove any existing subviews
230+
cell?.subviews.forEach { $0.removeFromSuperview() }
231+
232+
let itemView = items[row].view
233+
cell?.addSubview(itemView)
234+
itemView.translatesAutoresizingMaskIntoConstraints = false
235+
NSLayoutConstraint.activate([
236+
itemView.topAnchor.constraint(equalTo: cell!.topAnchor),
237+
itemView.leadingAnchor.constraint(equalTo: cell!.leadingAnchor, constant: 4),
238+
itemView.trailingAnchor.constraint(equalTo: cell!.trailingAnchor, constant: -4),
239+
itemView.bottomAnchor.constraint(equalTo: cell!.bottomAnchor)
240+
])
241+
242+
return cell
243+
}
244+
}
245+
246+
private class CustomTableCellView: NSTableCellView {
247+
private let backgroundView = NSView()
248+
249+
override init(frame frameRect: NSRect) {
250+
super.init(frame: frameRect)
251+
setup()
252+
}
253+
254+
required init?(coder: NSCoder) {
255+
fatalError("init(coder:) has not been implemented")
256+
}
257+
258+
private func setup() {
259+
wantsLayer = true
260+
layerContentsRedrawPolicy = .onSetNeedsDisplay
261+
262+
backgroundView.wantsLayer = true
263+
backgroundView.layer?.cornerRadius = 4
264+
addSubview(backgroundView, positioned: .below, relativeTo: nil)
265+
266+
backgroundView.translatesAutoresizingMaskIntoConstraints = false
267+
NSLayoutConstraint.activate([
268+
backgroundView.topAnchor.constraint(equalTo: topAnchor),
269+
backgroundView.leadingAnchor.constraint(equalTo: leadingAnchor),
270+
backgroundView.trailingAnchor.constraint(equalTo: trailingAnchor),
271+
backgroundView.bottomAnchor.constraint(equalTo: bottomAnchor)
272+
])
273+
}
274+
275+
override var backgroundStyle: NSView.BackgroundStyle {
276+
didSet {
277+
updateBackgroundColor()
278+
}
279+
}
280+
281+
private func updateBackgroundColor() {
282+
switch backgroundStyle {
283+
case .normal:
284+
backgroundView.layer?.backgroundColor = NSColor.clear.cgColor
285+
case .emphasized:
286+
backgroundView.layer?.backgroundColor = NSColor.systemBlue.withAlphaComponent(0.5).cgColor
287+
@unknown default:
288+
backgroundView.layer?.backgroundColor = NSColor.clear.cgColor
289+
}
290+
}
291+
}

0 commit comments

Comments
 (0)