diff --git a/src/context.cc b/src/context.cc index 0775d5684d..34db0292f2 100644 --- a/src/context.cc +++ b/src/context.cc @@ -346,6 +346,7 @@ error Context::save(const bool push_changes) { UIElements UIElements::Reset() { UIElements render; + render.first_load = true; render.display_time_entries = true; render.display_time_entry_autocomplete = true; render.display_mini_timer_autocomplete = true; @@ -494,10 +495,6 @@ void Context::updateUI(const UIElements &what) { view::TimeEntry editor_time_entry_view; - std::vector time_entry_autocompletes; - std::vector minitimer_autocompletes; - std::vector project_autocompletes; - bool use_proxy(false); bool record_timeline(false); Poco::Int64 unsynced_item_count(0); @@ -518,10 +515,6 @@ void Context::updateUI(const UIElements &what) { { Poco::Mutex::ScopedLock lock(user_m_); - if (what.display_project_autocomplete && user_) { - user_->related.ProjectAutocompleteItems(&project_autocompletes); - } - if (what.display_time_entry_editor && user_) { TimeEntry *editor_time_entry = user_->related.TimeEntryByGUID(what.time_entry_editor_guid); @@ -585,15 +578,6 @@ void Context::updateUI(const UIElements &what) { } } - if (what.display_time_entry_autocomplete && user_) { - user_->related.TimeEntryAutocompleteItems( - &time_entry_autocompletes); - } - - if (what.display_mini_timer_autocomplete && user_) { - user_->related.MinitimerAutocompleteItems(&minitimer_autocompletes); - } - if (what.display_workspace_select && user_) { std::vector workspaces; user_->related.WorkspaceList(&workspaces); @@ -872,11 +856,27 @@ void Context::updateUI(const UIElements &what) { } if (what.display_time_entry_autocomplete) { - UI()->DisplayTimeEntryAutocomplete(&time_entry_autocompletes); + if (what.first_load) { + std::vector time_entry_autocompletes; + user_->related.TimeEntryAutocompleteItems(&time_entry_autocompletes); + UI()->DisplayTimeEntryAutocomplete(&time_entry_autocompletes); + } else { + Poco::Util::TimerTask::Ptr teTask = + new Poco::Util::TimerTaskAdapter(*this, &Context::onTimeEntryAutocompletes); + timer_.schedule(teTask, Poco::Timestamp()); + } } if (what.display_mini_timer_autocomplete) { - UI()->DisplayMinitimerAutocomplete(&minitimer_autocompletes); + if (what.first_load) { + std::vector minitimer_autocompletes; + user_->related.MinitimerAutocompleteItems(&minitimer_autocompletes); + UI()->DisplayMinitimerAutocomplete(&minitimer_autocompletes); + } else { + Poco::Util::TimerTask::Ptr mtTask = + new Poco::Util::TimerTaskAdapter(*this, &Context::onMiniTimerAutocompletes); + timer_.schedule(mtTask, Poco::Timestamp()); + } } if (what.display_workspace_select) { @@ -922,7 +922,15 @@ void Context::updateUI(const UIElements &what) { // Apply autocomplete as last element, // as its depending on selects on Windows if (what.display_project_autocomplete) { - UI()->DisplayProjectAutocomplete(&project_autocompletes); + if (what.first_load) { + std::vector project_autocompletes; + user_->related.ProjectAutocompleteItems(&project_autocompletes); + UI()->DisplayProjectAutocomplete(&project_autocompletes); + } else { + Poco::Util::TimerTask::Ptr prTask = + new Poco::Util::TimerTaskAdapter(*this, &Context::onProjectAutocompletes); + timer_.schedule(prTask, Poco::Timestamp()); + } } if (what.display_unsynced_items) { @@ -1079,6 +1087,24 @@ void Context::onSync(Poco::Util::TimerTask& task) { // NOLINT displayError(save(false)); } +void Context::onTimeEntryAutocompletes(Poco::Util::TimerTask& task) { // NOLINT + std::vector time_entry_autocompletes; + user_->related.TimeEntryAutocompleteItems(&time_entry_autocompletes); + UI()->DisplayTimeEntryAutocomplete(&time_entry_autocompletes); +} + +void Context::onMiniTimerAutocompletes(Poco::Util::TimerTask& task) { // NOLINT + std::vector minitimer_autocompletes; + user_->related.MinitimerAutocompleteItems(&minitimer_autocompletes); + UI()->DisplayMinitimerAutocomplete(&minitimer_autocompletes); +} + +void Context::onProjectAutocompletes(Poco::Util::TimerTask& task) { // NOLINT + std::vector project_autocompletes; + user_->related.ProjectAutocompleteItems(&project_autocompletes); + UI()->DisplayProjectAutocomplete(&project_autocompletes); +} + void Context::setOnline(const std::string reason) { std::stringstream ss; ss << "setOnline, reason:" << reason; diff --git a/src/context.h b/src/context.h index 250bd4db7c..53018623f6 100644 --- a/src/context.h +++ b/src/context.h @@ -36,7 +36,8 @@ class WindowChangeRecorder; class UIElements { public: UIElements() - : display_time_entries(false) + : first_load(false) + , display_time_entries(false) , display_time_entry_autocomplete(false) , display_mini_timer_autocomplete(false) , display_project_autocomplete(false) @@ -61,6 +62,7 @@ class UIElements { const std::string editor_guid, const std::vector &changes); + bool first_load; bool display_time_entries; bool display_time_entry_autocomplete; bool display_mini_timer_autocomplete; @@ -472,6 +474,10 @@ class Context : public TimelineDatasource { void onWake(Poco::Util::TimerTask& task); // NOLINT void onLoadMore(Poco::Util::TimerTask& task); // NOLINT + void onTimeEntryAutocompletes(Poco::Util::TimerTask& task); // NOLINT + void onMiniTimerAutocompletes(Poco::Util::TimerTask& task); // NOLINT + void onProjectAutocompletes(Poco::Util::TimerTask& task); // NOLINT + void startPeriodicUpdateCheck(); void executeUpdateCheck(); diff --git a/src/related_data.cc b/src/related_data.cc index d49c52f79e..d1e66941a0 100644 --- a/src/related_data.cc +++ b/src/related_data.cc @@ -284,6 +284,10 @@ void RelatedData::taskAutocompleteItems( it != Tasks.end(); it++) { Task *t = *it; + if (t == NULL) { + continue; + } + if (t->IsMarkedAsDeletedOnServer()) { continue; } @@ -406,8 +410,6 @@ void RelatedData::MinitimerAutocompleteItems( timeEntryAutocompleteItems(&unique_names, result); taskAutocompleteItems(&unique_names, nullptr, result); projectAutocompleteItems(&unique_names, nullptr, result); - - std::sort(result->begin(), result->end(), CompareAutocompleteItems); } void RelatedData::ProjectAutocompleteItems( @@ -418,9 +420,6 @@ void RelatedData::ProjectAutocompleteItems( workspaceAutocompleteItems(&unique_names, &ws_names, result); projectAutocompleteItems(&unique_names, &ws_names, result); taskAutocompleteItems(&unique_names, &ws_names, result); - - std::sort(result->begin(), result->end(), - CompareStructuredAutocompleteItems); } void RelatedData::workspaceAutocompleteItems( @@ -451,13 +450,6 @@ void RelatedData::workspaceAutocompleteItems( std::string ws_name = Poco::UTF8::toUpper(ws->Name()); (*ws_names)[ws->ID()] = ws_name; - - view::Autocomplete autocomplete_item; - autocomplete_item.Text = ws_name; - autocomplete_item.WorkspaceName = ws_name; - autocomplete_item.WorkspaceID = ws->ID(); - autocomplete_item.Type = kAutocompleteItemWorkspace; - list->push_back(autocomplete_item); } } diff --git a/src/ui/osx/TogglDesktop/AutoComplete/AutoCompleteInput.m b/src/ui/osx/TogglDesktop/AutoComplete/AutoCompleteInput.m new file mode 100644 index 0000000000..b65648e9ec --- /dev/null +++ b/src/ui/osx/TogglDesktop/AutoComplete/AutoCompleteInput.m @@ -0,0 +1,20 @@ +// +// AutoCompleteInput.m +// TogglDesktop +// +// Created by Indrek Vändrik on 19/02/2018. +// Copyright © 2018 Alari. All rights reserved. +// + +#import "AutoCompleteInput.h" + +@implementation AutoCompleteInput + +- (void)drawRect:(NSRect)dirtyRect +{ + [super drawRect:dirtyRect]; + + // Drawing code here. +} + +@end diff --git a/src/ui/osx/TogglDesktop/AutoComplete/AutoCompleteTableCell.m b/src/ui/osx/TogglDesktop/AutoComplete/AutoCompleteTableCell.m new file mode 100644 index 0000000000..8741641081 --- /dev/null +++ b/src/ui/osx/TogglDesktop/AutoComplete/AutoCompleteTableCell.m @@ -0,0 +1,20 @@ +// +// AutoCompleteTableCell.m +// TogglDesktop +// +// Created by Indrek Vändrik on 19/02/2018. +// Copyright © 2018 Alari. All rights reserved. +// + +#import "AutoCompleteTableCell.h" + +@implementation AutoCompleteTableCell + +- (void)drawRect:(NSRect)dirtyRect +{ + [super drawRect:dirtyRect]; + + // Drawing code here. +} + +@end diff --git a/src/ui/osx/TogglDesktop/TimeEntryEditViewController.h b/src/ui/osx/TogglDesktop/TimeEntryEditViewController.h index 91a9d60a97..fcafd933c5 100644 --- a/src/ui/osx/TogglDesktop/TimeEntryEditViewController.h +++ b/src/ui/osx/TogglDesktop/TimeEntryEditViewController.h @@ -12,12 +12,12 @@ #import "NSTextFieldClickable.h" #import "NSResize.h" #import "MkColorWellCustom.h" +#import "AutoCompleteInput.h" +#import "AutoCompleteItem.h" -@interface TimeEntryEditViewController : NSViewController { +@interface TimeEntryEditViewController : NSViewController { } @property (strong) IBOutlet MKColorWellCustom *colorPicker; -@property IBOutlet NSCustomComboBox *descriptionCombobox; -@property IBOutlet NSCustomComboBox *projectSelect; @property IBOutlet NSTextField *durationTextField; @property IBOutlet NSTextField *startTime; @property IBOutlet NSTextField *endTime; @@ -49,9 +49,11 @@ @property (strong) IBOutlet NSButton *addClientButton; @property (strong) IBOutlet NSTextField *clientNameTextField; @property (strong) IBOutlet NSButton *saveNewClientButton; -- (IBAction)descriptionComboboxChanged:(id)sender; +@property (strong) IBOutlet AutoCompleteInput *descriptionAutoCompleteInput; +- (IBAction)descriptionAutoCompleteChanged:(id)sender; +@property (strong) IBOutlet AutoCompleteInput *projectAutoCompleteInput; +- (IBAction)projectAutoCompleteChanged:(id)sender; - (IBAction)durationTextFieldChanged:(id)sender; -- (IBAction)projectSelectChanged:(id)sender; - (IBAction)startTimeChanged:(id)sender; - (IBAction)endTimeChanged:(id)sender; - (IBAction)dateChanged:(id)sender; @@ -67,4 +69,7 @@ - (void)setDragHandle:(BOOL)onLeft; - (void)setInsertionPointColor; - (void)closeEdit; +- (BOOL)autcompleteFocused; +- (void)updateWithSelectedDescription:(AutocompleteItem *)autocomplete withKey:(NSString *)key; +- (void)updateWithSelectedProject:(AutocompleteItem *)autocomplete withKey:(NSString *)key; @end diff --git a/src/ui/osx/TogglDesktop/TimeEntryEditViewController.m b/src/ui/osx/TogglDesktop/TimeEntryEditViewController.m index f1e072720a..2f5945dfad 100644 --- a/src/ui/osx/TogglDesktop/TimeEntryEditViewController.m +++ b/src/ui/osx/TogglDesktop/TimeEntryEditViewController.m @@ -8,19 +8,17 @@ #import "TimeEntryEditViewController.h" #import "UIEvents.h" #import "TimeEntryViewItem.h" -#import "AutocompleteItem.h" #import "AutocompleteDataSource.h" +#import "LiteAutoCompleteDataSource.h" #import "NSComboBox_Expansion.h" #import "ViewItem.h" -#import "NSCustomComboBoxCell.h" -#import "NSCustomComboBox.h" #import "toggl_api.h" #import "DisplayCommand.h" #import "Utils.h" @interface TimeEntryEditViewController () -@property AutocompleteDataSource *projectAutocompleteDataSource; -@property AutocompleteDataSource *descriptionComboboxDataSource; +@property LiteAutoCompleteDataSource *liteDescriptionAutocompleteDataSource; +@property LiteAutoCompleteDataSource *liteProjectAutocompleteDataSource; @property NSTimer *timerMenubarTimer; @property TimeEntryViewItem *timeEntry; // Time entry being edited @property NSMutableArray *tagsList; @@ -56,8 +54,9 @@ - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil self.endTimeChanged = NO; self.popupOnLeft = NO; - self.projectAutocompleteDataSource = [[AutocompleteDataSource alloc] initWithNotificationName:kDisplayProjectAutocomplete]; - self.descriptionComboboxDataSource = [[AutocompleteDataSource alloc] initWithNotificationName:kDisplayTimeEntryAutocomplete]; + self.liteDescriptionAutocompleteDataSource = [[LiteAutoCompleteDataSource alloc] initWithNotificationName:kDisplayTimeEntryAutocomplete]; + + self.liteProjectAutocompleteDataSource = [[LiteAutoCompleteDataSource alloc] initWithNotificationName:kDisplayProjectAutocomplete]; self.timerMenubarTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self @@ -103,11 +102,11 @@ - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil - (void)viewDidLoad { - self.projectAutocompleteDataSource.combobox = self.projectSelect; - self.descriptionComboboxDataSource.combobox = self.descriptionCombobox; + self.liteDescriptionAutocompleteDataSource.input = self.descriptionAutoCompleteInput; + [self.liteDescriptionAutocompleteDataSource setFilter:@""]; - [self.projectAutocompleteDataSource setFilter:@""]; - [self.descriptionComboboxDataSource setFilter:@""]; + self.liteProjectAutocompleteDataSource.input = self.projectAutoCompleteInput; + [self.liteProjectAutocompleteDataSource setFilter:@""]; // Setting "Add project" link color to blue NSColor *color = [NSColor alternateSelectedControlColor]; @@ -148,6 +147,24 @@ - (void)viewDidLoad [self.resizeHandle setCursor:[NSCursor resizeLeftRightCursor]]; [self.resizeHandleLeft setCursor:[NSCursor resizeLeftRightCursor]]; toggl_get_project_colors(ctx); + + // Setup autocomplete table row click event + [self.descriptionAutoCompleteInput.autocompleteTableView setTarget:self]; + [self.descriptionAutoCompleteInput.autocompleteTableView setAction:@selector(performDescriptionTableClick:)]; + [self.projectAutoCompleteInput.autocompleteTableView setTarget:self]; + [self.projectAutoCompleteInput.autocompleteTableView setAction:@selector(performProjectTableClick:)]; +} + +- (void)viewDidAppear +{ + NSRect descriptionViewFrameInWindowCoords = [self.descriptionAutoCompleteInput convertRect:[self.descriptionAutoCompleteInput bounds] toView:nil]; + NSRect projectViewFrameInWindowCoords = [self.projectAutoCompleteInput convertRect:[self.projectAutoCompleteInput bounds] toView:nil]; + + [self.descriptionAutoCompleteInput setPos:(int)descriptionViewFrameInWindowCoords.origin.y]; + [self.projectAutoCompleteInput setPos:(int)projectViewFrameInWindowCoords.origin.y]; + + [self.descriptionAutoCompleteInput.autocompleteTableView setDelegate:self]; + [self.projectAutoCompleteInput.autocompleteTableView setDelegate:self]; } - (void)loadView @@ -170,10 +187,10 @@ - (void)setFocus:(NSNotification *)notification } if ([self.timeEntry.focusedFieldName isEqualToString:[NSString stringWithUTF8String:kFocusedFieldNameProject]]) { - [self.view.window setInitialFirstResponder:self.projectSelect]; + [self.view.window setInitialFirstResponder:self.projectAutoCompleteInput]; return; } - [self.view.window setInitialFirstResponder:self.descriptionCombobox]; + [self.view.window setInitialFirstResponder:self.descriptionAutoCompleteInput]; } - (void)setProjectColors:(NSNotification *)notification @@ -188,19 +205,28 @@ - (void)resetPopover:(NSNotification *)notification [self.addProjectBox setHidden:YES]; [self.projectSelectBox setHidden:NO]; + [self.projectAutoCompleteInput setHidden:NO]; [self.projectPublicCheckbox setState:NSOffState]; [self removeCustomConstraints]; - [self.descriptionCombobox setNextKeyView:self.projectSelect]; + [self.descriptionAutoCompleteInput setNextKeyView:self.projectAutoCompleteInput]; [self toggleAddClient:YES]; [self.addProjectButton setNextKeyView:self.durationTextField]; + + // reset autocompletes + self.descriptionAutoCompleteInput.stringValue = @""; + self.projectAutoCompleteInput.stringValue = @""; + [self.descriptionAutoCompleteInput resetTable]; + [self.projectAutoCompleteInput resetTable]; + self.liteDescriptionAutocompleteDataSource.currentFilter = nil; + self.liteProjectAutocompleteDataSource.currentFilter = nil; } - (IBAction)addProjectButtonClicked:(id)sender { self.projectNameTextField.stringValue = @""; self.clientSelect.stringValue = @""; - [self.descriptionCombobox setNextKeyView:self.projectNameTextField]; + [self.descriptionAutoCompleteInput setNextKeyView:self.projectNameTextField]; if (!self.addProjectBoxHeight) { @@ -284,6 +310,23 @@ - (IBAction)backButtonClicked:(id)sender } } +// Checks if any autocompletes are focused so we don't close the popup +- (BOOL)autcompleteFocused +{ + if ([self.descriptionAutoCompleteInput currentEditor] != nil + && !self.descriptionAutoCompleteInput.autocompleteTableContainer.isHidden) + { + return YES; + } + + if ([self.projectAutoCompleteInput currentEditor] != nil + && !self.projectAutoCompleteInput.autocompleteTableContainer.isHidden) + { + return YES; + } + return NO; +} + // Returns NO if there's an error and UI should not go out of the add project // mode. - (BOOL)applyAddProject @@ -381,8 +424,8 @@ - (void)displayTimeEntryEditor:(DisplayCommand *)cmd if (cmd.open) { - [self.projectAutocompleteDataSource setFilter:@""]; - [self.descriptionComboboxDataSource setFilter:@""]; + [self.liteDescriptionAutocompleteDataSource setFilter:@""]; + [self.liteProjectAutocompleteDataSource setFilter:@""]; } [self.billableCheckbox setHidden:!self.timeEntry.CanSeeBillable]; @@ -411,32 +454,32 @@ - (void)displayTimeEntryEditor:(DisplayCommand *)cmd } // Overwrite description only if user is not editing it: - if (cmd.open || [self.descriptionCombobox currentEditor] == nil) + if (cmd.open || [self.descriptionAutoCompleteInput currentEditor] == nil) { - self.descriptionCombobox.stringValue = self.timeEntry.Description; + self.descriptionAutoCompleteInput.stringValue = self.timeEntry.Description; self.descriptionComboboxPreviousStringValue = self.timeEntry.Description; } - self.projectSelectPreviousStringValue = self.projectSelect.stringValue; + self.projectSelectPreviousStringValue = self.projectAutoCompleteInput.stringValue; // Overwrite project only if user is not editing it - if (cmd.open || [self.projectSelect currentEditor] == nil) + if (cmd.open || [self.projectAutoCompleteInput currentEditor] == nil) { if (self.timeEntry.ProjectAndTaskLabel != nil) { - self.projectSelect.stringValue = self.timeEntry.ProjectAndTaskLabel; + self.projectAutoCompleteInput.stringValue = self.timeEntry.ProjectAndTaskLabel; self.projectSelectPreviousStringValue = self.timeEntry.ProjectAndTaskLabel; } else { - self.projectSelect.stringValue = @""; + self.projectAutoCompleteInput.stringValue = @""; self.projectSelectPreviousStringValue = @""; } if (cmd.open) { if ([self.timeEntry.focusedFieldName isEqualToString:[NSString stringWithUTF8String:kFocusedFieldNameProject]]) { - [self.projectSelect becomeFirstResponder]; + [self.projectAutoCompleteInput becomeFirstResponder]; } } } @@ -738,37 +781,6 @@ - (IBAction)durationTextFieldChanged:(id)sender toggl_set_time_entry_duration(ctx, [self.timeEntry.GUID UTF8String], value); } -- (IBAction)projectSelectChanged:(id)sender -{ - if (self.willTerminate) - { - return; - } - - if (self.projectSelectPreviousStringValue != nil && - [self.projectSelectPreviousStringValue isEqualToString:self.projectSelect.stringValue]) - { - return; - } - - NSAssert(self.timeEntry != nil, @"Expected time entry"); - - NSString *key = self.projectSelect.stringValue; - AutocompleteItem *autocomplete = [self.projectAutocompleteDataSource get:key]; - uint64_t task_id = 0; - uint64_t project_id = 0; - if (autocomplete != nil) - { - task_id = autocomplete.TaskID; - project_id = autocomplete.ProjectID; - } - if ([key length] && project_id == 0) - { - return; - } - toggl_set_time_entry_project(ctx, [self.timeEntry.GUID UTF8String], task_id, project_id, 0); -} - - (IBAction)startTimeChanged:(id)sender { if (self.willTerminate) @@ -834,28 +846,28 @@ - (IBAction)billableCheckBoxClicked:(id)sender toggl_set_time_entry_billable(ctx, [self.timeEntry.GUID UTF8String], value); } -- (IBAction)descriptionComboboxChanged:(id)sender +- (IBAction)descriptionAutoCompleteChanged:(id)sender { if (self.willTerminate) { return; } - if (self.descriptionCombobox.stringValue != nil && - [self.descriptionCombobox.stringValue isEqualToString:self.descriptionComboboxPreviousStringValue]) + if (self.descriptionAutoCompleteInput.stringValue != nil && + [self.descriptionAutoCompleteInput.stringValue isEqualToString:self.descriptionComboboxPreviousStringValue]) { return; } NSAssert(self.timeEntry != nil, @"Time entry expected"); - NSString *key = self.descriptionCombobox.stringValue; - - NSLog(@"descriptionComboboxChanged, stringValue = %@", key); - - AutocompleteItem *autocomplete = - [self.descriptionComboboxDataSource get:key]; + NSString *key = [self.descriptionAutoCompleteInput stringValue]; + AutocompleteItem *autocomplete = [self.liteDescriptionAutocompleteDataSource get:key]; + [self updateWithSelectedDescription:autocomplete withKey:key]; +} +- (void)updateWithSelectedDescription:(AutocompleteItem *)autocomplete withKey:(NSString *)key +{ const char *GUID = [self.timeEntry.GUID UTF8String]; if (!autocomplete) @@ -863,31 +875,91 @@ - (IBAction)descriptionComboboxChanged:(id)sender toggl_set_time_entry_description(ctx, GUID, [key UTF8String]); + [self.descriptionAutoCompleteInput becomeFirstResponder]; + [self.descriptionAutoCompleteInput resetTable]; + self.liteDescriptionAutocompleteDataSource.currentFilter = nil; + return; + } + + self.descriptionAutoCompleteInput.stringValue = autocomplete.Description; + + @synchronized(self) + { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + if (![self.timeEntry.Description isEqualToString:key] && + !toggl_set_time_entry_project(ctx, + GUID, + autocomplete.TaskID, + autocomplete.ProjectID, + 0)) + { + [self.descriptionAutoCompleteInput becomeFirstResponder]; + [self.descriptionAutoCompleteInput resetTable]; + self.liteDescriptionAutocompleteDataSource.currentFilter = nil; + return; + } + + toggl_set_time_entry_description(ctx, GUID, [autocomplete.Description UTF8String]); + + const char *value = [[autocomplete.tags componentsJoinedByString:@"\t"] UTF8String]; + toggl_set_time_entry_tags(ctx, GUID, value); + + bool_t isBillable = autocomplete.Billable; + + if (isBillable) + { + toggl_set_time_entry_billable(ctx, GUID, isBillable); + } + }); + } + [self.descriptionAutoCompleteInput becomeFirstResponder]; + [self.descriptionAutoCompleteInput resetTable]; + self.liteDescriptionAutocompleteDataSource.currentFilter = nil; +} + +- (IBAction)projectAutoCompleteChanged:(id)sender +{ + if (self.willTerminate) + { return; } - if (![self.timeEntry.Description isEqualToString:key] && - !toggl_set_time_entry_project(ctx, - GUID, - autocomplete.TaskID, - autocomplete.ProjectID, - 0)) + if (self.projectSelectPreviousStringValue != nil && + [self.projectSelectPreviousStringValue isEqualToString:self.projectAutoCompleteInput.stringValue]) { return; } - self.descriptionCombobox.stringValue = autocomplete.Description; - toggl_set_time_entry_description(ctx, GUID, [autocomplete.Description UTF8String]); + NSAssert(self.timeEntry != nil, @"Expected time entry"); + + NSString *key = self.projectAutoCompleteInput.stringValue; + AutocompleteItem *autocomplete = [self.liteProjectAutocompleteDataSource get:key]; + [self updateWithSelectedProject:autocomplete withKey:key]; +} + +- (void)updateWithSelectedProject:(AutocompleteItem *)autocomplete withKey:(NSString *)key +{ + uint64_t task_id = 0; + uint64_t project_id = 0; - const char *value = [[autocomplete.tags componentsJoinedByString:@"\t"] UTF8String]; - toggl_set_time_entry_tags(ctx, GUID, value); + if (autocomplete == nil) + { + return; + } - bool_t isBillable = autocomplete.Billable; + task_id = autocomplete.TaskID; + project_id = autocomplete.ProjectID; + self.projectAutoCompleteInput.stringValue = autocomplete.ProjectAndTaskLabel; - if (isBillable) + @synchronized(self) { - toggl_set_time_entry_billable(ctx, GUID, isBillable); + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + toggl_set_time_entry_project(ctx, [self.timeEntry.GUID UTF8String], task_id, project_id, 0); + }); } + [self.projectAutoCompleteInput becomeFirstResponder]; + [self.projectAutoCompleteInput resetTable]; + self.liteProjectAutocompleteDataSource.currentFilter = nil; } - (IBAction)deleteButtonClicked:(id)sender @@ -1001,6 +1073,12 @@ - (void)controlTextDidEndEditing:(NSNotification *)aNotification return; } + if ([[aNotification object] isKindOfClass:[AutoCompleteInput class]]) + { + AutoCompleteInput *input = [aNotification object]; + [input resetTable]; + } + if (![[aNotification object] isKindOfClass:[NSTokenField class]]) { // If enter was pressed then close editpopup @@ -1034,6 +1112,25 @@ - (void)controlTextDidChange:(NSNotification *)aNotification return; } + if ([[aNotification object] isKindOfClass:[AutoCompleteInput class]]) + { + AutoCompleteInput *field = [aNotification object]; + if (field == self.descriptionAutoCompleteInput) + { + NSLog(@"Filter DESCRIPTION: %@", [field stringValue]); + [self.liteDescriptionAutocompleteDataSource setFilter:[field stringValue]]; + } + + if (field == self.projectAutoCompleteInput) + { + NSLog(@"Filter PROJECTS: %@", [field stringValue]); + [self.liteProjectAutocompleteDataSource setFilter:[field stringValue]]; + } + + [field.autocompleteTableView resetSelected]; + return; + } + // Don't trigger combobox autocomplete when inside tags field if (![[aNotification object] isKindOfClass:[NSComboBox class]]) { @@ -1044,14 +1141,6 @@ - (void)controlTextDidChange:(NSNotification *)aNotification NSString *filter = [comboBox stringValue]; AutocompleteDataSource *dataSource = nil; - if (comboBox == self.projectSelect) - { - dataSource = self.projectAutocompleteDataSource; - } - if (comboBox == self.descriptionCombobox) - { - dataSource = self.descriptionComboboxDataSource; - } if (comboBox == self.clientSelect) { [self resultsInComboForString:comboBox.stringValue]; @@ -1206,6 +1295,184 @@ - (void)closeEdit toggl_edit(ctx, [self.timeEntry.GUID UTF8String], false, ""); } +#pragma AutocompleteTableView Delegate + +- (BOOL) tableView:(NSTableView *)aTableView + shouldSelectRow:(NSInteger)rowIndex +{ + AutoCompleteTable *table = (AutoCompleteTable *)aTableView; + + [table setCurrentSelected:rowIndex]; + return YES; +} + +- (NSView *) tableView:(NSTableView *)tableView + viewForTableColumn:(NSTableColumn *)tableColumn + row:(NSInteger)row +{ + if (row < 0) + { + return nil; + } + + AutocompleteItem *item = nil; + LiteAutoCompleteDataSource *dataSource = nil; + + if (tableView == self.descriptionAutoCompleteInput.autocompleteTableView) + { + dataSource = self.liteDescriptionAutocompleteDataSource; + } + + if (tableView == self.projectAutoCompleteInput.autocompleteTableView) + { + dataSource = self.liteProjectAutocompleteDataSource; + } + + @synchronized(self) + { + item = [dataSource.filteredOrderedKeys objectAtIndex:row]; + } + NSLog(@"%@", item); + NSAssert(item != nil, @"view item from viewitems array is nil"); + + AutoCompleteTableCell *cell = [tableView makeViewWithIdentifier:@"AutoCompleteTableCell" + owner:self]; + + [cell render:item]; + return cell; +} + +- (CGFloat)tableView:(NSTableView *)tableView + heightOfRow:(NSInteger)row +{ + return 25; +} + +- (IBAction)performDescriptionTableClick:(id)sender +{ + AutoCompleteInput *input = self.descriptionAutoCompleteInput; + LiteAutoCompleteDataSource *dataSource = self.liteDescriptionAutocompleteDataSource; + + NSInteger row = [input.autocompleteTableView clickedRow]; + + if (row < 0) + { + return; + } + + AutocompleteItem *item = [dataSource itemAtIndex:row]; + [self updateWithSelectedDescription:item withKey:item.Text]; +} + +- (IBAction)performProjectTableClick:(id)sender +{ + AutoCompleteInput *input = self.projectAutoCompleteInput; + LiteAutoCompleteDataSource *dataSource = self.liteProjectAutocompleteDataSource; + + NSInteger row = [input.autocompleteTableView clickedRow]; + + if (row < 0) + { + return; + } + + AutocompleteItem *item = [dataSource itemAtIndex:row]; + [self updateWithSelectedProject:item withKey:item.Text]; +} + +- (BOOL)control:(NSControl *)control textView:(NSTextView *)fieldEditor doCommandBySelector:(SEL)commandSelector +{ + BOOL retval = NO; + BOOL valid = YES; + AutoCompleteInput *input = nil; + LiteAutoCompleteDataSource *dataSource = nil; + NSInteger lastSelected = -1; + + if ([self.descriptionAutoCompleteInput currentEditor] != nil) + { + input = self.descriptionAutoCompleteInput; + lastSelected = input.autocompleteTableView.lastSelected; + dataSource = self.liteDescriptionAutocompleteDataSource; + } + if ([self.projectAutoCompleteInput currentEditor] != nil) + { + input = self.projectAutoCompleteInput; + lastSelected = input.autocompleteTableView.lastSelected; + dataSource = self.liteProjectAutocompleteDataSource; + + // Validate project input + if (input.autocompleteTableView.isHidden) + { + NSString *key = self.projectAutoCompleteInput.stringValue; + AutocompleteItem *autocomplete = [self.liteProjectAutocompleteDataSource get:key]; + valid = (autocomplete != nil); + } + } + if (input != nil) + { + if (commandSelector == @selector(moveDown:)) + { + [input.autocompleteTableView nextItem]; + retval = YES; + } + if (commandSelector == @selector(moveUp:)) + { + [input.autocompleteTableView previousItem]; + retval = YES; + } + if (commandSelector == @selector(insertTab:)) + { + if (input == self.projectAutoCompleteInput && !valid) + { + self.projectAutoCompleteInput.stringValue = self.timeEntry.ProjectAndTaskLabel; + } + [input resetTable]; + dataSource.currentFilter = nil; + } + if (commandSelector == @selector(insertNewline:)) + { + // allow default action when autocomplete is closed + if (input.autocompleteTableView.isHidden) + { + if (input == self.projectAutoCompleteInput && !valid) + { + self.projectAutoCompleteInput.stringValue = self.timeEntry.ProjectAndTaskLabel; + } + + return NO; + } + + // avoid firing default Enter actions + retval = YES; + + // Set data according to selected item + if (lastSelected >= 0) + { + AutocompleteItem *item = [dataSource itemAtIndex:lastSelected]; + + if (item == nil) + { + return NO; + } + + [input resetTable]; + dataSource.currentFilter = nil; + + if (input == self.descriptionAutoCompleteInput) + { + [self updateWithSelectedDescription:item withKey:item.Text]; + } + else if (input == self.projectAutoCompleteInput) + { + [self updateWithSelectedProject:item withKey:item.Text]; + } + } + } + } + + return retval; +} + @end // http://stackoverflow.com/questions/4499262/how-to-programmatically-open-an-nscomboboxs-list diff --git a/src/ui/osx/TogglDesktop/TimeEntryEditViewController.xib b/src/ui/osx/TogglDesktop/TimeEntryEditViewController.xib index 6bb1252732..cae1cc7a60 100644 --- a/src/ui/osx/TogglDesktop/TimeEntryEditViewController.xib +++ b/src/ui/osx/TogglDesktop/TimeEntryEditViewController.xib @@ -20,14 +20,14 @@ - + + - @@ -65,43 +65,52 @@ - + - - - + + + + + + - - - + + + - - - + + + - + - - - + + + + + + - - - + + + - - - + + + - + - - - - - + + + + + @@ -580,8 +589,10 @@ + - + + @@ -591,8 +602,6 @@ - - diff --git a/src/ui/osx/TogglDesktop/TimeEntryListViewController.m b/src/ui/osx/TogglDesktop/TimeEntryListViewController.m index 0baf604480..6abaf0679d 100644 --- a/src/ui/osx/TogglDesktop/TimeEntryListViewController.m +++ b/src/ui/osx/TogglDesktop/TimeEntryListViewController.m @@ -513,6 +513,10 @@ - (void)closeEditPopup:(NSNotification *)notification { if (self.timeEntrypopover.shown) { + if ([self.timeEntryEditViewController autcompleteFocused]) + { + return; + } if (self.runningEdit) { [self.timeEntryEditViewController closeEdit]; diff --git a/src/ui/osx/TogglDesktop/TimerEditViewController.h b/src/ui/osx/TogglDesktop/TimerEditViewController.h index f1fb257dfb..5aee543644 100644 --- a/src/ui/osx/TogglDesktop/TimerEditViewController.h +++ b/src/ui/osx/TogglDesktop/TimerEditViewController.h @@ -12,14 +12,14 @@ #import "NSCustomTimerComboBox.h" #import "NSHoverButton.h" #import "NSBoxClickable.h" +#import "AutoCompleteInput.h" -@interface TimerEditViewController : NSViewController { +@interface TimerEditViewController : NSViewController { } @property (strong) IBOutlet NSBoxClickable *manualBox; @property (strong) IBOutlet NSBox *hidingBox; @property (strong) IBOutlet NSBoxClickable *mainBox; @property IBOutlet NSTextFieldDuration *durationTextField; -@property IBOutlet NSCustomTimerComboBox *descriptionComboBox; @property (weak) IBOutlet NSLayoutConstraint *descriptionTrailing; @property IBOutlet NSHoverButton *startButton; @property IBOutlet NSTextField *projectTextField; @@ -30,8 +30,10 @@ @property NSArray *projectComboConstraint; @property NSArray *projectLabelConstraint; - (IBAction)startButtonClicked:(id)sender; -- (IBAction)descriptionComboBoxChanged:(id)sender; - (IBAction)durationFieldChanged:(id)sender; +- (IBAction)autoCompleteChanged:(id)sender; @property (strong) IBOutlet NSTextFieldClickable *addEntryLabel; +@property (weak) IBOutlet AutoCompleteInput *autoCompleteInput; - (void)timerFired:(NSTimer *)timer; +- (void)fillEntryFromAutoComplete:(AutocompleteItem *)item; @end diff --git a/src/ui/osx/TogglDesktop/TimerEditViewController.m b/src/ui/osx/TogglDesktop/TimerEditViewController.m index ba650d0eef..9b20d113bd 100644 --- a/src/ui/osx/TogglDesktop/TimerEditViewController.m +++ b/src/ui/osx/TogglDesktop/TimerEditViewController.m @@ -9,7 +9,7 @@ #import "TimerEditViewController.h" #import "UIEvents.h" #import "AutocompleteItem.h" -#import "AutocompleteDataSource.h" +#import "LiteAutoCompleteDataSource.h" #import "ConvertHexColor.h" #import "NSComboBox_Expansion.h" #import "TimeEntryViewItem.h" @@ -20,7 +20,7 @@ #import "DisplayCommand.h" @interface TimerEditViewController () -@property AutocompleteDataSource *autocompleteDataSource; +@property LiteAutoCompleteDataSource *liteAutocompleteDataSource; @property TimeEntryViewItem *time_entry; @property NSTimer *timer; @property BOOL constraintsAdded; @@ -39,7 +39,7 @@ - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; if (self) { - self.autocompleteDataSource = [[AutocompleteDataSource alloc] initWithNotificationName:kDisplayMinitimerAutocomplete]; + self.liteAutocompleteDataSource = [[LiteAutoCompleteDataSource alloc] initWithNotificationName:kDisplayMinitimerAutocomplete]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(startDisplayTimerState:) @@ -87,9 +87,9 @@ - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil - (void)viewDidLoad { - self.autocompleteDataSource.combobox = self.descriptionComboBox; + self.liteAutocompleteDataSource.input = self.autoCompleteInput; + [self.liteAutocompleteDataSource setFilter:@""]; - [self.autocompleteDataSource setFilter:@""]; NSFont *descriptionFont = [NSFont fontWithName:@"Lucida Grande" size:13.0]; NSFont *durationFont = [NSFont fontWithName:@"Lucida Grande" size:14.0]; NSColor *color = [ConvertHexColor hexCodeToNSColor:kTrackingColor]; @@ -112,7 +112,7 @@ - (void)viewDidLoad [[self.durationTextField cell] setPlaceholderAttributedString:durationLightString]; [[self.descriptionLabel cell] setPlaceholderAttributedString:descriptionLightString]; - [[self.descriptionComboBox cell] setPlaceholderAttributedString:descriptionLightString]; + [[self.autoCompleteInput cell] setPlaceholderAttributedString:descriptionLightString]; [self.startButton setHoverAlpha:0.75]; @@ -123,6 +123,9 @@ - (void)viewDidLoad trail = 60; } self.descriptionTrailing.constant = trail; + + [self.autoCompleteInput.autocompleteTableView setTarget:self]; + [self.autoCompleteInput.autocompleteTableView setAction:@selector(performClick:)]; } - (void)loadView @@ -131,6 +134,14 @@ - (void)loadView [self viewDidLoad]; } +- (void)viewDidAppear +{ + NSRect viewFrameInWindowCoords = [self.view convertRect:[self.view bounds] toView:nil]; + + [self.autoCompleteInput setPos:(int)viewFrameInWindowCoords.origin.y]; + [self.autoCompleteInput.autocompleteTableView setDelegate:self]; +} + - (void)startDisplayLogin:(NSNotification *)notification { [self clear]; @@ -144,7 +155,7 @@ - (void)focusTimer:(NSNotification *)notification } else { - [self.descriptionComboBox becomeFirstResponder]; + [self.autoCompleteInput becomeFirstResponder]; } } @@ -161,7 +172,7 @@ - (void)displayTimeEntryList:(DisplayCommand *)cmd if (cmd.open && self.time_entry && self.time_entry.duration_in_seconds >= 0) { - [self.descriptionComboBox becomeFirstResponder]; + [self.autoCompleteInput becomeFirstResponder]; } } @@ -182,24 +193,16 @@ - (void)displayTimerState:(TimeEntryViewItem *)te } self.time_entry = te; - // Start/stop button title and color depend on - // whether time entry is running + // Description and duration cannot be edited + // while time entry is running if (self.time_entry.duration_in_seconds < 0) { + // Start/stop button title and color depend on + // whether time entry is running self.startButton.toolTip = @"Stop"; [self.startButton setImage:[NSImage imageNamed:@"stop_button.pdf"]]; toggl_set_settings_manual_mode(ctx, NO); - } - else - { - self.startButton.toolTip = @"Start"; - [self.startButton setImage:[NSImage imageNamed:@"start_button.pdf"]]; - } - // Description and duration cannot be edited - // while time entry is running - if (self.time_entry.duration_in_seconds < 0) - { [self.durationTextField setDelegate:self]; // Time entry has a description if (self.time_entry.Description && [self.time_entry.Description length] > 0) @@ -212,7 +215,7 @@ - (void)displayTimerState:(TimeEntryViewItem *)te self.descriptionLabel.stringValue = @"(no description)"; self.descriptionLabel.toolTip = @"(no description)"; } - [self.descriptionComboBox setHidden:YES]; + [self.autoCompleteInput hide]; [self.descriptionLabel setHidden:NO]; [self.durationTextField setEditable:NO]; [self.durationTextField setSelectable:NO]; @@ -238,16 +241,7 @@ - (void)displayTimerState:(TimeEntryViewItem *)te } else { - [self.descriptionComboBox setHidden:NO]; - [self.descriptionLabel setHidden:YES]; - [self.durationTextField setEditable:YES]; - [self.durationTextField setSelectable:YES]; - [self.durationTextField setHidden:YES]; - [self.descriptionLabel setTextColor:[ConvertHexColor hexCodeToNSColor:kInactiveTimerColor]]; - - [self.durationTextField setTextColor:[ConvertHexColor hexCodeToNSColor:kInactiveTimerColor]]; - [self.tagFlag setHidden:YES]; - [self.billableFlag setHidden:YES]; + [self showDefaultTimer]; } [self checkProjectConstraints]; @@ -326,6 +320,25 @@ - (void)checkProjectConstraints } } +- (void)showDefaultTimer +{ + // Start/stop button title and color depend on + // whether time entry is running + self.startButton.toolTip = @"Start"; + [self.startButton setImage:[NSImage imageNamed:@"start_button.pdf"]]; + + [self.autoCompleteInput setHidden:NO]; + [self.descriptionLabel setHidden:YES]; + [self.durationTextField setEditable:YES]; + [self.durationTextField setSelectable:YES]; + [self.durationTextField setHidden:YES]; + [self.descriptionLabel setTextColor:[ConvertHexColor hexCodeToNSColor:kInactiveTimerColor]]; + + [self.durationTextField setTextColor:[ConvertHexColor hexCodeToNSColor:kInactiveTimerColor]]; + [self.tagFlag setHidden:YES]; + [self.billableFlag setHidden:YES]; +} + - (NSMutableAttributedString *)setProjectClientLabel:(TimeEntryViewItem *)view_item { NSMutableAttributedString *clientName = [[NSMutableAttributedString alloc] initWithString:view_item.ClientLabel]; @@ -364,7 +377,7 @@ - (NSMutableAttributedString *)setProjectClientLabel:(TimeEntryViewItem *)view_i - (void)textFieldClicked:(id)sender { - [self.descriptionComboBox becomeFirstResponder]; + [self.autoCompleteInput becomeFirstResponder]; [[NSNotificationCenter defaultCenter] postNotificationName:kResetEditPopoverSize object:nil @@ -394,15 +407,15 @@ - (void)textFieldClicked:(id)sender - (void)createConstraints { - NSDictionary *viewsDict = NSDictionaryOfVariableBindings(_descriptionComboBox, _projectTextField); + NSDictionary *viewsDict = NSDictionaryOfVariableBindings(_autoCompleteInput, _projectTextField); - self.projectComboConstraint = [NSLayoutConstraint constraintsWithVisualFormat:@"V:[_descriptionComboBox]-6@1000-[_projectTextField]" + self.projectComboConstraint = [NSLayoutConstraint constraintsWithVisualFormat:@"V:[_autoCompleteInput]-2@1000-[_projectTextField]" options:0 metrics:nil views:viewsDict]; NSDictionary *viewsDict_ = NSDictionaryOfVariableBindings(_descriptionLabel, _projectTextField); - self.projectLabelConstraint = [NSLayoutConstraint constraintsWithVisualFormat:@"V:[_descriptionLabel]-4@1000-[_projectTextField]" + self.projectLabelConstraint = [NSLayoutConstraint constraintsWithVisualFormat:@"V:[_descriptionLabel]-6@1000-[_projectTextField]" options:0 metrics:nil views:viewsDict_]; @@ -411,7 +424,9 @@ - (void)createConstraints - (void)clear { self.durationTextField.stringValue = @""; - self.descriptionComboBox.stringValue = @""; + self.autoCompleteInput.stringValue = @""; + [self.autoCompleteInput resetTable]; + self.liteAutocompleteDataSource.currentFilter = nil; self.projectTextField.stringValue = @""; [self.projectTextField setHidden:YES]; } @@ -426,6 +441,14 @@ - (IBAction)startButtonClicked:(id)sender if (self.time_entry.duration_in_seconds < 0) { [self clear]; + [self showDefaultTimer]; + [self.projectTextField setHidden:YES]; + if (self.constraintsAdded) + { + [self.view removeConstraints:self.projectComboConstraint]; + [self.view removeConstraints:self.projectLabelConstraint]; + self.constraintsAdded = NO; + } [[NSNotificationCenter defaultCenter] postNotificationName:kCommandStop object:nil]; return; @@ -439,18 +462,13 @@ - (IBAction)startButtonClicked:(id)sender [self.durationTextField.window makeFirstResponder:[self.durationTextField superview]]; self.disableChange = NO; self.time_entry.duration = self.durationTextField.stringValue; - self.time_entry.Description = self.descriptionComboBox.stringValue; + self.time_entry.Description = self.autoCompleteInput.stringValue; [[NSNotificationCenter defaultCenter] postNotificationName:kCommandNew object:self.time_entry]; - // Reset autocomplete filter - [self.autocompleteDataSource setFilter:@""]; - - if (self.time_entry.duration_in_seconds >= 0) - { - [self clear]; - self.time_entry = [[TimeEntryViewItem alloc] init]; - } + // Reset autocomplete + self.liteAutocompleteDataSource.currentFilter = nil; + [self.autoCompleteInput resetTable]; } - (IBAction)durationFieldChanged:(id)sender @@ -471,22 +489,27 @@ - (IBAction)durationFieldChanged:(id)sender [self.durationTextField setStringValue:newValue]; } -- (IBAction)descriptionComboBoxChanged:(id)sender +- (IBAction)autoCompleteChanged:(id)sender { if (self.disableChange == YES) { return; } - NSString *key = [self.descriptionComboBox stringValue]; - AutocompleteItem *item = [self.autocompleteDataSource get:key]; + NSString *key = [self.autoCompleteInput stringValue]; + AutocompleteItem *item = [self.liteAutocompleteDataSource get:key]; // User has entered free text if (item == nil) { - self.time_entry.Description = [self.descriptionComboBox stringValue]; + self.time_entry.Description = [self.autoCompleteInput stringValue]; return; } + [self fillEntryFromAutoComplete:item]; +} + +- (void)fillEntryFromAutoComplete:(AutocompleteItem *)item +{ // User has selected a autocomplete item. // It could be a time entry, a task or a project. self.time_entry.WorkspaceID = item.WorkspaceID; @@ -500,7 +523,7 @@ - (IBAction)descriptionComboBoxChanged:(id)sender self.time_entry.tags = [[NSMutableArray alloc] initWithArray:item.tags copyItems:YES]; self.time_entry.Description = ([item.Description length] != 0) ? item.Description : item.TaskLabel; - self.descriptionComboBox.stringValue = self.time_entry.Description; + self.autoCompleteInput.stringValue = self.time_entry.Description; if (item.ProjectID) { [self.projectTextField setAttributedStringValue:[self setProjectClientLabel:self.time_entry]]; @@ -517,25 +540,15 @@ - (void)controlTextDidChange:(NSNotification *)aNotification { return; } - NSComboBox *box = [aNotification object]; - NSString *filter = [box stringValue]; - [self.autocompleteDataSource setFilter:filter]; - - // Hide dropdown if filter is empty or nothing was found - if (!filter || ![filter length] || !self.autocompleteDataSource.count) + if ([[aNotification object] isKindOfClass:[AutoCompleteInput class]]) { - if ([box isExpanded] == YES) - { - [box setExpanded:NO]; - } + AutoCompleteInput *field = [aNotification object]; + [self.liteAutocompleteDataSource setFilter:[field stringValue]]; + [field.autocompleteTableView resetSelected]; + NSLog(@"Filter: %@", [field stringValue]); return; } - - if ([box isExpanded] == NO) - { - [box setExpanded:YES]; - } } - (void)timerFired:(NSTimer *)timer @@ -569,7 +582,7 @@ - (void)addButtonClicked { const char *tag_list = [[self.time_entry.tags componentsJoinedByString:@"\t"] UTF8String]; char *guid = toggl_start(ctx, - [self.descriptionComboBox.stringValue UTF8String], + [self.autoCompleteInput.stringValue UTF8String], "0", self.time_entry.TaskID, self.time_entry.ProjectID, @@ -585,4 +598,102 @@ - (void)addButtonClicked toggl_edit(ctx, [GUID UTF8String], false, kFocusedFieldNameDescription); } +#pragma AutocompleteTableView Delegate + +- (BOOL) tableView:(NSTableView *)aTableView + shouldSelectRow:(NSInteger)rowIndex +{ + AutoCompleteTable *table = (AutoCompleteTable *)aTableView; + + [table setCurrentSelected:rowIndex]; + return YES; +} + +- (NSView *) tableView:(NSTableView *)tableView + viewForTableColumn:(NSTableColumn *)tableColumn + row:(NSInteger)row +{ + if (row < 0) + { + return nil; + } + + AutocompleteItem *item = nil; + + @synchronized(self) + { + item = [self.liteAutocompleteDataSource.filteredOrderedKeys objectAtIndex:row]; + } + // NSLog(@"%@", item); + NSAssert(item != nil, @"view item from viewitems array is nil"); + + AutoCompleteTableCell *cell = [tableView makeViewWithIdentifier:@"AutoCompleteTableCell" + owner:self]; + + [cell render:item]; + return cell; +} + +- (CGFloat)tableView:(NSTableView *)tableView + heightOfRow:(NSInteger)row +{ + return 25; +} + +- (IBAction)performClick:(id)sender +{ + NSInteger row = [self.autoCompleteInput.autocompleteTableView clickedRow]; + + if (row < 0) + { + return; + } + + AutocompleteItem *item = [self.liteAutocompleteDataSource itemAtIndex:row]; + [self fillEntryFromAutoComplete:item]; + [self.autoCompleteInput becomeFirstResponder]; + NSRange tRange = [[self.autoCompleteInput currentEditor] selectedRange]; + [[self.autoCompleteInput currentEditor] setSelectedRange:NSMakeRange(tRange.length, 0)]; + [self.autoCompleteInput resetTable]; +} + +- (BOOL)control:(NSControl *)control textView:(NSTextView *)fieldEditor doCommandBySelector:(SEL)commandSelector +{ + BOOL retval = NO; + + if ([self.autoCompleteInput currentEditor] != nil) + { + if (commandSelector == @selector(moveDown:)) + { + [self.autoCompleteInput.autocompleteTableView nextItem]; + } + if (commandSelector == @selector(moveUp:)) + { + [self.autoCompleteInput.autocompleteTableView previousItem]; + } + if (commandSelector == @selector(insertTab:)) + { + [self.autoCompleteInput resetTable]; + } + if (commandSelector == @selector(insertNewline:)) + { + // avoid firing default Enter actions + retval = YES; + + // Set data according to selected item + if (self.autoCompleteInput.autocompleteTableView.lastSelected >= 0) + { + AutocompleteItem *item = [self.liteAutocompleteDataSource itemAtIndex:self.autoCompleteInput.autocompleteTableView.lastSelected]; + [self.autoCompleteInput resetTable]; + [self fillEntryFromAutoComplete:item]; + } + + // Start entry + [self startButtonClicked:nil]; + } + } + // NSLog(@"Selector = %@", NSStringFromSelector( commandSelector ) ); + return retval; +} + @end diff --git a/src/ui/osx/TogglDesktop/TimerEditViewController.xib b/src/ui/osx/TogglDesktop/TimerEditViewController.xib index fe813df51b..9c7ee192c3 100644 --- a/src/ui/osx/TogglDesktop/TimerEditViewController.xib +++ b/src/ui/osx/TogglDesktop/TimerEditViewController.xib @@ -10,10 +10,9 @@ + - - @@ -54,7 +53,6 @@ DQ - @@ -67,22 +65,22 @@ DQ - - + + - + - + - - - + + + - - - + + + - + @@ -95,7 +93,7 @@ DQ + + + @@ -198,20 +199,17 @@ DQ - - + - - diff --git a/src/ui/osx/TogglDesktop/TogglDesktop.xcodeproj/project.pbxproj b/src/ui/osx/TogglDesktop/TogglDesktop.xcodeproj/project.pbxproj index 63eb9b070d..080cd8be92 100644 --- a/src/ui/osx/TogglDesktop/TogglDesktop.xcodeproj/project.pbxproj +++ b/src/ui/osx/TogglDesktop/TogglDesktop.xcodeproj/project.pbxproj @@ -27,6 +27,11 @@ 3C65A7D31DE574E7005586B4 /* group_icon_closed.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 3C65A7CF1DE574E7005586B4 /* group_icon_closed.pdf */; }; 3C65A7D41DE574E7005586B4 /* group_icon_open.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 3C65A7D01DE574E7005586B4 /* group_icon_open.pdf */; }; 3C65A7E51DE5757A005586B4 /* continue_regular.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 3C65A7E41DE5757A005586B4 /* continue_regular.pdf */; }; + 3C6B2486203E01D90063FC08 /* AutoCompleteTableCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C6B2480203E01D60063FC08 /* AutoCompleteTableCell.m */; }; + 3C6B2487203E01D90063FC08 /* AutoCompleteInput.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C6B2483203E01D90063FC08 /* AutoCompleteInput.m */; }; + 3C6B2488203E01D90063FC08 /* AutoCompleteTable.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C6B2484203E01D90063FC08 /* AutoCompleteTable.m */; }; + 3C6B2489203E01D90063FC08 /* AutoCompleteTableCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 3C6B2485203E01D90063FC08 /* AutoCompleteTableCell.xib */; }; + 3C6B24A4203FC8200063FC08 /* LiteAutoCompleteDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C6B24A3203FC8200063FC08 /* LiteAutoCompleteDataSource.m */; }; 3C7B4E8D190FA6D200627DC3 /* NSTextFieldVerticallyAligned.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C7B4E8C190FA6D200627DC3 /* NSTextFieldVerticallyAligned.m */; }; 3C7B4E9E190FA7F900627DC3 /* logo-white@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3C7B4E9D190FA7F900627DC3 /* logo-white@2x.png */; }; 3C7B4EB3190FB57A00627DC3 /* NSSecureTextFieldVerticallyAligned.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C7B4EB2190FB57A00627DC3 /* NSSecureTextFieldVerticallyAligned.m */; }; @@ -52,6 +57,7 @@ 3CDA2C5B1913B61000A94967 /* icon-billable@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3CDA2C591913B60F00A94967 /* icon-billable@2x.png */; }; 3CDA2C5C1913B61000A94967 /* icon-tags@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3CDA2C5A1913B60F00A94967 /* icon-tags@2x.png */; }; 3CE1CAC11C774F2B00D0ADD5 /* LoadMoreCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CE1CAC01C774F2B00D0ADD5 /* LoadMoreCell.m */; }; + 3CE30E022052BD8B00AF2E2A /* AutoCompleteTableContainer.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CE30E012052BD8B00AF2E2A /* AutoCompleteTableContainer.m */; }; 3CFE547E201781A7006B673A /* libcrypto.1.1.dylib in CopyFiles */ = {isa = PBXBuildFile; fileRef = 3CFE546D201781A5006B673A /* libcrypto.1.1.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 3CFE547F201781A7006B673A /* libssl.1.1.dylib in CopyFiles */ = {isa = PBXBuildFile; fileRef = 3CFE547D201781A5006B673A /* libssl.1.1.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 69FC17F517E6534400B96425 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 69FC17F417E6534400B96425 /* Cocoa.framework */; }; @@ -334,6 +340,15 @@ 3C65A7CF1DE574E7005586B4 /* group_icon_closed.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = group_icon_closed.pdf; sourceTree = ""; }; 3C65A7D01DE574E7005586B4 /* group_icon_open.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = group_icon_open.pdf; sourceTree = ""; }; 3C65A7E41DE5757A005586B4 /* continue_regular.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = continue_regular.pdf; sourceTree = ""; }; + 3C6B247F203E01D60063FC08 /* AutoCompleteTableCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AutoCompleteTableCell.h; sourceTree = ""; }; + 3C6B2480203E01D60063FC08 /* AutoCompleteTableCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AutoCompleteTableCell.m; sourceTree = ""; }; + 3C6B2481203E01D80063FC08 /* AutoCompleteInput.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AutoCompleteInput.h; sourceTree = ""; }; + 3C6B2482203E01D80063FC08 /* AutoCompleteTable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AutoCompleteTable.h; sourceTree = ""; }; + 3C6B2483203E01D90063FC08 /* AutoCompleteInput.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AutoCompleteInput.m; sourceTree = ""; }; + 3C6B2484203E01D90063FC08 /* AutoCompleteTable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AutoCompleteTable.m; sourceTree = ""; }; + 3C6B2485203E01D90063FC08 /* AutoCompleteTableCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = AutoCompleteTableCell.xib; sourceTree = ""; }; + 3C6B24A2203FC8200063FC08 /* LiteAutoCompleteDataSource.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LiteAutoCompleteDataSource.h; sourceTree = ""; }; + 3C6B24A3203FC8200063FC08 /* LiteAutoCompleteDataSource.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LiteAutoCompleteDataSource.m; sourceTree = ""; }; 3C7B4E8B190FA6D200627DC3 /* NSTextFieldVerticallyAligned.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NSTextFieldVerticallyAligned.h; sourceTree = ""; }; 3C7B4E8C190FA6D200627DC3 /* NSTextFieldVerticallyAligned.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NSTextFieldVerticallyAligned.m; sourceTree = ""; }; 3C7B4E9D190FA7F900627DC3 /* logo-white@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "logo-white@2x.png"; sourceTree = ""; }; @@ -368,6 +383,8 @@ 3CDA2C5A1913B60F00A94967 /* icon-tags@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "icon-tags@2x.png"; sourceTree = ""; }; 3CE1CABF1C774F2B00D0ADD5 /* LoadMoreCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LoadMoreCell.h; sourceTree = ""; }; 3CE1CAC01C774F2B00D0ADD5 /* LoadMoreCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LoadMoreCell.m; sourceTree = ""; }; + 3CE30E002052BD8B00AF2E2A /* AutoCompleteTableContainer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AutoCompleteTableContainer.h; sourceTree = ""; }; + 3CE30E012052BD8B00AF2E2A /* AutoCompleteTableContainer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AutoCompleteTableContainer.m; sourceTree = ""; }; 3CFE546D201781A5006B673A /* libcrypto.1.1.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libcrypto.1.1.dylib; path = ../../../../third_party/openssl/libcrypto.1.1.dylib; sourceTree = ""; }; 3CFE547D201781A5006B673A /* libssl.1.1.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libssl.1.1.dylib; path = ../../../../third_party/openssl/libssl.1.1.dylib; sourceTree = ""; }; 69FC17F117E6534400B96425 /* TogglDesktop.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TogglDesktop.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -586,6 +603,24 @@ path = ../../../../third_party/MKPopoverColorWell; sourceTree = ""; }; + 3C6B246F203E01B70063FC08 /* AutoComplete */ = { + isa = PBXGroup; + children = ( + 3C6B2481203E01D80063FC08 /* AutoCompleteInput.h */, + 3C6B2483203E01D90063FC08 /* AutoCompleteInput.m */, + 3C6B2482203E01D80063FC08 /* AutoCompleteTable.h */, + 3C6B2484203E01D90063FC08 /* AutoCompleteTable.m */, + 3C6B247F203E01D60063FC08 /* AutoCompleteTableCell.h */, + 3C6B2480203E01D60063FC08 /* AutoCompleteTableCell.m */, + 3C6B2485203E01D90063FC08 /* AutoCompleteTableCell.xib */, + 3C6B24A2203FC8200063FC08 /* LiteAutoCompleteDataSource.h */, + 3C6B24A3203FC8200063FC08 /* LiteAutoCompleteDataSource.m */, + 3CE30E002052BD8B00AF2E2A /* AutoCompleteTableContainer.h */, + 3CE30E012052BD8B00AF2E2A /* AutoCompleteTableContainer.m */, + ); + name = AutoComplete; + sourceTree = ""; + }; 69FC17E817E6534400B96425 = { isa = PBXGroup; children = ( @@ -660,6 +695,7 @@ 69FC17FA17E6534400B96425 /* ui */ = { isa = PBXGroup; children = ( + 3C6B246F203E01B70063FC08 /* AutoComplete */, 3C068C681C22F25000874B9A /* MKColorWellCustom.h */, 3C068C691C22F25000874B9A /* MKColorWellCustom.m */, 746947F81AF3FE3E0024BED7 /* AutotrackerRuleItem.h */, @@ -1191,6 +1227,7 @@ 3C65A7E51DE5757A005586B4 /* continue_regular.pdf in Resources */, 3C65A7D41DE574E7005586B4 /* group_icon_open.pdf in Resources */, 74E857BC194F8854007A88B9 /* cacert.pem in Resources */, + 3C6B2489203E01D90063FC08 /* AutoCompleteTableCell.xib in Resources */, 95DBF8D718C48B300021FB41 /* logo.png in Resources */, 3CDA2C5B1913B61000A94967 /* icon-billable@2x.png in Resources */, 3CD30F441F58B02C006FAA0D /* MissingWSViewController.xib in Resources */, @@ -1299,6 +1336,7 @@ 74762BA118A139D4004433A9 /* NSUnstripedTableView.m in Sources */, C5DA1FBF17F1B08A001C4565 /* MainWindowController.m in Sources */, 747B74871A0AD28200BB3791 /* ConsoleViewController.m in Sources */, + 3C6B2488203E01D90063FC08 /* AutoCompleteTable.m in Sources */, 74AA9472180909F50000539F /* GTMHTTPFetchHistory.m in Sources */, 74E3CDC617FBABE400C3ADD3 /* Bugsnag.m in Sources */, 745126B619A28AA600390F47 /* Reachability.m in Sources */, @@ -1318,12 +1356,14 @@ 746947FA1AF3FE3E0024BED7 /* AutotrackerRuleItem.m in Sources */, 74E3CDCA17FBABE400C3ADD3 /* BugsnagEvent.m in Sources */, 74FE0D5C18E260A000ECFED2 /* MASShortcut+Monitoring.m in Sources */, + 3C6B2487203E01D90063FC08 /* AutoCompleteInput.m in Sources */, 748664D0188D6617006DB4C5 /* TimeEntryCellWithHeader.m in Sources */, 74098C331919899600CBDFB9 /* Utils.m in Sources */, 69FC180117E6534400B96425 /* main.m in Sources */, 74F1070118993FFE00E93BD5 /* FeedbackWindowController.m in Sources */, 74FE0D6018E260A000ECFED2 /* MASShortcutView.m in Sources */, 69FC180817E6534400B96425 /* AppDelegate.m in Sources */, + 3C6B24A4203FC8200063FC08 /* LiteAutoCompleteDataSource.m in Sources */, 3C068C6A1C22F25000874B9A /* MKColorWellCustom.m in Sources */, 3CA14529192DE32300414620 /* NSViewEscapable.m in Sources */, 3C0A571B1C22E12E00301D77 /* MKColorSwatchCell.m in Sources */, @@ -1353,6 +1393,8 @@ 74E3CDC817FBABE400C3ADD3 /* BugsnagConfiguration.m in Sources */, 743D782A182791FA00978BCC /* idler.c in Sources */, 74AA9473180909F50000539F /* GTMOAuth2Authentication.m in Sources */, + 3C6B2486203E01D90063FC08 /* AutoCompleteTableCell.m in Sources */, + 3CE30E022052BD8B00AF2E2A /* AutoCompleteTableContainer.m in Sources */, 3C0A571D1C22E12E00301D77 /* MKColorWell+Bindings.m in Sources */, C5DA1FC417F1B38B001C4565 /* LoginViewController.m in Sources */, 743D942F1827C633000E6F70 /* IdleEvent.m in Sources */, diff --git a/src/ui/osx/TogglDesktop/test2/AutoCompleteInput.h b/src/ui/osx/TogglDesktop/test2/AutoCompleteInput.h new file mode 100644 index 0000000000..f9582dd395 --- /dev/null +++ b/src/ui/osx/TogglDesktop/test2/AutoCompleteInput.h @@ -0,0 +1,29 @@ +// +// AutoComleteInput.h +// LiteComplete +// +// Created by Indrek Vändrik on 20/02/2018. +// Copyright © 2018 Toggl. All rights reserved. +// + +#import +#include +#import "AutoCompleteTable.h" +#import "AutoCompleteTableCell.h" +#import "AutoCompleteTableContainer.h" + +@interface AutoCompleteInput : NSTextField +@property NSNib *nibAutoCompleteTableCell; +@property AutoCompleteTableContainer *autocompleteTableContainer; +@property AutoCompleteTable *autocompleteTableView; +@property int posY; +@property int lastItemCount; +@property int maxVisibleItems; +@property int itemHeight; +@property NSLayoutConstraint *heightConstraint; +- (void)toggleTableView:(int)itemCount; +- (void)setPos:(int)posy; +- (void)hide; +- (void)resetTable; +- (void)showAutoComplete:(BOOL)show; +@end diff --git a/src/ui/osx/TogglDesktop/test2/AutoCompleteInput.m b/src/ui/osx/TogglDesktop/test2/AutoCompleteInput.m new file mode 100644 index 0000000000..805c88ed6d --- /dev/null +++ b/src/ui/osx/TogglDesktop/test2/AutoCompleteInput.m @@ -0,0 +1,170 @@ +// +// AutoComleteInput.m +// LiteComplete +// +// Created by Indrek Vändrik on 20/02/2018. +// Copyright © 2018 Toggl. All rights reserved. +// + +#import "AutoCompleteInput.h" + +@implementation AutoCompleteInput + +- (void)drawRect:(NSRect)dirtyRect +{ + [super drawRect:dirtyRect]; + + // Drawing code here. +} + +- (id)initWithCoder:(NSCoder *)coder +{ + self = [super initWithCoder:coder]; + if (self) + { + self.posY = 0; + self.itemHeight = 25; + self.maxVisibleItems = 6; + [self createAutocomplete]; + } + return self; +} + +- (void)createAutocomplete +{ + self.autocompleteTableContainer = [[AutoCompleteTableContainer alloc] initWithFrame:NSMakeRect(0, 0, 0, 0)]; + self.nibAutoCompleteTableCell = [[NSNib alloc] initWithNibNamed:@"AutoCompleteTableCell" bundle:nil]; + self.autocompleteTableView = [[AutoCompleteTable alloc] initWithFrame:NSMakeRect(0, 0, 0, 0)]; + [self.autocompleteTableView registerNib:self.nibAutoCompleteTableCell + forIdentifier :@"AutoCompleteTableCell"]; + + [self.autocompleteTableView setDelegate:self]; + + [self.autocompleteTableContainer setDocumentView:self.autocompleteTableView]; + [self.autocompleteTableContainer setAutohidesScrollers:YES]; + [self.autocompleteTableContainer setHasVerticalScroller:YES]; + [self.autocompleteTableContainer setHidden:YES]; + + self.autocompleteTableContainer.translatesAutoresizingMaskIntoConstraints = NO; +} + +- (void)setupAutocompleteConstraints +{ + // Set constraints to input field so autocomplete size is always connected to input + NSLayoutConstraint *leftConstraint = [NSLayoutConstraint constraintWithItem:self.autocompleteTableContainer attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeLeft multiplier:1 constant:0]; + + NSLayoutConstraint *rightConstraint = [NSLayoutConstraint constraintWithItem:self.autocompleteTableContainer attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeRight multiplier:1 constant:0]; + + NSLayoutConstraint *topConstraint = [NSLayoutConstraint constraintWithItem:self.autocompleteTableContainer attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeBottom multiplier:1 constant:1]; + + self.heightConstraint = [NSLayoutConstraint constraintWithItem:self.autocompleteTableContainer attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1 constant:self.itemHeight]; + + [NSLayoutConstraint activateConstraints:[NSArray arrayWithObjects:leftConstraint, rightConstraint, self.heightConstraint, topConstraint, nil]]; +} + +- (void)setPos:(int)posy +{ + self.posY = posy; +} + +- (void)toggleTableView:(int)itemCount +{ + NSLog(@"// ** Toggle table (items: %d) ** //", itemCount); + if (itemCount > 0 || (itemCount == 0 && self.lastItemCount > 0)) + { + if (self.autocompleteTableContainer.hidden) + { + if (self.heightConstraint == nil) + { + [self.window.contentView addSubview:self.autocompleteTableContainer positioned:NSWindowAbove relativeTo:nil]; + [self setupAutocompleteConstraints]; + } + [self showAutoComplete:YES]; + } + [self updateDropdownHeight:itemCount]; + } + else if (self.autocompleteTableContainer != nil) + { + [self showAutoComplete:NO]; + } + self.lastItemCount = itemCount; +} + +- (void)updateDropdownHeight:(int)count +{ + int h = MIN((count * self.itemHeight), self.posY - 50); + + self.heightConstraint.constant = h; + NSLog(@"Update table position | H: %d, POSY: %d", h, self.posY); +} + +- (void)keyUp:(NSEvent *)event +{ + // NSLog(@"EventCode: %hu", [event keyCode]); + if ([event keyCode] == kVK_DownArrow) + { + if ([event modifierFlags] & NSShiftKeyMask) + { + [super keyUp:event]; + return; + } + if (self.autocompleteTableContainer.isHidden) + { + [self toggleTableView:(int)self.autocompleteTableView.numberOfRows]; + return; + } + } + else if (event.keyCode == kVK_Escape) + { + // Hide autocomplete list + if (self.autocompleteTableContainer != nil) + { + [self showAutoComplete:NO]; + return; + } + } + else if ((event.keyCode == kVK_Return) || (event.keyCode == kVK_ANSI_KeypadEnter)) + { + if (!self.autocompleteTableView.isHidden) + { + [self showAutoComplete:NO]; + return; + } + } + [super keyUp:event]; +} + +- (void)hide +{ + [self showAutoComplete:NO]; + [self setHidden:YES]; +} + +- (void)resetTable +{ + [self showAutoComplete:NO]; + [self.autocompleteTableView resetSelected]; +} + +- (void)showAutoComplete:(BOOL)show +{ + [self.autocompleteTableContainer setHidden:!show]; + [self.autocompleteTableView setHidden:!show]; +} + +- (BOOL)becomeFirstResponder +{ + BOOL success = [super becomeFirstResponder]; + + if (success && self.isEditable) + { + NSTextView *textField = (NSTextView *)[self currentEditor]; + if ([textField respondsToSelector:@selector(setInsertionPointColor:)]) + { + [textField setInsertionPointColor:[self textColor]]; + } + } + return success; +} + +@end diff --git a/src/ui/osx/TogglDesktop/test2/AutoCompleteTable.h b/src/ui/osx/TogglDesktop/test2/AutoCompleteTable.h new file mode 100644 index 0000000000..b132f21161 --- /dev/null +++ b/src/ui/osx/TogglDesktop/test2/AutoCompleteTable.h @@ -0,0 +1,20 @@ +// +// AutoCompleteTable.h +// LiteComplete +// +// Created by Indrek Vändrik on 21/02/2018. +// Copyright © 2018 Toggl. All rights reserved. +// + +#import +#import "AutoCompleteTableCell.h" + +@interface AutoCompleteTable : NSTableView +@property NSInteger lastSelected; +- (void)nextItem; +- (void)previousItem; +- (AutoCompleteTableCell *)getSelectedCell:(NSInteger)row; +- (void)setFirstRowAsSelected; +- (void)setCurrentSelected:(NSInteger)index; +- (void)resetSelected; +@end diff --git a/src/ui/osx/TogglDesktop/test2/AutoCompleteTable.m b/src/ui/osx/TogglDesktop/test2/AutoCompleteTable.m new file mode 100644 index 0000000000..04a112f334 --- /dev/null +++ b/src/ui/osx/TogglDesktop/test2/AutoCompleteTable.m @@ -0,0 +1,112 @@ +// +// AutoCompleteTable.m +// LiteComplete +// +// Created by Indrek Vändrik on 21/02/2018. +// Copyright © 2018 Toggl. All rights reserved. +// + +#import "AutoCompleteTable.h" +#include + +@implementation AutoCompleteTable + +- (instancetype)initWithFrame:(NSRect)frame +{ + self = [super initWithFrame:frame]; + if (self) + { + self.lastSelected = -1; + NSTableColumn *column = [[NSTableColumn alloc] initWithIdentifier:@"column"]; + [self addTableColumn:column]; + [self setHeaderView:nil]; + [self setSelectionHighlightStyle:NSTableViewSelectionHighlightStyleNone]; + [self setIntercellSpacing:NSMakeSize(0, 0)]; + } + return self; +} + +- (void)drawGridInClipRect:(NSRect)clipRect +{ + NSRect lastRowRect = [self rectOfRow:[self numberOfRows] - 1]; + NSRect myClipRect = NSMakeRect(0, 0, lastRowRect.size.width, NSMaxY(lastRowRect)); + NSRect finalClipRect = NSIntersectionRect(clipRect, myClipRect); + + [super drawGridInClipRect:finalClipRect]; +} + +- (void)setFirstRowAsSelected +{ + [self deselectAll:nil]; + + NSIndexSet *indexSet = [NSIndexSet indexSetWithIndex:0]; + [self selectRowIndexes:indexSet byExtendingSelection:NO]; + + [self setCurrentSelected:0]; +} + +- (void)nextItem +{ + if (self.lastSelected < self.numberOfRows - 1) + { + [self setCurrentSelected:self.lastSelected + 1]; + } +} + +- (void)previousItem +{ + if (self.lastSelected > 0) + { + [self setCurrentSelected:self.lastSelected - 1]; + } +} + +- (void)setCurrentSelected:(NSInteger)index +{ + [self resetSelected]; + self.lastSelected = index; + AutoCompleteTableCell *cell = [self getSelectedCell:index]; + if (cell != nil) + { + [cell setFocused:YES]; + [self scrollRowToVisible:self.lastSelected]; + } +} + +- (AutoCompleteTableCell *)getSelectedCell:(NSInteger)row +{ + if (row < 0) + { + return nil; + } + + NSView *latestView = [self rowViewAtRow:row makeIfNecessary:YES]; + + if (latestView == nil) + { + return nil; + } + + for (NSView *subview in [latestView subviews]) + { + if ([subview isKindOfClass:[AutoCompleteTableCell class]]) + { + return (AutoCompleteTableCell *)subview; + } + } + + return nil; +} + +- (void)resetSelected +{ + if (self.lastSelected != -1) + { + AutoCompleteTableCell *cell = [self getSelectedCell:self.lastSelected]; + [cell setFocused:NO]; + } + self.lastSelected = -1; +} + +@end + diff --git a/src/ui/osx/TogglDesktop/test2/AutoCompleteTableCell.h b/src/ui/osx/TogglDesktop/test2/AutoCompleteTableCell.h new file mode 100644 index 0000000000..53a2edc049 --- /dev/null +++ b/src/ui/osx/TogglDesktop/test2/AutoCompleteTableCell.h @@ -0,0 +1,19 @@ +// +// AutoCompleteTableCell.h +// LiteComplete +// +// Created by Indrek Vändrik on 20/02/2018. +// Copyright © 2018 Toggl. All rights reserved. +// + +#import +#import "AutocompleteItem.h" +#import "ConvertHexColor.h" + +@interface AutoCompleteTableCell : NSTableCellView +@property (weak) IBOutlet NSTextField *cellDescription; +@property (weak) IBOutlet NSBox *backgroundBox; +- (void)render:(AutocompleteItem *)view_item; +- (void)setFocused:(BOOL)focus; +- (NSMutableAttributedString *)setFormatedText:(AutocompleteItem *)view_item; +@end diff --git a/src/ui/osx/TogglDesktop/test2/AutoCompleteTableCell.m b/src/ui/osx/TogglDesktop/test2/AutoCompleteTableCell.m new file mode 100644 index 0000000000..c4944e45c5 --- /dev/null +++ b/src/ui/osx/TogglDesktop/test2/AutoCompleteTableCell.m @@ -0,0 +1,113 @@ +// +// AutoCompleteTableCell.m +// LiteComplete +// +// Created by Indrek Vändrik on 20/02/2018. +// Copyright © 2018 Toggl. All rights reserved. +// + +#import "AutoCompleteTableCell.h" + +@implementation AutoCompleteTableCell + +- (void)drawRect:(NSRect)dirtyRect +{ + [super drawRect:dirtyRect]; + + // Drawing code here. +} + +- (void)render:(AutocompleteItem *)view_item +{ + NSAssert([NSThread isMainThread], @"Rendering stuff should happen on main thread"); + + [self.cellDescription setAttributedStringValue:[self setFormatedText:view_item]]; + self.cellDescription.toolTip = view_item.Text; +} + +- (void)setFocused:(BOOL)focus +{ + NSString *color = @"#ffffff"; + + if (focus == YES) + { + color = @"#f4f4f4"; + } + [self.backgroundBox setFillColor:[ConvertHexColor hexCodeToNSColor:color]]; +} + +- (NSMutableAttributedString *)setFormatedText:(AutocompleteItem *)view_item +{ + // Format is: Description - TaskName ·ProjectName - ClientName + NSMutableAttributedString *string; + + string = [[NSMutableAttributedString alloc] initWithString:view_item.Description]; + + [string setAttributes: + @{ + NSFontAttributeName : [NSFont systemFontOfSize:[NSFont systemFontSize]], + NSForegroundColorAttributeName:[NSColor controlTextColor] + } + range:NSMakeRange(0, [string length])]; + + if (view_item.ProjectID == 0) + { + return string; + } + + if (view_item.TaskID != 0) + { + [string appendAttributedString:[[NSMutableAttributedString alloc] initWithString:@" -"]]; + NSMutableAttributedString *task = [[NSMutableAttributedString alloc] initWithString:view_item.TaskLabel]; + + [task setAttributes: + @{ + NSFontAttributeName : [NSFont systemFontOfSize:[NSFont systemFontSize]], + NSForegroundColorAttributeName:[NSColor controlTextColor] + } + range:NSMakeRange(0, [task length])]; + [string appendAttributedString:task]; + } + if ([string length] > 0) + { + [string appendAttributedString:[[NSMutableAttributedString alloc] initWithString:@" "]]; + } + + NSMutableAttributedString *projectDot = [[NSMutableAttributedString alloc] initWithString:@"•"]; + + [projectDot setAttributes: + @{ + NSFontAttributeName : [NSFont systemFontOfSize:[NSFont systemFontSize]], + NSForegroundColorAttributeName:[ConvertHexColor hexCodeToNSColor:view_item.ProjectColor] + } + range:NSMakeRange(0, [projectDot length])]; + [string appendAttributedString:projectDot]; + + NSMutableAttributedString *projectName = [[NSMutableAttributedString alloc] initWithString:view_item.ProjectLabel]; + + [projectName setAttributes: + @{ + NSFontAttributeName : [NSFont systemFontOfSize:[NSFont systemFontSize]], + NSForegroundColorAttributeName:[ConvertHexColor hexCodeToNSColor:view_item.ProjectColor] + } + range:NSMakeRange(0, [projectName length])]; + + [string appendAttributedString:projectName]; + + if ([view_item.ClientLabel length] > 0) + { + NSMutableAttributedString *clientName = [[NSMutableAttributedString alloc] initWithString:[@" - " stringByAppendingString:view_item.ClientLabel]]; + + [clientName setAttributes: + @{ + NSFontAttributeName : [NSFont systemFontOfSize:[NSFont systemFontSize]], + NSForegroundColorAttributeName:[NSColor disabledControlTextColor] + } + range:NSMakeRange(0, [clientName length])]; + [string appendAttributedString:clientName]; + } + + return string; +} + +@end diff --git a/src/ui/osx/TogglDesktop/test2/AutoCompleteTableCell.xib b/src/ui/osx/TogglDesktop/test2/AutoCompleteTableCell.xib new file mode 100644 index 0000000000..09c9e06247 --- /dev/null +++ b/src/ui/osx/TogglDesktop/test2/AutoCompleteTableCell.xib @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/ui/osx/TogglDesktop/test2/AutoCompleteTableContainer.h b/src/ui/osx/TogglDesktop/test2/AutoCompleteTableContainer.h new file mode 100644 index 0000000000..0ff521f8dc --- /dev/null +++ b/src/ui/osx/TogglDesktop/test2/AutoCompleteTableContainer.h @@ -0,0 +1,13 @@ +// +// AutoCompleteTableContainer.h +// TogglDesktop +// +// Created by Indrek Vändrik on 09/03/2018. +// Copyright © 2018 Alari. All rights reserved. +// + +#import + +@interface AutoCompleteTableContainer : NSScrollView +@property NSShadow *dropShadow; +@end diff --git a/src/ui/osx/TogglDesktop/test2/AutoCompleteTableContainer.m b/src/ui/osx/TogglDesktop/test2/AutoCompleteTableContainer.m new file mode 100644 index 0000000000..d76e41cab8 --- /dev/null +++ b/src/ui/osx/TogglDesktop/test2/AutoCompleteTableContainer.m @@ -0,0 +1,37 @@ +// +// AutoCompleteTableContainer.m +// TogglDesktop +// +// Created by Indrek Vändrik on 09/03/2018. +// Copyright © 2018 Alari. All rights reserved. +// + +#import "AutoCompleteTableContainer.h" + +@implementation AutoCompleteTableContainer + +- (instancetype)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self) + { + [self setWantsLayer:YES]; + self.layer.masksToBounds = YES; + self.dropShadow = [[NSShadow alloc] init]; + [self.dropShadow setShadowColor:[[NSColor grayColor] + colorWithAlphaComponent:0.7f]]; + [self.dropShadow setShadowOffset:NSMakeSize(0, 5.0)]; + [self.dropShadow setShadowBlurRadius:5.0]; + } + return self; +} + +- (void)drawRect:(NSRect)dirtyRect +{ + [super drawRect:dirtyRect]; + [self.window.contentView setWantsLayer:YES]; + [self setWantsLayer:YES]; + [self setShadow:self.dropShadow]; +} + +@end diff --git a/src/ui/osx/TogglDesktop/test2/LiteAutoCompleteDataSource.h b/src/ui/osx/TogglDesktop/test2/LiteAutoCompleteDataSource.h new file mode 100644 index 0000000000..e09aa945e8 --- /dev/null +++ b/src/ui/osx/TogglDesktop/test2/LiteAutoCompleteDataSource.h @@ -0,0 +1,27 @@ +// +// LiteAutoCompleteDataSource.h +// TogglDesktop +// +// Created by Indrek Vändrik on 23/02/2018. +// Copyright © 2018 Alari. All rights reserved. +// + +#import +#import +#import "AutoCompleteInput.h" +#import "AutocompleteItem.h" +#import "AutoCompleteTableCell.h" + +@interface LiteAutoCompleteDataSource : NSObject +@property NSMutableArray *orderedKeys; +@property NSMutableArray *filteredOrderedKeys; +@property NSMutableDictionary *dictionary; +@property NSString *currentFilter; +@property NSInteger textLength; +@property AutoCompleteInput *input; +- (NSString *)completedString:(NSString *)partialString; +- (AutocompleteItem *)get:(NSString *)key; +- (void)setFilter:(NSString *)filter; +- (id)initWithNotificationName:(NSString *)notificationName; +- (AutocompleteItem *)itemAtIndex:(NSInteger)row; +@end diff --git a/src/ui/osx/TogglDesktop/test2/LiteAutoCompleteDataSource.m b/src/ui/osx/TogglDesktop/test2/LiteAutoCompleteDataSource.m new file mode 100644 index 0000000000..42768b9b01 --- /dev/null +++ b/src/ui/osx/TogglDesktop/test2/LiteAutoCompleteDataSource.m @@ -0,0 +1,213 @@ +// +// LiteAutoCompleteDataSource.m +// TogglDesktop +// +// Created by Indrek Vändrik on 23/02/2018. +// Copyright © 2018 Alari. All rights reserved. +// + +#import "LiteAutoCompleteDataSource.h" + +@implementation LiteAutoCompleteDataSource + +extern void *ctx; + +- (id)initWithNotificationName:(NSString *)notificationName +{ + self = [super init]; + + self.orderedKeys = [[NSMutableArray alloc] init]; + self.filteredOrderedKeys = [[NSMutableArray alloc] init]; + self.dictionary = [[NSMutableDictionary alloc] init]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(startDisplayAutocomplete:) + name:notificationName + object:nil]; + return self; +} + +- (NSString *)completedString:(NSString *)partialString +{ + @synchronized(self) + { + for (NSString *text in self.filteredOrderedKeys) + { + if ([[text commonPrefixWithString:partialString + options:NSCaseInsensitiveSearch] length] == [partialString length]) + { + return text; + } + } + } + return @""; +} + +- (NSString *)get:(NSString *)key +{ + NSString *object = nil; + + @synchronized(self) + { + object = [self.dictionary objectForKey:key]; + } + return object; +} + +- (AutocompleteItem *)itemAtIndex:(NSInteger)row +{ + AutocompleteItem *item; + + @synchronized(self) + { + item = [self.filteredOrderedKeys objectAtIndex:row]; + } + return item; +} + +- (void)startDisplayAutocomplete:(NSNotification *)notification +{ + [self performSelectorOnMainThread:@selector(displayAutocomplete:) + withObject:notification.object + waitUntilDone:NO]; +} + +- (void)displayAutocomplete:(NSMutableArray *)entries +{ + NSAssert([NSThread isMainThread], @"Rendering stuff should happen on main thread"); + + @synchronized(self) + { + [self.orderedKeys removeAllObjects]; + [self.dictionary removeAllObjects]; + for (AutocompleteItem *item in entries) + { + NSString *key = item.Text; + if ([self.dictionary objectForKey:key] == nil) + { + [self.orderedKeys addObject:item]; + [self.dictionary setObject:item forKey:key]; + } + } + + // self.table.usesDataSource = YES; + if (self.input.autocompleteTableView.dataSource == nil) + { + self.input.autocompleteTableView.dataSource = self; + } + + [self setFilter:self.currentFilter]; + } +} + +- (void)reload +{ + NSAssert([NSThread isMainThread], @"Rendering stuff should happen on main thread"); + + [self.input toggleTableView:(int)[self.filteredOrderedKeys count]]; + [self.input.autocompleteTableView reloadData]; +} + +- (void)findFilter:(NSString *)filter +{ + @synchronized(self) + { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + // Code that runs async + NSMutableArray *filtered = [[NSMutableArray alloc] init]; + for (int i = 0; i < self.orderedKeys.count; i++) + { + AutocompleteItem *item = self.orderedKeys[i]; + NSString *key = item.Text; + + NSArray *stringArray = [filter componentsSeparatedByString:@" "]; + if (stringArray.count > 1) + { + // Filter is more than 1 word. Let's search for all the words entered. + int foundCount = 0; + for (int j = 0; j < stringArray.count; j++) + { + NSString *splitFilter = stringArray[j]; + + if ([key rangeOfString:splitFilter options:NSCaseInsensitiveSearch].location != NSNotFound) + { + foundCount++; + if ([key length] > self.textLength) + { + self.textLength = [key length]; + } + + if (foundCount == stringArray.count) + { + [filtered addObject:item]; + } + } + } + } + else + { + // Single word filter + if ([key rangeOfString:filter options:NSCaseInsensitiveSearch].location != NSNotFound) + { + if ([key length] > self.textLength) + { + self.textLength = [key length]; + } + [filtered addObject:item]; + } + } + } + // NSLog(@" FILTERED: %@", [filtered count]); + self.filteredOrderedKeys = filtered; + dispatch_sync(dispatch_get_main_queue(), ^{ + // This will be called on the main thread, + // when async calls finish + [self reload]; + }); + }); + } +} + +- (void)setFilter:(NSString *)filter +{ + self.textLength = 0; + @synchronized(self) + { + bool lastFilterWasNonEmpty = ((filter == nil || filter.length == 0) && (self.currentFilter != nil && self.currentFilter.length > 0)); + + self.currentFilter = filter; + if (filter == nil || filter.length == 0) + { + self.filteredOrderedKeys = [NSMutableArray arrayWithArray:self.orderedKeys]; + if (lastFilterWasNonEmpty) + { + [self reload]; + } + else + { + [self.input.autocompleteTableView reloadData]; + } + return; + } + NSString *trimmedFilter = [filter stringByTrimmingCharactersInSet: + [NSCharacterSet whitespaceAndNewlineCharacterSet]]; + [self findFilter:trimmedFilter]; + } +} + +- (NSInteger)numberOfRowsInTableView:(NSTableView *)tv +{ + NSUInteger result = 0; + + @synchronized(self) + { + result = [self.filteredOrderedKeys count]; + } + + // NSLog(@"----- ROWS: %lu", (unsigned long)result); + + return result; +} + +@end + diff --git a/src/user.cc b/src/user.cc index 1b0819be52..5f3766506c 100644 --- a/src/user.cc +++ b/src/user.cc @@ -62,8 +62,17 @@ Project *User::CreateProject( p->SetColorCode(project_color); } - related.Projects.push_back(p); - + // We should push the project to correct alphabetical position + // (since we try to avoid sorting the large list) + for (std::vector::const_iterator it = + related.Projects.begin(); + it != related.Projects.end(); it++) { + Project *pr = *it; + if (Poco::UTF8::icompare(p->Name(), pr->Name()) < 0) { + related.Projects.insert(it,p); + break; + } + } return p; } @@ -74,7 +83,17 @@ Client *User::CreateClient( c->SetWID(workspace_id); c->SetName(client_name); c->SetUID(ID()); - related.Clients.push_back(c); + + // We should push the project to correct alphabetical position + // (since we try to avoid sorting the large list) + for (std::vector::const_iterator it = + related.Clients.begin(); + it != related.Clients.end(); it++) { + Client *cl = *it; + if (Poco::UTF8::icompare(c->Name(), cl->Name()) < 0) { + related.Clients.insert(it,c); + } + } return c; }