diff --git a/mac-app/Tip.xcodeproj/project.pbxproj b/mac-app/Tip.xcodeproj/project.pbxproj index df38793..aff9df9 100644 --- a/mac-app/Tip.xcodeproj/project.pbxproj +++ b/mac-app/Tip.xcodeproj/project.pbxproj @@ -27,6 +27,7 @@ F3A35D2D23D6301200982791 /* malformed_json_provider.rb in Resources */ = {isa = PBXBuildFile; fileRef = F3A35D2C23D6301200982791 /* malformed_json_provider.rb */; }; F3A35D3023D6344000982791 /* TipNoticeView.m in Sources */ = {isa = PBXBuildFile; fileRef = F3A35D2F23D6344000982791 /* TipNoticeView.m */; }; F3A35D3223D636ED00982791 /* empty_provider.rb in Resources */ = {isa = PBXBuildFile; fileRef = F3A35D3123D636EC00982791 /* empty_provider.rb */; }; + F3C248222470A6D3003EA099 /* WrappedTextView.m in Sources */ = {isa = PBXBuildFile; fileRef = F3C248212470A6D3003EA099 /* WrappedTextView.m */; }; F3E5B64B23BA846A002BEDD6 /* FontAwesome-Solid.otf in Resources */ = {isa = PBXBuildFile; fileRef = F3E5B64A23BA846A002BEDD6 /* FontAwesome-Solid.otf */; }; F3EA2538246A528000B21837 /* auto_execute_url_provider.rb in Resources */ = {isa = PBXBuildFile; fileRef = F3EA2537246A528000B21837 /* auto_execute_url_provider.rb */; }; FA0E180D23E6AD86006B4DD6 /* TipUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA0E180C23E6AD86006B4DD6 /* TipUITests.swift */; }; @@ -78,6 +79,8 @@ F3A35D2E23D6344000982791 /* TipNoticeView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TipNoticeView.h; sourceTree = ""; }; F3A35D2F23D6344000982791 /* TipNoticeView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TipNoticeView.m; sourceTree = ""; }; F3A35D3123D636EC00982791 /* empty_provider.rb */ = {isa = PBXFileReference; lastKnownFileType = text.script.ruby; path = empty_provider.rb; sourceTree = ""; }; + F3C248202470A6D3003EA099 /* WrappedTextView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WrappedTextView.h; sourceTree = ""; }; + F3C248212470A6D3003EA099 /* WrappedTextView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = WrappedTextView.m; sourceTree = ""; }; F3E5B64A23BA846A002BEDD6 /* FontAwesome-Solid.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "FontAwesome-Solid.otf"; sourceTree = ""; }; F3EA2537246A528000B21837 /* auto_execute_url_provider.rb */ = {isa = PBXFileReference; lastKnownFileType = text.script.ruby; path = auto_execute_url_provider.rb; sourceTree = ""; }; FA0E180723E6A7D3006B4DD6 /* TipUITests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "TipUITests-Bridging-Header.h"; sourceTree = ""; }; @@ -150,6 +153,8 @@ F3A35D2F23D6344000982791 /* TipNoticeView.m */, F357936223E66CD600031E09 /* AppDelegate.h */, F357936323E66CD600031E09 /* AppDelegate.m */, + F3C248202470A6D3003EA099 /* WrappedTextView.h */, + F3C248212470A6D3003EA099 /* WrappedTextView.m */, ); path = Tip; sourceTree = ""; @@ -299,6 +304,7 @@ F300998123B8159D0000B9E3 /* main.m in Sources */, F30CBFF123B9A154008D6B47 /* TipTableView.m in Sources */, F3A35D3023D6344000982791 /* TipNoticeView.m in Sources */, + F3C248222470A6D3003EA099 /* WrappedTextView.m in Sources */, F30099A623B816AF0000B9E3 /* Receiver.m in Sources */, F37DB94723D548E7002D799E /* ForTest.m in Sources */, ); diff --git a/mac-app/Tip/Info.plist b/mac-app/Tip/Info.plist index 2c25a48..86c8a42 100644 --- a/mac-app/Tip/Info.plist +++ b/mac-app/Tip/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.3.0-rc1 + 1.3.0-rc2 CFBundleURLTypes diff --git a/mac-app/Tip/TipItemTextField.h b/mac-app/Tip/TipItemTextField.h index 4b63133..2abd053 100644 --- a/mac-app/Tip/TipItemTextField.h +++ b/mac-app/Tip/TipItemTextField.h @@ -12,9 +12,6 @@ NS_ASSUME_NONNULL_BEGIN @interface TipItemTextField : NSTextField -@property (nonatomic, retain) NSLayoutConstraint* widthConstraint; -@property (nonatomic, retain) NSLayoutConstraint* heightConstraint; - @end NS_ASSUME_NONNULL_END diff --git a/mac-app/Tip/TipItemTextField.m b/mac-app/Tip/TipItemTextField.m index 3d85076..c52f360 100644 --- a/mac-app/Tip/TipItemTextField.m +++ b/mac-app/Tip/TipItemTextField.m @@ -7,34 +7,21 @@ // #import "TipItemTextField.h" +#import "VeritcallyAlignNSTextFieldCell.h" @implementation TipItemTextField -- (void) setStringValue:(NSString *)stringValue { - [super setStringValue:stringValue]; - - CGSize textSize = [self getTextSize:stringValue]; - - CGFloat width = textSize.width; - CGFloat height = MAX(16, textSize.height); - - self.frame = NSMakeRect(self.frame.origin.x, self.frame.origin.y, width, height); - - [self removeConstraint:_widthConstraint]; - [self removeConstraint:_heightConstraint]; - _widthConstraint = [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:width]; - _heightConstraint = [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:height]; - [self addConstraint:_widthConstraint]; - [self addConstraint:_heightConstraint]; - self.needsLayout = true; -} - -- (CGSize) getTextSize:(NSString *) text { - NSDictionary *attributes = [NSDictionary dictionaryWithObjectsAndKeys:self.font, NSFontAttributeName, nil]; - NSMutableAttributedString *as = [[NSMutableAttributedString alloc] initWithString:text attributes:attributes]; - CGSize size = [as size]; - size.width += 4; - return size; +- (instancetype)init { + if (self = [super init]) { + self.cell = [VeritcallyAlignNSTextFieldCell new]; + self.editable = NO; + self.selectable = YES; + self.bezeled = NO; + self.drawsBackground = NO; + self.font = [NSFont fontWithName:@"RobotoMono-Regular" size:12]; + self.lineBreakMode = NSLineBreakByTruncatingTail; + } + return self; } @end diff --git a/mac-app/Tip/TipNoticeView.h b/mac-app/Tip/TipNoticeView.h index d3df60d..93fa6c7 100644 --- a/mac-app/Tip/TipNoticeView.h +++ b/mac-app/Tip/TipNoticeView.h @@ -7,6 +7,7 @@ // #import +#import "TipItem.h" NS_ASSUME_NONNULL_BEGIN @@ -18,17 +19,15 @@ typedef NS_ENUM(NSInteger, TipNoticeViewAction) { @interface TipNoticeView : NSView -- (instancetype)initWithFrame:(NSRect)frame; -- (void) updateWithMessage:(NSString*) message icon:(UniChar)icon action:(TipNoticeViewAction)action; -@property NSTextField* textField; +@property NSTextView* textField; @property NSTextField* iconField; @property TipNoticeViewAction action; + +- (void) updateWithItems:(nonnull NSArray *)items andError:(NSException *) error; -@property (nonatomic, retain) NSLayoutConstraint* heightConstraint; -@property (nonatomic, retain) NSLayoutConstraint* widthConstraint; - +@property CGSize preferredSize; @end diff --git a/mac-app/Tip/TipNoticeView.m b/mac-app/Tip/TipNoticeView.m index 6562492..818bf42 100644 --- a/mac-app/Tip/TipNoticeView.m +++ b/mac-app/Tip/TipNoticeView.m @@ -8,60 +8,95 @@ #import "TipNoticeView.h" #import "VeritcallyAlignNSTextFieldCell.h" +#import "WrappedTextView.h" @implementation TipNoticeView -- (instancetype)initWithFrame:(NSRect)frame{ - if (self = [super initWithFrame:frame]) { - _iconField = [[NSTextField alloc] initWithFrame:frame]; - _iconField.cell.font = [NSFont fontWithName:@"Font Awesome 5 Free" size:14]; +- (instancetype)init{ + if (self = [super init]) { + self.translatesAutoresizingMaskIntoConstraints = NO; + + _iconField = [[NSTextField alloc] init]; + _iconField.identifier = @"iconField"; + _iconField.translatesAutoresizingMaskIntoConstraints = NO; + _iconField.cell.font = [NSFont fontWithName:@"Font Awesome 5 Free" size:16]; _iconField.editable = NO; _iconField.selectable = NO; _iconField.bezeled = NO; _iconField.drawsBackground = NO; _iconField.alignment = NSTextAlignmentCenter; [self addSubview:_iconField]; - - _textField = [[NSTextField alloc] init]; - _textField.cell = [VeritcallyAlignNSTextFieldCell new]; - _textField.cell.font = [NSFont fontWithName:@"RobotoMono-Regular" size:13]; + + _textField = [[WrappedTextView alloc] init]; + _textField.identifier = @"textField"; + _textField.translatesAutoresizingMaskIntoConstraints = NO; + _textField.font = [NSFont fontWithName:@"RobotoMono-Regular" size:13]; _textField.editable = NO; _textField.selectable = NO; - _textField.bezeled = NO; _textField.drawsBackground = NO; _textField.alignment = NSTextAlignmentLeft; [self addSubview:_textField]; + + NSDictionary *viewDict = NSDictionaryOfVariableBindings(_iconField, _textField); + [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-3-[_iconField]-1-[_textField]-2-|" options:0 metrics:nil views:viewDict]]; + [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-5-[_iconField]-(>=0)-|" options:0 metrics:nil views:viewDict]]; + [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-2-[_textField]-5-|" options:0 metrics:nil views:viewDict]]; + + [_iconField setContentCompressionResistancePriority:NSLayoutPriorityRequired forOrientation:NSLayoutConstraintOrientationVertical]; + [_iconField setContentCompressionResistancePriority:NSLayoutPriorityRequired forOrientation:NSLayoutConstraintOrientationHorizontal]; + + [_textField setContentCompressionResistancePriority:NSLayoutPriorityRequired forOrientation:NSLayoutConstraintOrientationVertical]; + [_textField setContentCompressionResistancePriority:NSLayoutPriorityRequired forOrientation:NSLayoutConstraintOrientationHorizontal]; } return self; } +- (void) updateWithItems:(NSArray *)items andError:(NSException *)error { + if (error) { + NSString *message = nil; + TipNoticeViewAction action = TipNoticeViewActionNone; + + if ([error.name isEqualToString:@"MalformedJsonException"]) { + message = @"Malformed JSON returned from provider. Click to see logs in Console. You'll need to set the filter Process=Tip."; + action = TipNoticeViewActionOpenConsole; + } else if ([error.name isEqualToString:@"ProviderNotExistException"]) { + message = [NSString stringWithFormat:@"%@ doesn't exist. Please make a provider script. Click to see instruction.", [error.userInfo objectForKey:@"provider"]]; + action = TipNoticeViewActionOpenProviderInstruction; + } else if ([error.name isEqualToString:@"ProviderNotExecutableException"]) { + message = [NSString stringWithFormat:@"Provider isn't executable. Please chmod 755 %@", [error.userInfo objectForKey:@"provider"]]; + } else { + message = @"Error occurred. Click to see logs in Console. You'll need to set the filter Process=Tip."; + action = TipNoticeViewActionOpenConsole; + } + + [self updateWithMessage:message + icon:0xf06a + action:action]; + } else if (items.count == 0) { + [self updateWithMessage:@"No tips. You can add tips through your provider script. Click to see the instruction." + icon:0xf59a + action:TipNoticeViewActionOpenProviderInstruction]; + } else { + [self updateWithMessage:@"No error." + icon:0xf06a + action:TipNoticeViewActionOpenConsole]; + } +} + - (void) updateWithMessage:(NSString*)message icon:(UniChar)icon action:(TipNoticeViewAction)action { self.action = action; - _textField.stringValue = message; - + _textField.string = message; _iconField.stringValue = [NSString stringWithFormat:@"%C", icon]; - - NSSize size = [_textField.cell cellSizeForBounds:NSMakeRect(0, 0, self.frame.size.width - 40, FLT_MAX)]; - _textField.frame = NSMakeRect( - self.frame.origin.x + 30, - self.frame.origin.y + 8, - size.width, - size.height); - _iconField.frame = NSMakeRect(self.frame.origin.x + 5, size.height + 10 - 30, 25, 25); - self.frame = NSMakeRect(self.frame.origin.x, self.frame.origin.y, self.frame.size.width, size.height + 15); - - [self removeConstraint:_widthConstraint]; - [self removeConstraint:_heightConstraint]; - - _widthConstraint = [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:self.frame.size.width]; - _heightConstraint = [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:self.frame.size.height]; - [self addConstraint:_widthConstraint]; - [self addConstraint:_heightConstraint]; - self.needsLayout = YES; + + _preferredSize = CGSizeMake(3 + _iconField.intrinsicContentSize.width + 1 + _textField.intrinsicContentSize.width + 2, 2 + _textField.intrinsicContentSize.height + 5); +} + +- (NSSize)intrinsicContentSize { + return _preferredSize; } - (void)resetCursorRects { diff --git a/mac-app/Tip/TipTableController.h b/mac-app/Tip/TipTableController.h index 0d56a1b..6a7824b 100644 --- a/mac-app/Tip/TipTableController.h +++ b/mac-app/Tip/TipTableController.h @@ -15,15 +15,16 @@ NS_ASSUME_NONNULL_BEGIN @interface TipTableController : NSViewController -@property (nonatomic) NSArray* items; -@property (nonatomic) NSException* error; @property TipNoticeView* noticeView; @property TipTableView* table; -@property NSTableColumn* iconColumn; -@property NSTableColumn* textColumn; + +@property (nonatomic) NSException* error; +@property (nonatomic) NSArray* items; - (void) performAction:(NSUInteger)row; +- (void)setItems:(nonnull NSArray *)items; + @end NS_ASSUME_NONNULL_END diff --git a/mac-app/Tip/TipTableController.m b/mac-app/Tip/TipTableController.m index 3897a1c..79bfda9 100644 --- a/mac-app/Tip/TipTableController.m +++ b/mac-app/Tip/TipTableController.m @@ -6,253 +6,77 @@ // Copyright © 2019 Tanin Na Nakorn. All rights reserved. // -#import "AppDelegate.h" +#include + #import "TipTableController.h" #import "TipTableView.h" -#import "TipItemTextField.h" -#import "VeritcallyAlignNSTextFieldCell.h" -#include #import "TipNoticeView.h" @implementation TipTableController - (instancetype)init { if (self = [super init]) { - _items = [[NSMutableArray alloc] init]; self.view = [[NSView alloc] init]; - self.view.translatesAutoresizingMaskIntoConstraints = NO; - - _noticeView = [[TipNoticeView alloc] initWithFrame:CGRectMake(0, 0, 350, 10)]; - _noticeView.translatesAutoresizingMaskIntoConstraints = NO; - _noticeView.hidden = YES; + + _noticeView = [[TipNoticeView alloc] init]; [self.view addSubview:_noticeView]; NSDictionary *noticeViewDict = NSDictionaryOfVariableBindings(_noticeView); - [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-2-[_noticeView]-2-|" options:0 metrics:nil views:noticeViewDict]]; - [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-2-[_noticeView]-2-|" options:0 metrics:nil views:noticeViewDict]]; + [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(>=2)-[_noticeView]-(>=2)-|" options:0 metrics:nil views:noticeViewDict]]; + [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-(>=2)-[_noticeView]-(>=2)-|" options:0 metrics:nil views:noticeViewDict]]; + [_noticeView setContentCompressionResistancePriority:NSLayoutPriorityRequired forOrientation:NSLayoutConstraintOrientationVertical]; + [_noticeView setContentCompressionResistancePriority:NSLayoutPriorityRequired forOrientation:NSLayoutConstraintOrientationHorizontal]; + [_noticeView setContentHuggingPriority:NSLayoutPriorityRequired forOrientation:NSLayoutConstraintOrientationVertical]; + [_noticeView setContentHuggingPriority:NSLayoutPriorityRequired forOrientation:NSLayoutConstraintOrientationHorizontal]; - _table = [[TipTableView alloc] initWithFrame:CGRectMake(0, 0, 10, 10)]; - _table.translatesAutoresizingMaskIntoConstraints = NO; - _table.focusRingType = NSFocusRingTypeNone; - _table.dataSource = self; - _table.delegate = self; - _table.cell.bordered = NO; - _table.allowsMultipleSelection = NO; - _table.action = @selector(clickRow:); - _table.cell.bezeled = NO; - _table.enterPressedAction = @selector(pressEnter); - _table.target = self; - _table.intercellSpacing = CGSizeMake(0, 0); - _table.hidden = YES; - _table.usesAutomaticRowHeights = YES; - - _iconColumn = [[NSTableColumn alloc] initWithIdentifier:@"icon"]; - _iconColumn.width = 18; - _textColumn = [[NSTableColumn alloc] initWithIdentifier:@"text"]; - _textColumn.width = 25; - [_table addTableColumn:_iconColumn]; - [_table addTableColumn:_textColumn]; + _table = [[TipTableView alloc] init]; [self.view addSubview:_table]; NSDictionary *tableDict = NSDictionaryOfVariableBindings(_table); - [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-0-[_table(>=10)]-0-|" options:0 metrics:nil views:tableDict]]; - [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-0-[_table(>=10)]-(-1)-|" options:0 metrics:nil views:tableDict]]; + [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-0-[_table]-0-|" options:0 metrics:nil views:tableDict]]; + [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-0-[_table]-0-|" options:0 metrics:nil views:tableDict]]; + [_table setContentCompressionResistancePriority:NSLayoutPriorityRequired forOrientation:NSLayoutConstraintOrientationVertical]; + [_table setContentCompressionResistancePriority:NSLayoutPriorityRequired forOrientation:NSLayoutConstraintOrientationHorizontal]; + [_table setContentHuggingPriority:NSLayoutPriorityRequired forOrientation:NSLayoutConstraintOrientationVertical]; + [_table setContentHuggingPriority:NSLayoutPriorityRequired forOrientation:NSLayoutConstraintOrientationHorizontal]; } return self; } - (void) viewDidAppear { [super viewDidAppear]; - if (_items.count > 0) { - [_table selectRowIndexes:[NSIndexSet indexSetWithIndex:0] byExtendingSelection:NO]; - } + [_table selectFirstRow]; } -- (void) clickRow:(id)sender { - [self performAction:_table.clickedRow]; -} - -- (void) pressEnter { - if (_table.selectedRow >= 0) { - [self performAction:_table.selectedRow]; - } +- (void) performAction:(NSUInteger)row { + [_table performAction:row]; } - (void) setError:(NSException*)error { _error = error; - _items = nil; - [self update]; + [self updateWithItems:[[NSMutableArray alloc] init] andError:error]; } +- (NSArray*)items { + return _table.items; +} -- (void)setItems:(NSArray *)items { - _items = items; - _error = nil; - [self update]; - [_table reloadData]; +- (void)setItems:(nonnull NSArray *)items { + [self updateWithItems:items andError:nil]; } -- (void)update { - if (_error) { - _noticeView.hidden = NO; - _table.hidden = YES; - - NSString *message = nil; - TipNoticeViewAction action = TipNoticeViewActionNone; - - if ([_error.name isEqualToString:@"MalformedJsonException"]) { - message = @"Malformed JSON returned from provider. Click to see logs in Console. You'll need to set the filter Process=Tip."; - action = TipNoticeViewActionOpenConsole; - } else if ([_error.name isEqualToString:@"ProviderNotExistException"]) { - message = [NSString stringWithFormat:@"%@ doesn't exist. Please make a provider script. Click to see instruction.", [_error.userInfo objectForKey:@"provider"]]; - action = TipNoticeViewActionOpenProviderInstruction; - } else if ([_error.name isEqualToString:@"ProviderNotExecutableException"]) { - message = [NSString stringWithFormat:@"Provider isn't executable. Please chmod 755 %@", [_error.userInfo objectForKey:@"provider"]]; - } else { - message = @"Error occurred. Click to see logs in Console. You'll need to set the filter Process=Tip."; - action = TipNoticeViewActionOpenConsole; - } - - [_noticeView updateWithMessage:message - icon:0xf06a - action:action]; - self.preferredContentSize = _noticeView.frame.size; - } else if (_items.count == 0) { +- (void)updateWithItems:(nonnull NSArray *)items andError:(NSException*)error { + _table.items = items; + [_noticeView updateWithItems:items andError:error]; + + if (error || items.count == 0) { _noticeView.hidden = NO; _table.hidden = YES; - [_noticeView updateWithMessage:@"No tips. You can add tips through your provider script. Click to see the instruction." - icon:0xf59a - action:TipNoticeViewActionOpenProviderInstruction]; - self.preferredContentSize = _noticeView.frame.size; } else { _noticeView.hidden = YES; _table.hidden = NO; - - NSTextField* textField = [self makeTextField]; - - CGFloat textFieldWidth = 0; - CGFloat height = 0; - for (TipItem* item in _items) { - textField.stringValue = item.label; - textFieldWidth = MAX(textFieldWidth, textField.frame.size.width); - height += textField.frame.size.height; - } - - self.preferredContentSize = CGSizeMake(17 + textFieldWidth, height); - } -} - -- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView { - return _items.count; -} - -- (void) performAction:(NSUInteger) row { - TipItem* item = [_items objectAtIndex:row]; - - if (item.type == TipItemTypeUrl) { - [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:item.value]]; - [self hide]; - } else { - NSTableRowView* rowView = [_table rowViewAtRow:row makeIfNecessary:false]; - NSTextField* iconText = ((NSView*)[rowView viewAtColumn:0]).subviews.firstObject; - - [NSAnimationContext runAnimationGroup:^(NSAnimationContext *context) { - context.duration = 0.1; - iconText.animator.alphaValue = 0; - } - completionHandler:^{ - iconText.alphaValue = 1; - iconText.stringValue = @"\uf46c"; - - [self performSelector:@selector(hide) - withObject:nil - afterDelay:0.15]; - }]; - - NSPasteboard *pasteboard = [NSPasteboard generalPasteboard]; - [pasteboard clearContents]; - [pasteboard setString:item.value forType:NSPasteboardTypeString]; } } -- (void) hide { - [AppDelegate hide]; -} - -- (NSTextField*)makeTextField { - NSTextField* textField = [[TipItemTextField alloc] initWithFrame:NSMakeRect(0, 0, 1, 1)]; - textField.cell = [VeritcallyAlignNSTextFieldCell new]; - textField.editable = NO; - textField.selectable = YES; - textField.bezeled = NO; - textField.drawsBackground = NO; - textField.font = [NSFont fontWithName:@"RobotoMono-Regular" size:12]; - textField.lineBreakMode = NSLineBreakByTruncatingTail; - return textField; -} - -- (nullable NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(nullable NSTableColumn *)tableColumn row:(NSInteger)row { - NSView* result = [tableView makeViewWithIdentifier:tableColumn.identifier owner:self]; - - TipItem* item = [_items objectAtIndex:row]; - - if (!item) { return nil; } - - - if (tableColumn == _iconColumn) { - NSView* iconCol = result; - if (iconCol == nil) { - iconCol = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, 14, 14)]; - iconCol.translatesAutoresizingMaskIntoConstraints = NO; - - NSTextField* icon = [[NSTextField alloc] initWithFrame:NSMakeRect(0, 0, 14, 14)]; - icon.translatesAutoresizingMaskIntoConstraints = NO; - icon.cell = [VeritcallyAlignNSTextFieldCell new]; - icon.cell.font = [NSFont fontWithName:@"Font Awesome 5 Free" size:11]; - icon.editable = NO; - icon.selectable = NO; - icon.bezeled = NO; - icon.drawsBackground = NO; - - [iconCol addSubview:icon]; - - NSDictionary *iconTextDict = NSDictionaryOfVariableBindings(icon); - [iconCol addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-3-[icon]-0-|" options:0 metrics:nil views:iconTextDict]]; - [iconCol addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-3-[icon]->=2-|" options:0 metrics:nil views:iconTextDict]]; - } - - NSTextField* iconText = iconCol.subviews.firstObject; - if (item.type == TipItemTypeUrl) { - iconText.stringValue = @"\uf35d"; - } else { - iconText.stringValue = @"\uf0c5"; - } - return iconCol; - } else if (tableColumn == _textColumn) { - NSView* textCol = result; - - if (textCol == nil) { - textCol = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, 10, 10)]; - textCol.translatesAutoresizingMaskIntoConstraints = NO; - - NSTextField* text = [self makeTextField]; - text.translatesAutoresizingMaskIntoConstraints = NO; - [textCol addSubview:text]; - - NSDictionary *textDict = NSDictionaryOfVariableBindings(text); - [textCol addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-0-[text]-0-|" options:0 metrics:nil views:textDict]]; - [textCol addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-1-[text]-4-|" options:0 metrics:nil views:textDict]]; - } - - NSTextField* textField = textCol.subviews.firstObject; - textField.stringValue = item.label; - return textCol; - } else { - @throw [NSException - exceptionWithName:@"Exception" - reason:[NSString stringWithFormat:@"Invalid column: %@", tableColumn.identifier] - userInfo:nil]; - } -} @end diff --git a/mac-app/Tip/TipTableView.h b/mac-app/Tip/TipTableView.h index c626736..629af29 100644 --- a/mac-app/Tip/TipTableView.h +++ b/mac-app/Tip/TipTableView.h @@ -7,12 +7,24 @@ // #import +#import "TipItem.h" NS_ASSUME_NONNULL_BEGIN -@interface TipTableView : NSTableView +@interface TipTableView : NSTableView -@property (nullable) SEL enterPressedAction; +@property (nonatomic) NSArray* items; + +@property NSTableColumn* iconColumn; +@property NSTableColumn* textColumn; + +@property CGSize preferredSize; + +- (void) selectFirstRow; + +- (void) performAction:(NSUInteger)row; + +- (void) recomputePreferredSize; @end diff --git a/mac-app/Tip/TipTableView.m b/mac-app/Tip/TipTableView.m index 0cb843b..22facf1 100644 --- a/mac-app/Tip/TipTableView.m +++ b/mac-app/Tip/TipTableView.m @@ -7,22 +7,199 @@ // #import "TipTableView.h" +#import "TipItemTextField.h" +#import "AppDelegate.h" +#import "VeritcallyAlignNSTextFieldCell.h" @implementation TipTableView -- (void)keyUp:(NSEvent *)event -{ +NSTextField *textFieldForSizing; + +- (instancetype)init { + if (self = [super init]) { + _items = [[NSMutableArray alloc] init]; + self.translatesAutoresizingMaskIntoConstraints = NO; + self.focusRingType = NSFocusRingTypeNone; + self.cell.bordered = NO; + self.allowsMultipleSelection = NO; + self.allowsColumnResizing = NO; + self.cell.bezeled = NO; + self.intercellSpacing = CGSizeMake(0, 0); + self.hidden = YES; + self.usesAutomaticRowHeights = YES; + self.headerView = nil; + self.cornerView = nil; + + self.dataSource = self; + self.delegate = self; + self.target = self; + self.action = @selector(clickRow:); + + _iconColumn = [[NSTableColumn alloc] initWithIdentifier:@"icon"]; + _iconColumn.width = 18; + _textColumn = [[NSTableColumn alloc] initWithIdentifier:@"text"]; + [self addTableColumn:_iconColumn]; + [self addTableColumn:_textColumn]; + + textFieldForSizing = [[TipItemTextField alloc] init]; + } + return self; +} + +- (void) selectFirstRow { + if (_items && _items.count > 0) { + [self selectRowIndexes:[NSIndexSet indexSetWithIndex:0] byExtendingSelection:NO]; + } +} + +- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView { + return _items.count; +} + +- (void) performAction:(NSUInteger) row { + TipItem* item = [_items objectAtIndex:row]; + + if (item.type == TipItemTypeUrl) { + [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:item.value]]; + [self hide]; + } else { + NSTableRowView* rowView = [self rowViewAtRow:row makeIfNecessary:false]; + NSTextField* iconText = ((NSView*)[rowView viewAtColumn:0]).subviews.firstObject; + + [NSAnimationContext runAnimationGroup:^(NSAnimationContext *context) { + context.duration = 0.1; + iconText.animator.alphaValue = 0; + } + completionHandler:^{ + iconText.alphaValue = 1; + iconText.stringValue = @"\uf46c"; + + [self performSelector:@selector(hide) + withObject:nil + afterDelay:0.15]; + }]; + + NSPasteboard *pasteboard = [NSPasteboard generalPasteboard]; + [pasteboard clearContents]; + [pasteboard setString:item.value forType:NSPasteboardTypeString]; + } +} + +- (void)setItems:(NSArray *)items { + _items = items; + [self recomputePreferredSize]; + [self reloadData]; +} + +- (void) recomputePreferredSize { + CGSize newSize = CGSizeMake(0, 0); + for (TipItem* item in _items) { + textFieldForSizing.stringValue = item.label; + newSize.width = MAX(newSize.width, textFieldForSizing.intrinsicContentSize.width); + newSize.height += textFieldForSizing.intrinsicContentSize.height + 1 + 4; + } + + newSize.width += _iconColumn.width + 6; + _preferredSize = newSize; + [self invalidateIntrinsicContentSize]; +} + +- (void) hide { + [AppDelegate hide]; +} + +- (void)keyUp:(NSEvent *)event { if (event.keyCode == 36 || event.keyCode == 76) { - if (_enterPressedAction) { - IMP imp = [self.target methodForSelector:_enterPressedAction]; - void (*func)(id, SEL) = (void *) imp; - func(self.target, _enterPressedAction); + if (self.selectedRow >= 0) { + [self performAction:self.selectedRow]; } } } +- (void) clickRow:(id)sender { + [self performAction:self.clickedRow]; +} + - (void)resetCursorRects { [self addCursorRect:self.bounds cursor:[NSCursor pointingHandCursor]]; } +- (NSSize)intrinsicContentSize { + return _preferredSize; +} + +- (nullable NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(nullable NSTableColumn *)tableColumn row:(NSInteger)row { + NSView* result = [tableView makeViewWithIdentifier:tableColumn.identifier owner:self]; + + TipItem* item = [_items objectAtIndex:row]; + + if (!item) { return nil; } + + + if (tableColumn == _iconColumn) { + NSView* iconCol = result; + if (iconCol == nil) { + iconCol = [[NSView alloc] init]; + iconCol.identifier = @"iconCol"; + + NSTextField* icon = [[NSTextField alloc] init]; + icon.identifier = @"iconText"; + icon.translatesAutoresizingMaskIntoConstraints = NO; + icon.cell = [VeritcallyAlignNSTextFieldCell new]; + icon.cell.font = [NSFont fontWithName:@"Font Awesome 5 Free" size:11]; + icon.editable = NO; + icon.selectable = NO; + icon.bezeled = NO; + icon.drawsBackground = NO; + + [iconCol addSubview:icon]; + + NSDictionary *iconTextDict = NSDictionaryOfVariableBindings(icon); + [iconCol addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-3-[icon]-3-|" options:0 metrics:nil views:iconTextDict]]; + [iconCol addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-3-[icon]->=2-|" options:0 metrics:nil views:iconTextDict]]; + [icon setContentCompressionResistancePriority:NSLayoutPriorityRequired forOrientation:NSLayoutConstraintOrientationVertical]; + [icon setContentCompressionResistancePriority:NSLayoutPriorityRequired forOrientation:NSLayoutConstraintOrientationHorizontal]; + [icon setContentHuggingPriority:NSLayoutPriorityRequired forOrientation:NSLayoutConstraintOrientationVertical]; + [icon setContentHuggingPriority:NSLayoutPriorityRequired forOrientation:NSLayoutConstraintOrientationHorizontal]; + } + + NSTextField* iconText = iconCol.subviews.firstObject; + if (item.type == TipItemTypeUrl) { + iconText.stringValue = @"\uf35d"; + } else { + iconText.stringValue = @"\uf0c5"; + } + [iconCol invalidateIntrinsicContentSize]; + return iconCol; + } else if (tableColumn == _textColumn) { + NSView* textCol = result; + + if (textCol == nil) { + textCol = [[NSView alloc] init]; + + NSTextField* text = [[TipItemTextField alloc] init]; + text.translatesAutoresizingMaskIntoConstraints = NO; + [textCol addSubview:text]; + + NSDictionary *textDict = NSDictionaryOfVariableBindings(text); + [textCol addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-0-[text]-0-|" options:0 metrics:nil views:textDict]]; + [textCol addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-1-[text]-4-|" options:0 metrics:nil views:textDict]]; + [text setContentCompressionResistancePriority:NSLayoutPriorityRequired forOrientation:NSLayoutConstraintOrientationVertical]; + [text setContentCompressionResistancePriority:NSLayoutPriorityRequired forOrientation:NSLayoutConstraintOrientationHorizontal]; + [text setContentHuggingPriority:NSLayoutPriorityRequired forOrientation:NSLayoutConstraintOrientationVertical]; + [text setContentHuggingPriority:NSLayoutPriorityRequired forOrientation:NSLayoutConstraintOrientationHorizontal]; + } + + NSTextField* textField = textCol.subviews.firstObject; + textField.stringValue = item.label; + [textCol invalidateIntrinsicContentSize]; + return textCol; + } else { + @throw [NSException + exceptionWithName:@"Exception" + reason:[NSString stringWithFormat:@"Invalid column: %@", tableColumn.identifier] + userInfo:nil]; + } +} + @end diff --git a/mac-app/Tip/WrappedTextView.h b/mac-app/Tip/WrappedTextView.h new file mode 100644 index 0000000..d8ec3fc --- /dev/null +++ b/mac-app/Tip/WrappedTextView.h @@ -0,0 +1,19 @@ +// +// WrappedTextField.h +// Tip +// +// Created by Tanin Na Nakorn on 5/16/20. +// Copyright © 2020 Tanin Na Nakorn. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface WrappedTextView : NSTextView + +@property CGSize preferredSize; + +@end + +NS_ASSUME_NONNULL_END diff --git a/mac-app/Tip/WrappedTextView.m b/mac-app/Tip/WrappedTextView.m new file mode 100644 index 0000000..79f6658 --- /dev/null +++ b/mac-app/Tip/WrappedTextView.m @@ -0,0 +1,27 @@ +// +// WrappedTextField.m +// Tip +// +// Created by Tanin Na Nakorn on 5/16/20. +// Copyright © 2020 Tanin Na Nakorn. All rights reserved. +// + +#import "WrappedTextView.h" + +@implementation WrappedTextView + +- (void)setString:(NSString *)string { + [super setString:string]; + + self.textContainer.size = NSMakeSize(250, FLT_MAX); + [self.layoutManager glyphRangeForTextContainer:self.textContainer]; + + NSSize size = [self.layoutManager usedRectForTextContainer:self.textContainer].size; + _preferredSize = CGSizeMake(size.width, size.height); +} + +- (NSSize)intrinsicContentSize { + return _preferredSize; +} + +@end