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
433213extension 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