Skip to content

Commit 4409f0a

Browse files
Split up ItemBoxWindowController
1 parent 2a1d12e commit 4409f0a

File tree

2 files changed

+282
-274
lines changed

2 files changed

+282
-274
lines changed

Sources/CodeEditTextView/TextView/TextView+ItemBox.swift renamed to Sources/CodeEditTextView/ItemBox/ItemBoxWindowController+Window.swift

Lines changed: 13 additions & 274 deletions
Original file line numberDiff line numberDiff line change
@@ -1,124 +1,11 @@
11
//
2-
// TextView+ItemBox.swift
2+
// ItemBoxWindowController+Window.swift
33
// CodeEditTextView
44
//
5-
// Created by Abe Malla on 6/18/24.
5+
// Created by Abe Malla on 12/22/24.
66
//
77

8-
import AppKit
9-
import LanguageServerProtocol
10-
11-
// TODO:
12-
// DOCUMENTATION BAR BEHAVIOR:
13-
// IF THE DOCUMENTATION BAR APPEARS WHEN SELECTING AN ITEM AND IT EXTENDS BELOW THE SCREEN, IT WILL FLIP THE DIRECTION OF THE ENTIRE WINDOW
14-
// IF IT GETS FLIPPED AND THEN THE DOCUMENTATION BAR DISAPPEARS FOR EXAMPLE, IT WONT FLIP BACK EVEN IF THERES SPACE NOW
15-
16-
/// Represents an item that can be displayed in the ItemBox
17-
public protocol ItemBoxEntry {
18-
var view: NSView { get }
19-
}
20-
21-
/// Padding at top and bottom of the window
22-
private let WINDOW_PADDING: CGFloat = 5
23-
24-
public final class ItemBoxWindowController: NSWindowController {
25-
26-
// MARK: - Properties
27-
28-
public static var DEFAULT_SIZE: NSSize {
29-
NSSize(
30-
width: 256, // TODO: DOES MIN WIDTH DEPEND ON FONT SIZE?
31-
height: rowsToWindowHeight(for: 1)
32-
)
33-
}
34-
35-
/// The items to be displayed in the window
36-
public var items: [CompletionItem] = [] {
37-
didSet { onItemsUpdated() }
38-
}
39-
40-
/// Whether the ItemBox window is visbile
41-
public var isVisible: Bool {
42-
window?.isVisible ?? false
43-
}
44-
45-
public weak var delegate: ItemBoxDelegate?
46-
47-
// MARK: - Private Properties
48-
49-
/// Height of a single row
50-
private static let ROW_HEIGHT: CGFloat = 21
51-
/// Maximum number of visible rows (8.5)
52-
private static let MAX_VISIBLE_ROWS: CGFloat = 8.5
53-
54-
private let tableView = NSTableView()
55-
private let scrollView = NSScrollView()
56-
private let popover = NSPopover()
57-
/// Tracks when the window is placed above the cursor
58-
private var isWindowAboveCursor = false
59-
60-
private let noItemsLabel: NSTextField = {
61-
let label = NSTextField(labelWithString: "No Completions")
62-
label.textColor = .secondaryLabelColor
63-
label.alignment = .center
64-
label.translatesAutoresizingMaskIntoConstraints = false
65-
label.isHidden = false
66-
// TODO: GET FONT SIZE FROM THEME
67-
label.font = .monospacedSystemFont(ofSize: 12, weight: .regular)
68-
return label
69-
}()
70-
71-
/// An event monitor for keyboard events
72-
private var localEventMonitor: Any?
73-
74-
public static let itemSelectedNotification = NSNotification.Name("ItemBoxItemSelected")
75-
76-
// MARK: - Initialization
77-
78-
public init() {
79-
let window = Self.makeWindow()
80-
super.init(window: window)
81-
configureTableView()
82-
configureScrollView()
83-
setupNoItemsLabel()
84-
}
85-
86-
required init?(coder: NSCoder) {
87-
fatalError("init(coder:) has not been implemented")
88-
}
89-
90-
/// Opens the window of items
91-
private func show() {
92-
setupEventMonitor()
93-
resetScrollPosition()
94-
super.showWindow(nil)
95-
}
96-
97-
/// Opens the window as a child of another window
98-
public func showWindow(attachedTo parentWindow: NSWindow) {
99-
guard let window = window else { return }
100-
101-
parentWindow.addChildWindow(window, ordered: .above)
102-
window.orderFront(nil)
103-
104-
// Close on window switch
105-
NotificationCenter.default.addObserver(
106-
self,
107-
selector: #selector(parentWindowDidResignKey),
108-
name: NSWindow.didResignKeyNotification,
109-
object: parentWindow
110-
)
111-
112-
self.show()
113-
}
114-
115-
/// Close the window
116-
public override func close() {
117-
guard isVisible else { return }
118-
removeEventMonitor()
119-
super.close()
120-
}
121-
8+
extension ItemBoxWindowController {
1229
/// Will constrain the window's frame to be within the visible screen
12310
public func constrainWindowToScreenEdges(cursorRect: NSRect) {
12411
guard let window = self.window,
@@ -260,94 +147,7 @@ public final class ItemBoxWindowController: NSWindowController {
260147
scrollView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
261148
])
262149
}
263-
264-
private func setupNoItemsLabel() {
265-
window?.contentView?.addSubview(noItemsLabel)
266-
267-
NSLayoutConstraint.activate([
268-
noItemsLabel.centerXAnchor.constraint(equalTo: window!.contentView!.centerXAnchor),
269-
noItemsLabel.centerYAnchor.constraint(equalTo: window!.contentView!.centerYAnchor)
270-
])
271-
}
272-
273-
@objc private func parentWindowDidResignKey() {
274-
close()
275-
}
276-
277-
private func onItemsUpdated() {
278-
updateItemBoxWindowAndContents()
279-
resetScrollPosition()
280-
tableView.reloadData()
281-
}
282-
283-
private func setupEventMonitor() {
284-
localEventMonitor = NSEvent.addLocalMonitorForEvents(
285-
matching: [.keyDown, .leftMouseDown, .rightMouseDown]
286-
) { [weak self] event in
287-
guard let self = self else { return event }
288-
289-
switch event.type {
290-
case .keyDown:
291-
switch event.keyCode {
292-
case 53: // Escape
293-
self.close()
294-
return nil
295-
case 125, 126: // Down/Up Arrow
296-
self.tableView.keyDown(with: event)
297-
if self.isVisible {
298-
return nil
299-
}
300-
return event
301-
case 124: // Right Arrow
302-
// handleRightArrow()
303-
self.close()
304-
return event
305-
case 123: // Left Arrow
306-
self.close()
307-
return event
308-
case 36, 48: // Return/Tab
309-
guard tableView.selectedRow >= 0 else { return event }
310-
let selectedItem = items[tableView.selectedRow]
311-
self.delegate?.applyCompletionItem(selectedItem)
312-
self.close()
313-
return nil
314-
default:
315-
return event
316-
}
317-
318-
case .leftMouseDown, .rightMouseDown:
319-
// If we click outside the window, close the window
320-
if !NSMouseInRect(NSEvent.mouseLocation, self.window!.frame, false) {
321-
self.close()
322-
}
323-
return event
324-
325-
default:
326-
return event
327-
}
328-
}
329-
}
330-
331-
private func handleRightArrow() {
332-
guard let window = self.window,
333-
let selectedRow = tableView.selectedRowIndexes.first,
334-
selectedRow < items.count,
335-
!popover.isShown else {
336-
return
337-
}
338-
let rowRect = tableView.rect(ofRow: selectedRow)
339-
let rowRectInWindow = tableView.convert(rowRect, to: nil)
340-
let popoverPoint = NSPoint(
341-
x: window.frame.maxX,
342-
y: window.frame.minY + rowRectInWindow.midY
343-
)
344-
popover.show(
345-
relativeTo: NSRect(x: popoverPoint.x, y: popoverPoint.y, width: 1, height: 1),
346-
of: window.contentView!,
347-
preferredEdge: .maxX
348-
)
349-
}
350-
150+
351151
/// Updates the item box window's height based on the number of items.
352152
/// If there are no items, the default label will be displayed instead.
353153
private func updateItemBoxWindowAndContents() {
@@ -385,13 +185,16 @@ public final class ItemBoxWindowController: NSWindowController {
385185
window.minSize = NSSize(width: Self.DEFAULT_SIZE.width, height: newHeight)
386186
}
387187

388-
@objc private func tableViewDoubleClick(_ sender: Any) {
389-
guard tableView.clickedRow >= 0 else { return }
390-
let selectedItem = items[tableView.clickedRow]
391-
delegate?.applyCompletionItem(selectedItem)
392-
self.close()
393-
}
394188

189+
private func configureNoItemsLabel() {
190+
window?.contentView?.addSubview(noItemsLabel)
191+
192+
NSLayoutConstraint.activate([
193+
noItemsLabel.centerXAnchor.constraint(equalTo: window!.contentView!.centerXAnchor),
194+
noItemsLabel.centerYAnchor.constraint(equalTo: window!.contentView!.centerYAnchor)
195+
])
196+
}
197+
395198
/// Calculate the window height for a given number of rows.
396199
private static func rowsToWindowHeight(for numberOfRows: CGFloat) -> CGFloat {
397200
let wholeRows = floor(numberOfRows)
@@ -405,29 +208,6 @@ public final class ItemBoxWindowController: NSWindowController {
405208

406209
return baseHeight + partialHeight + padding
407210
}
408-
409-
private func resetScrollPosition() {
410-
guard let clipView = scrollView.contentView as? NSClipView else { return }
411-
412-
// Scroll to the top of the content
413-
clipView.scroll(to: NSPoint(x: 0, y: -WINDOW_PADDING))
414-
415-
// Select the first item
416-
if !items.isEmpty {
417-
tableView.selectRowIndexes(IndexSet(integer: 0), byExtendingSelection: false)
418-
}
419-
}
420-
421-
private func removeEventMonitor() {
422-
if let monitor = localEventMonitor {
423-
NSEvent.removeMonitor(monitor)
424-
localEventMonitor = nil
425-
}
426-
}
427-
428-
deinit {
429-
removeEventMonitor()
430-
}
431211
}
432212

433213
extension ItemBoxWindowController: NSTableViewDataSource, NSTableViewDelegate {
@@ -452,44 +232,3 @@ extension ItemBoxWindowController: NSTableViewDataSource, NSTableViewDelegate {
452232
return true
453233
}
454234
}
455-
456-
private class NoSlotScroller: NSScroller {
457-
override class var isCompatibleWithOverlayScrollers: Bool { true }
458-
459-
override func drawKnobSlot(in slotRect: NSRect, highlight flag: Bool) {
460-
// Don't draw the knob slot (the background track behind the knob)
461-
}
462-
}
463-
464-
private class ItemBoxRowView: NSTableRowView {
465-
override func drawSelection(in dirtyRect: NSRect) {
466-
guard isSelected else { return }
467-
guard let context = NSGraphicsContext.current?.cgContext else { return }
468-
469-
context.saveGState()
470-
defer { context.restoreGState() }
471-
472-
// Create a rect that's inset from the edges and has proper padding
473-
// TODO: We create a new selectionRect instead of using dirtyRect
474-
// because there is a visual bug when holding down the arrow keys
475-
// to select the first or last item, which draws a clipped
476-
// rectangular highlight shape instead of the whole rectangle.
477-
// Replace this when it gets fixed.
478-
let selectionRect = NSRect(
479-
x: WINDOW_PADDING,
480-
y: 0,
481-
width: bounds.width - (WINDOW_PADDING * 2),
482-
height: bounds.height
483-
)
484-
let cornerRadius: CGFloat = 5
485-
let path = NSBezierPath(roundedRect: selectionRect, xRadius: cornerRadius, yRadius: cornerRadius)
486-
let selectionColor = NSColor.gray.withAlphaComponent(0.19)
487-
488-
context.setFillColor(selectionColor.cgColor)
489-
path.fill()
490-
}
491-
}
492-
493-
public protocol ItemBoxDelegate: AnyObject {
494-
func applyCompletionItem(_ item: CompletionItem)
495-
}

0 commit comments

Comments
 (0)