mirror of
https://github.com/ksherlock/ample.git
synced 2024-10-31 15:04:56 +00:00
886 lines
22 KiB
Objective-C
886 lines
22 KiB
Objective-C
//
|
|
// AutocompleteControl.m
|
|
// Autocomplete
|
|
//
|
|
// Created by Kelvin Sherlock on 2/20/2021.
|
|
// Copyright © 2021 Kelvin Sherlock. All rights reserved.
|
|
//
|
|
|
|
#import "AutocompleteControl.h"
|
|
#include <wctype.h>
|
|
|
|
|
|
/*
|
|
|
|
Todo --
|
|
- when there is a value, can filter the list by only including header items and the selected value
|
|
- draw inactive menu items
|
|
- when menu is hidden then text is manually deleted (not esc canceled), then down/up arrow the list needs to update.
|
|
- eliminate nib and do it manually.
|
|
- when menus is too tall, macos moves it to the top of the screen.
|
|
- 1. it's not moved someplace more appropriate when the size shrinks
|
|
- 2. it should display to the left or right in that case.
|
|
- need to know parent's frame.
|
|
|
|
- fuzzy search - minimum distance between letters?
|
|
*/
|
|
|
|
|
|
@interface ACMenuView : NSView
|
|
@property (nonatomic) NSArray<id<AutocompleteItem>> *items;
|
|
|
|
@property (weak) AutocompleteControl *parent;
|
|
|
|
-(void)reset;
|
|
-(void)reset: (id<AutocompleteItem>)value;
|
|
-(void)setItems:(NSArray<id<AutocompleteItem>> *)items;
|
|
-(void)setItems:(NSArray<id<AutocompleteItem>> *)items value: (id<AutocompleteItem>)value;
|
|
|
|
@end
|
|
|
|
|
|
@interface AutocompleteControl ()
|
|
{
|
|
IBOutlet NSPanel *_panel;
|
|
__weak IBOutlet ACMenuView *_menuView;
|
|
__weak IBOutlet NSScrollView *_scrollView;
|
|
|
|
id<AutocompleteItem> _value;
|
|
BOOL _editing;
|
|
BOOL _dirty;
|
|
}
|
|
|
|
@end
|
|
|
|
@interface AutocompleteControl (SearchField) <NSSearchFieldDelegate>
|
|
-(void)fixTextColor: (BOOL)editing;
|
|
@end
|
|
|
|
@implementation AutocompleteControl
|
|
|
|
-(void)_init {
|
|
|
|
[self setDelegate: self];
|
|
[self setPlaceholderString: @""];
|
|
[(NSSearchFieldCell *)[self cell] setSearchButtonCell: nil];
|
|
|
|
NSBundle *bundle = [NSBundle mainBundle];
|
|
NSNib *nib = [[NSNib alloc] initWithNibNamed: @"Autocomplete" bundle: bundle];
|
|
|
|
NSArray *topLevel = nil;
|
|
[nib instantiateWithOwner: self topLevelObjects: &topLevel];
|
|
|
|
[_panel setMovable: NO];
|
|
[_panel setBecomesKeyOnlyIfNeeded: YES];
|
|
[_menuView setParent: self];
|
|
}
|
|
|
|
-(id)initWithFrame:(NSRect)frameRect {
|
|
if ((self = [super initWithFrame: frameRect])) {
|
|
[self _init];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
-(id)initWithCoder:(NSCoder *)coder {
|
|
if ((self = [super initWithCoder: coder])) {
|
|
[self _init];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
#if 0
|
|
-(NSString *)stringValue {
|
|
return [super stringValue];
|
|
}
|
|
#endif
|
|
|
|
-(id)objectValue {
|
|
return _value;
|
|
}
|
|
|
|
-(void)setStringValue:(NSString *)stringValue {
|
|
[super setStringValue: stringValue ? stringValue : @""];
|
|
if (_value && [[_value menuTitle] isEqualToString: stringValue] == NO) {
|
|
// post change notification?
|
|
_value = nil;
|
|
[_menuView reset];
|
|
}
|
|
|
|
[self fixTextColor: _editing];
|
|
// todo -- search for a matching item, update text color.
|
|
}
|
|
|
|
// todo -- _menuView has second copy of value, need to update that.
|
|
-(void)setObjectValue:(id)objectValue {
|
|
if (_value == objectValue) return;
|
|
if (![objectValue conformsToProtocol: @protocol(AutocompleteItem)]) {
|
|
_value = nil;
|
|
[_menuView reset];
|
|
[super setStringValue: @""];
|
|
[self fixTextColor: _editing];
|
|
return;
|
|
}
|
|
_value = objectValue;
|
|
if (!_value) [super setStringValue: @""]; //
|
|
else {
|
|
[super setStringValue: [_value menuTitle]];
|
|
[_menuView reset: _value];
|
|
// TODO -- menu view currently uses text search.
|
|
//NSArray *array = [_autocompleteDelegate autocomplete: self completionsForItem: _value];
|
|
//[_menuView setItems: array value: _value];
|
|
}
|
|
[self fixTextColor: NO];
|
|
}
|
|
|
|
-(BOOL)valid {
|
|
return _value != nil;
|
|
}
|
|
|
|
|
|
-(void)hideSuggestions: (id)sender {
|
|
|
|
if (![_panel isVisible]) return;
|
|
|
|
NSWindow *window = [self window];
|
|
|
|
[window removeChildWindow: _panel];
|
|
[_panel orderOut: sender];
|
|
|
|
}
|
|
|
|
-(void)showSuggestions: (id)sender {
|
|
|
|
if ([_panel isVisible]) return;
|
|
NSWindow *window = [self window];
|
|
|
|
NSRect wFrame = [_panel frame];
|
|
NSRect vFrame = [self frame];
|
|
|
|
NSRect rect = { .origin = vFrame.origin, .size = wFrame.size };
|
|
rect = [window convertRectToScreen:rect];
|
|
|
|
rect.origin.y -= wFrame.size.height + 4;
|
|
rect.size.width = MAX(vFrame.size.width, _minWidth);
|
|
// todo - min width option.
|
|
[_panel setFrame: rect display: YES];
|
|
|
|
//[_panel setFrameOrigin: rect.origin];
|
|
[window addChildWindow: _panel ordered: NSWindowAbove];
|
|
}
|
|
|
|
-(void)updateSuggestions {
|
|
|
|
if (!_autocompleteDelegate) return;
|
|
NSString *needle = [self stringValue];
|
|
|
|
NSArray *items = [_autocompleteDelegate autocomplete: self completionsForString: needle];
|
|
|
|
[_menuView setItems: items];
|
|
if ([items count]) {
|
|
[self showSuggestions: nil];
|
|
} else {
|
|
[self hideSuggestions: nil];
|
|
}
|
|
}
|
|
|
|
-(void)invalidate {
|
|
|
|
if (!_autocompleteDelegate) return;
|
|
|
|
NSArray *items = nil;
|
|
|
|
/* if there is an object value, try to retain it. */
|
|
if (_value) {
|
|
|
|
[_menuView reset];
|
|
items = [_autocompleteDelegate autocomplete: self completionsForItem: _value];
|
|
if (items) {
|
|
[_menuView setItems: items];
|
|
return;
|
|
}
|
|
_value = nil;
|
|
[self invoke];
|
|
}
|
|
NSString *needle = [self stringValue];
|
|
|
|
if ([needle length]) {
|
|
_dirty = YES;
|
|
}
|
|
// if only 1 match, auto-set value?
|
|
items = [_autocompleteDelegate autocomplete: self completionsForString: needle];
|
|
[self fixTextColor: _editing];
|
|
[_menuView setItems: items];
|
|
}
|
|
|
|
|
|
// prevent action messages from the search field/cell.
|
|
-(BOOL)sendAction:(SEL)action to:(id)target {
|
|
if (action == [self action] && target == [self target]) return NO;
|
|
return [super sendAction: action to: target];
|
|
|
|
}
|
|
|
|
-(void)invoke {
|
|
_dirty = NO;
|
|
[super sendAction: [self action] to: [self target]];
|
|
}
|
|
|
|
@end
|
|
|
|
|
|
@implementation AutocompleteControl (SearchField)
|
|
|
|
-(void)fixTextColor: (BOOL)editing {
|
|
NSColor *color = editing || _value ? [NSColor controlTextColor] : [NSColor systemRedColor];
|
|
[self setTextColor: color];
|
|
}
|
|
|
|
- (void)controlTextDidChange:(NSNotification *)notification {
|
|
//NSLog(@"controlTextDidChange");
|
|
|
|
if (_value) {
|
|
_dirty = YES;
|
|
_value = nil;
|
|
}
|
|
|
|
NSString *s = [self stringValue];
|
|
if ([s length]) {
|
|
[self updateSuggestions];
|
|
} else {
|
|
_dirty = YES;
|
|
_value = nil;
|
|
[_menuView reset];
|
|
[_menuView setItems: nil];
|
|
[self hideSuggestions: nil];
|
|
[self invoke];
|
|
}
|
|
}
|
|
- (void)controlTextDidBeginEditing:(NSNotification *)obj {
|
|
//NSLog(@"controlTextDidBeginEditing");
|
|
|
|
_editing = YES;
|
|
_dirty = NO;
|
|
[self fixTextColor: YES];
|
|
}
|
|
|
|
|
|
|
|
- (void)controlTextDidEndEditing:(NSNotification *)obj {
|
|
//NSLog(@"controlTextDidEndEditing");
|
|
|
|
_editing = NO;
|
|
|
|
[self hideSuggestions: nil];
|
|
|
|
if (_dirty) {
|
|
_value = nil;
|
|
[self invoke];
|
|
}
|
|
[self fixTextColor: NO];
|
|
}
|
|
|
|
|
|
-(BOOL)control:(NSControl *)control textShouldBeginEditing:(NSText *)fieldEditor {
|
|
return YES;
|
|
}
|
|
|
|
-(BOOL)control:(NSControl *)control textShouldEndEditing:(NSText *)fieldEditor {
|
|
return YES;
|
|
}
|
|
|
|
-(void)selectItem:(id<AutocompleteItem>)item withSelector:(SEL)selector {
|
|
|
|
// for newline/mousedown, will still retain focus after updating
|
|
// so we need to invalidate the value if it's edited further.
|
|
if (selector == @selector(insertNewline:) || selector == @selector(mouseDown:) || selector == @selector(insertTab:)) {
|
|
|
|
_value = item;
|
|
NSString *str = [item menuTitle];
|
|
|
|
[super setStringValue: str];
|
|
|
|
[self hideSuggestions: nil];
|
|
|
|
NSText *fieldEditor = [self currentEditor];
|
|
//[fieldEditor setSelectedRange: NSMakeRange([str length], 0)];
|
|
[fieldEditor setSelectedRange: NSMakeRange(0, [str length])];
|
|
|
|
[self invoke];
|
|
|
|
// need to invalidate the menu so it reloads
|
|
|
|
|
|
#if 0
|
|
NSArray *array = [_autocompleteDelegate autocomplete: self completionsForItem: _value];
|
|
[_menuView setItems: array];
|
|
#else
|
|
[_menuView setItems: nil];
|
|
#endif
|
|
|
|
//NSLog(@"selectItem:withSelector:");
|
|
}
|
|
}
|
|
|
|
|
|
- (BOOL)control:(NSControl *)control textView:(NSTextView *)textView doCommandBySelector:(SEL)commandSelector {
|
|
|
|
if (commandSelector == @selector(moveUp:)) {
|
|
//[self showSuggestions: nil];
|
|
if ([_panel isVisible]) {
|
|
[_menuView moveUp: textView];
|
|
} else {
|
|
[self updateSuggestions];
|
|
}
|
|
return YES;
|
|
}
|
|
|
|
if (commandSelector == @selector(moveDown:)) {
|
|
//[self showSuggestions: nil];
|
|
if ([_panel isVisible]) {
|
|
[_menuView moveDown: textView];
|
|
} else {
|
|
[self updateSuggestions];
|
|
}
|
|
return YES;
|
|
}
|
|
|
|
if (commandSelector == @selector(insertNewline:)) {
|
|
if ([_panel isVisible])
|
|
[_menuView insertNewline: textView];
|
|
return YES;
|
|
}
|
|
|
|
if (commandSelector == @selector(insertTab:)) {
|
|
if ([_panel isVisible])
|
|
[_menuView insertTab: textView];
|
|
return NO;
|
|
}
|
|
|
|
if (commandSelector == @selector(complete:)) {
|
|
if ([_panel isVisible]) {
|
|
[self hideSuggestions: nil];
|
|
} else {
|
|
[self updateSuggestions];
|
|
}
|
|
return YES;
|
|
}
|
|
|
|
|
|
// esc ?
|
|
// if panel open, hide
|
|
// if panel closed, delete.
|
|
if (commandSelector == @selector(cancelOperation:)) {
|
|
|
|
if ([_panel isVisible]) {
|
|
[self hideSuggestions: nil];
|
|
} else {
|
|
_value = nil;
|
|
[super setStringValue: @""];
|
|
|
|
[self hideSuggestions: nil];
|
|
[_menuView reset];
|
|
//[_menuView setItems: _completions];
|
|
// sigh...
|
|
#if 0
|
|
NSArray *items =[_autocompleteDelegate autocomplete: self completionsForString: @""];
|
|
[_menuView setItems: items];
|
|
#endif
|
|
[self invoke];
|
|
}
|
|
return YES;
|
|
}
|
|
|
|
if (commandSelector == @selector(scrollPageDown:)) {
|
|
if ([_panel isVisible]) {
|
|
[_menuView scrollPageDown: textView];
|
|
} else {
|
|
[self updateSuggestions];
|
|
}
|
|
return YES;
|
|
}
|
|
if (commandSelector == @selector(scrollPageUp:)) {
|
|
if ([_panel isVisible]) {
|
|
[_menuView scrollPageUp: textView];
|
|
} else {
|
|
[self updateSuggestions];
|
|
}
|
|
return YES;
|
|
}
|
|
|
|
//NSLog(@"%@", NSStringFromSelector(commandSelector));
|
|
return NO;
|
|
}
|
|
|
|
|
|
@end
|
|
|
|
|
|
@interface ACMenuView () {
|
|
id<AutocompleteItem> _value;
|
|
NSInteger _index;
|
|
NSInteger _count;
|
|
NSArray<id<AutocompleteItem>> *_items;
|
|
NSTrackingArea *_trackingArea;
|
|
NSColor *_backgroundColor;
|
|
NSColor *_selectedColor;
|
|
BOOL _tracking;
|
|
BOOL _clipped;
|
|
}
|
|
@end
|
|
|
|
@implementation ACMenuView
|
|
|
|
#define MENU_FONT_SIZE 11
|
|
#define MENU_HEIGHT 14
|
|
#define MARGIN_TOP 0 //6
|
|
#define MARGIN_BOTTOM 0 //6
|
|
#define INDENT 7
|
|
#define HEADER_INDENT 7
|
|
#define MAX_DISPLAY_ITEMS 16
|
|
|
|
|
|
-(void)_init {
|
|
_backgroundColor = [NSColor windowBackgroundColor];
|
|
if (@available(macOS 10.14, *)) {
|
|
_selectedColor = [NSColor selectedContentBackgroundColor];
|
|
} else {
|
|
_selectedColor = [NSColor selectedTextBackgroundColor];
|
|
}
|
|
NSTrackingAreaOptions options = NSTrackingMouseMoved | NSTrackingMouseEnteredAndExited | NSTrackingInVisibleRect | NSTrackingActiveInActiveApp;
|
|
_trackingArea = [[NSTrackingArea alloc] initWithRect: NSZeroRect
|
|
options: options
|
|
owner: self
|
|
userInfo: nil];
|
|
|
|
[self addTrackingArea: _trackingArea];
|
|
}
|
|
|
|
-(id)initWithCoder:(NSCoder *)coder {
|
|
if ((self = [super initWithCoder: coder])) {
|
|
[self _init];
|
|
}
|
|
return self;
|
|
|
|
}
|
|
|
|
-(BOOL)isFlipped {
|
|
return YES;
|
|
}
|
|
|
|
-(BOOL)acceptsFirstMouse:(NSEvent *)event {
|
|
return YES;
|
|
}
|
|
|
|
static CGFloat HeightForItems(NSUInteger count) {
|
|
return count * MENU_HEIGHT;
|
|
}
|
|
|
|
-(NSSize)intrinsicContentSize {
|
|
return NSMakeSize(NSViewNoIntrinsicMetric, _count * MENU_HEIGHT + MARGIN_TOP + MARGIN_BOTTOM);
|
|
}
|
|
|
|
- (NSSize)sizeThatFits:(NSSize)size {
|
|
size.height = _count * MENU_HEIGHT + MARGIN_TOP + MARGIN_BOTTOM;
|
|
return size;
|
|
}
|
|
|
|
- (void)sizeToFit {
|
|
NSSize size = [self frame].size;
|
|
size.height = _count * MENU_HEIGHT + MARGIN_TOP + MARGIN_BOTTOM;
|
|
[self setFrameSize: size];
|
|
[self setNeedsDisplay: YES];
|
|
}
|
|
|
|
-(void)reset {
|
|
[self invalidateRow: _index];
|
|
_index = -1;
|
|
_value = nil;
|
|
_items = nil;
|
|
}
|
|
|
|
-(void)reset: (id<AutocompleteItem>)value {
|
|
[self invalidateRow: _index];
|
|
_index = -1;
|
|
_items = nil;
|
|
_value = value;
|
|
}
|
|
|
|
-(void)setItems:(NSArray *)items value: (id<AutocompleteItem> )value {
|
|
if (_items == items && _value == value) return;
|
|
_items = [items copy];
|
|
_index = -1;
|
|
_count = [items count];
|
|
_value = value;
|
|
|
|
if (!_items) {
|
|
_value = nil;
|
|
return;
|
|
}
|
|
|
|
// also check enabled status....
|
|
if (_value) {
|
|
_index = [_items indexOfObject: _value];
|
|
if (_index == NSNotFound) {
|
|
_index = -1;
|
|
_value = nil;
|
|
}
|
|
}
|
|
|
|
// if only 1 entry, auto-select it.
|
|
if (!_value) {
|
|
NSInteger count = -1;
|
|
for (id<AutocompleteItem> item in _items) {
|
|
++count;
|
|
if ([item menuIsHeader]) continue;
|
|
if (_value) {
|
|
_value = nil;
|
|
_index = -1;
|
|
break;
|
|
}
|
|
_value = item;
|
|
_index = count;
|
|
}
|
|
}
|
|
|
|
NSInteger displayCount = MIN(_count, MAX_DISPLAY_ITEMS);
|
|
CGFloat newHeight = HeightForItems(displayCount) + 8 ; // 4px top/bottom
|
|
NSWindow *window = [self window];
|
|
NSRect wFrame = [window frame];
|
|
|
|
NSRect contentRect = [[[self enclosingScrollView] contentView] frame];
|
|
|
|
//NSSize size = [self intrinsicContentSize];
|
|
//NSInteger minWidth = [_parent minWidth];
|
|
//size.width = MAX(wFrame.size.width, minWidth);
|
|
//size.height += 8;
|
|
|
|
CGFloat delta = wFrame.size.height - newHeight;
|
|
|
|
wFrame.origin.y += delta;
|
|
wFrame.size.height = newHeight;
|
|
|
|
_clipped = (_count > displayCount);
|
|
|
|
[self setFrameSize: NSMakeSize(contentRect.size.width /*- 15.0*/, HeightForItems(_count))];
|
|
[self setNeedsDisplay: YES];
|
|
[window setFrame: wFrame display: YES];
|
|
|
|
if (_value) {
|
|
[self scrollToRow: _index position: ScrollToCenter force: NO];
|
|
} else {
|
|
[self scrollToRow: 0 position: ScrollToTop force: YES];
|
|
}
|
|
|
|
//[self sizeToFit];
|
|
//[[self window] setContentSize: [self frame].size];
|
|
|
|
//NSLog(@"%@", NSStringFromRect(wFrame));
|
|
}
|
|
|
|
-(void)setItems:(NSArray<id<AutocompleteItem>> *)items {
|
|
|
|
if (_items == items) return;
|
|
[self setItems: items value: _value];
|
|
}
|
|
|
|
|
|
|
|
-(id<AutocompleteItem>)itemAtPoint: (NSPoint)point indexPtr: (NSInteger *)indexPtr {
|
|
|
|
NSInteger index = floor(point.y / MENU_HEIGHT);
|
|
if (index < 0 || index >= _count) return nil;
|
|
if (indexPtr) *indexPtr = index;
|
|
return [_items objectAtIndex: index];
|
|
}
|
|
|
|
enum {
|
|
ScrollToTop,
|
|
ScrollToBottom,
|
|
ScrollToCenter,
|
|
};
|
|
|
|
-(void)scrollToRow: (NSInteger)row position: (unsigned)position force: (BOOL)force {
|
|
if (row < 0) return;
|
|
if (!_clipped) return;
|
|
|
|
NSScrollView *scrollView = [self enclosingScrollView];
|
|
NSClipView *clipView = [scrollView contentView];
|
|
|
|
NSRect visibleRect = [self visibleRect];
|
|
if (!force) {
|
|
NSRect mRect = NSMakeRect(0, row * MENU_HEIGHT, 1 , MENU_HEIGHT);
|
|
if (NSContainsRect(visibleRect, mRect)) return;
|
|
}
|
|
|
|
NSInteger topRow = row;
|
|
switch (position) {
|
|
case ScrollToTop:
|
|
break;
|
|
case ScrollToBottom:
|
|
topRow -= MAX_DISPLAY_ITEMS -1;
|
|
break;
|
|
case ScrollToCenter:
|
|
topRow -= MAX_DISPLAY_ITEMS/2 - 1;
|
|
break;
|
|
}
|
|
if (topRow < 0) topRow = 0;
|
|
if (topRow > _count - MAX_DISPLAY_ITEMS)
|
|
topRow = _count - MAX_DISPLAY_ITEMS;
|
|
NSPoint point = NSMakePoint(0, topRow * MENU_HEIGHT);
|
|
|
|
//[self scrollClipView: clipView toPoint: point];
|
|
[clipView scrollToPoint: point];
|
|
[scrollView reflectScrolledClipView: clipView];
|
|
|
|
}
|
|
|
|
|
|
-(void)moveUp:(id)sender {
|
|
if (_count == 0 || _index <= 0) return;
|
|
|
|
NSInteger index = 0;
|
|
id<AutocompleteItem> value = nil;
|
|
for (index = _index - 1; index >= 0; --index) {
|
|
value = [_items objectAtIndex: index];
|
|
if ([value menuIsHeader]) continue;
|
|
if (![value menuEnabled]) continue;
|
|
break;
|
|
}
|
|
if (index < 0) return;
|
|
if (index == _index) return;
|
|
[self invalidateRow: _index];
|
|
[self invalidateRow: index];
|
|
|
|
_index = index;
|
|
_value = value;
|
|
[self scrollToRow: index position: ScrollToTop force: NO];
|
|
[_parent selectItem: _value withSelector: _cmd];
|
|
}
|
|
|
|
-(void)moveDown:(id)sender {
|
|
|
|
// _index -1 selects first item.
|
|
if (_count == 0 || _index == _count - 1) return;
|
|
|
|
NSInteger index = 0;
|
|
id<AutocompleteItem> value = nil;
|
|
for (index = _index + 1; index < _count ; ++index) {
|
|
value = [_items objectAtIndex: index];
|
|
if ([value menuIsHeader]) continue;
|
|
if (![value menuEnabled]) continue;
|
|
break;
|
|
}
|
|
if (index == _count) return;
|
|
if (index == _index) return;
|
|
[self invalidateRow: _index];
|
|
[self invalidateRow: index];
|
|
|
|
_index = index;
|
|
_value = value;
|
|
|
|
[self scrollToRow: index position: ScrollToBottom force: NO];
|
|
[_parent selectItem: _value withSelector: _cmd];
|
|
}
|
|
|
|
-(void)scrollPageUp:(id)sender {
|
|
if (_count == 0 || _index <= 0) return;
|
|
|
|
|
|
}
|
|
|
|
-(void)scrollPageDown:(id)sender {
|
|
if (_count == 0 || _index == _count - 1) return;
|
|
|
|
}
|
|
|
|
|
|
-(void)insertNewline:(id)sender {
|
|
if (_value) {
|
|
[_parent selectItem: _value withSelector: _cmd];
|
|
}
|
|
}
|
|
|
|
-(void)insertTab:(id)sender {
|
|
// if only one option, autocomplete?
|
|
if (_value) {
|
|
[_parent selectItem: _value withSelector: _cmd];
|
|
}
|
|
}
|
|
|
|
-(void)mouseMoved:(NSEvent *)event {
|
|
//NSLog(@"mouse moved");
|
|
|
|
if (!_tracking) return;
|
|
|
|
NSPoint p = [event locationInWindow];
|
|
p = [self convertPoint: p fromView: nil];
|
|
|
|
|
|
NSInteger index;
|
|
id<AutocompleteItem> value = [self itemAtPoint: p indexPtr: &index];
|
|
if (!value) return;
|
|
if (index == _index) return;
|
|
if ([value menuIsHeader]) return;
|
|
if (![value menuEnabled]) return;
|
|
|
|
[self invalidateRow: _index];
|
|
[self invalidateRow: index];
|
|
|
|
_index = index;
|
|
_value = value;
|
|
[_parent selectItem: _value withSelector: _cmd];
|
|
}
|
|
-(void)mouseDown:(NSEvent *)event {
|
|
if (!_tracking) return;
|
|
|
|
NSPoint p = [event locationInWindow];
|
|
p = [self convertPoint: p fromView: nil];
|
|
|
|
NSInteger index;
|
|
id<AutocompleteItem> value = [self itemAtPoint: p indexPtr: &index];
|
|
if (!value) return;
|
|
if (index != _index) {
|
|
if ([value menuIsHeader]) return;
|
|
if (![value menuEnabled]) return;
|
|
|
|
[self invalidateRow: _index];
|
|
[self invalidateRow: index];
|
|
|
|
_index = index;
|
|
_value = value;
|
|
}
|
|
|
|
[_parent selectItem: _value withSelector: _cmd];
|
|
}
|
|
|
|
-(void)mouseEntered:(NSEvent *)event {
|
|
//NSLog(@"mouse entered");
|
|
_tracking = YES;
|
|
|
|
}
|
|
-(void)mouseExited:(NSEvent *)event {
|
|
//NSLog(@"mouse exited");
|
|
|
|
_tracking = NO;
|
|
}
|
|
|
|
-(void)invalidateRow:(NSInteger)row {
|
|
if (row < 0 || row >= _count) return;
|
|
|
|
NSRect r = NSZeroRect;
|
|
NSRect bounds = [self bounds];
|
|
|
|
r.size.width = bounds.size.width;
|
|
r.size.height = MENU_HEIGHT;
|
|
r.origin.y = MENU_HEIGHT * row + MARGIN_TOP;
|
|
//NSLog(@"Invalidating %ld - %@", row, NSStringFromRect(r));
|
|
[self setNeedsDisplayInRect: r];
|
|
}
|
|
|
|
|
|
static void DrawString(NSString *str, NSDictionary *attr, CGRect rect) {
|
|
|
|
NSSize size = [str sizeWithAttributes: attr];
|
|
if (size.width <= rect.size.width) {
|
|
[str drawInRect: rect withAttributes: attr];
|
|
return;
|
|
}
|
|
NSMutableString *mstr = [str mutableCopy];
|
|
// binary search is probably the best way to handle it :/
|
|
NSInteger l = [mstr length];
|
|
while (l > 2) {
|
|
[mstr replaceCharactersInRange: NSMakeRange(l-2, 2) withString: @"…"];
|
|
--l;
|
|
size = [mstr sizeWithAttributes: attr];
|
|
if (size.width <= rect.size.width) {
|
|
[mstr drawInRect: rect withAttributes: attr];
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
-(void)drawItem: (id<AutocompleteItem>)item inRect: (NSRect)rect {
|
|
NSColor *textColor = [NSColor textColor];
|
|
if (!item) return;
|
|
|
|
if (item == _value) {
|
|
textColor = [NSColor selectedMenuItemTextColor];
|
|
[_selectedColor setFill];
|
|
NSRectFill(rect);
|
|
}
|
|
NSString *string = [item menuTitle];
|
|
|
|
if ([item menuIsHeader]) {
|
|
textColor = [NSColor secondaryLabelColor];
|
|
NSDictionary *attr = @{
|
|
NSForegroundColorAttributeName: textColor,
|
|
NSFontAttributeName: [NSFont systemFontOfSize: MENU_FONT_SIZE], // [NSFont boldSystemFontOfSize: 13],
|
|
};
|
|
NSRect r = NSInsetRect(rect, HEADER_INDENT, 0);
|
|
DrawString(string, attr, r);
|
|
} else {
|
|
|
|
NSDictionary *attr = @{
|
|
NSForegroundColorAttributeName: textColor,
|
|
NSFontAttributeName: [NSFont systemFontOfSize: MENU_FONT_SIZE],
|
|
};
|
|
NSRect r = NSInsetRect(rect, INDENT, 0);
|
|
r.origin.x += INDENT;
|
|
r.size.width -= INDENT;
|
|
DrawString(string, attr, r);
|
|
}
|
|
}
|
|
|
|
- (void)drawRect:(NSRect)dirtyRect {
|
|
[super drawRect:dirtyRect];
|
|
|
|
NSRect r = [self bounds];
|
|
NSInteger begin = floor((NSMinY(dirtyRect) - MARGIN_TOP) / MENU_HEIGHT);
|
|
NSInteger end = ceil((NSMaxY(dirtyRect) - MARGIN_TOP) / MENU_HEIGHT);
|
|
if (begin < 0) begin = 0;
|
|
if (end > _count) end = _count;
|
|
|
|
|
|
r.origin.y = MENU_HEIGHT * begin + MARGIN_TOP;
|
|
r.size.height = MENU_HEIGHT;
|
|
for (NSInteger index = begin; index < end; ++index) {
|
|
id<AutocompleteItem> item = [_items objectAtIndex: index];
|
|
[self drawItem: item inRect: r];
|
|
r.origin.y += MENU_HEIGHT;
|
|
}
|
|
}
|
|
|
|
@end
|
|
|
|
|
|
/* custom scroller that doesn't draw a background. */
|
|
@interface ACScroller : NSScroller
|
|
@end
|
|
|
|
@implementation ACScroller
|
|
|
|
|
|
-(void)drawRect:(NSRect)dirtyRect {
|
|
//[[NSColor windowBackgroundColor] set];
|
|
[[NSColor clearColor] set];
|
|
NSRectFill(dirtyRect);
|
|
[self drawKnob];
|
|
}
|
|
|
|
@end
|
|
|
|
|
|
@interface ACPanel : NSPanel
|
|
@end
|
|
|
|
@implementation ACPanel
|
|
|
|
/* needed to prevent the pop-up child window from being moved when offscreen. */
|
|
- (NSRect)constrainFrameRect:(NSRect)frameRect toScreen:(NSScreen *)screen {
|
|
return frameRect;
|
|
}
|
|
|
|
|
|
@end
|