/
CPComboBox.j
1291 lines (1015 loc) · 34.8 KB
/
CPComboBox.j
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
* CPComboBox.j
* AppKit
*
* Created by Aparajita Fishman.
* Copyright (c) 2012, The Cappuccino Foundation
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
@import "CPText.j"
@import "CPTextField.j"
@import "_CPPopUpList.j"
// TODO : should conform to protocol CPTextFieldDelegate
@protocol CPComboBoxDelegate <CPObject>
@optional
- (void)comboBoxSelectionIsChanging:(CPNotification)aNotification;
- (void)comboBoxSelectionDidChange:(CPNotification)aNotification;
- (void)comboBoxWillPopUp:(CPNotification)aNotification;
- (void)comboBoxWillDismiss:(CPNotification)aNotification;
@end
@protocol CPComboBoxDataSource <CPObject>
@optional
- (CPString)comboBox:(CPComboBox)aComboBox completedString:(CPString)uncompletedString;
- (id)comboBox:(CPComboBox)aComboBox objectValueForItemAtIndex:(int)index;
- (int)comboBox:(CPComboBox)aComboBox indexOfItemWithStringValue:(CPString)stringValue;
- (int)numberOfItemsInComboBox:(CPComboBox)aComboBox;
@end
CPComboBoxSelectionDidChangeNotification = @"CPComboBoxSelectionDidChangeNotification";
CPComboBoxSelectionIsChangingNotification = @"CPComboBoxSelectionIsChangingNotification";
CPComboBoxWillDismissNotification = @"CPComboBoxWillDismissNotification";
CPComboBoxWillPopUpNotification = @"CPComboBoxWillPopUpNotification";
CPComboBoxStateButtonBordered = CPThemeState("button-bordered");
var CPComboBoxDelegate_comboBoxSelectionIsChanging_ = 1 << 0,
CPComboBoxDelegate_comboBoxSelectionDidChange_ = 1 << 1,
CPComboBoxDelegate_comboBoxWillPopUp_ = 1 << 2,
CPComboBoxDelegate_comboBoxWillDismiss_ = 1 << 3;
var CPComboBoxTextSubview = @"text",
CPComboBoxButtonSubview = @"button",
CPComboBoxDefaultNumberOfVisibleItems = 5,
CPComboBoxFocusRingWidth = -1;
@implementation CPComboBox : CPTextField
{
BOOL _canComplete;
BOOL _completes;
BOOL _forceSelection;
BOOL _hasVerticalScroller;
BOOL _popUpButtonCausedResign;
BOOL _usesDataSource;
CGSize _intercellSpacing;
CPArray _items;
id<CPComboBoxDataSource> _dataSource;
CPInteger _implementedDelegateComboBoxMethods;
CPString _selectedStringValue;
float _itemHeight;
int _numberOfVisibleItems;
_CPPopUpList _listDelegate;
}
+ (CPString)defaultThemeClass
{
return "combobox";
}
+ (CPDictionary)themeAttributes
{
return @{
@"popup-button-size": CGSizeMake(21.0, 29.0),
@"border-inset": CGInsetMake(3.0, 3.0, 3.0, 3.0),
};
}
+ (Class)_binderClassForBinding:(CPString)aBinding
{
if (aBinding === CPContentBinding || aBinding === CPContentValuesBinding)
return [_CPComboBoxContentBinder class];
return [super _binderClassForBinding:aBinding];
}
- (id)initWithFrame:(CGRect)aFrame
{
self = [super initWithFrame:aFrame];
if (self)
[self _initComboBox];
return self;
}
- (void)_initComboBox
{
_items = [CPArray array];
// _listClass = [_CPPopUpList class];
_usesDataSource = NO;
_completes = NO;
_canComplete = NO;
_numberOfVisibleItems = CPComboBoxDefaultNumberOfVisibleItems;
_forceSelection = NO;
_hasVerticalScroller = YES;
_selectedStringValue = @"";
_popUpButtonCausedResign = NO;
[self setTheme:[CPTheme defaultTheme]];
[self setBordered:YES];
[self setBezeled:YES];
[self setEditable:YES];
[self setThemeState:CPComboBoxStateButtonBordered];
}
#pragma mark Setting Display Attributes
- (BOOL)hasVerticalScroller
{
return _hasVerticalScroller;
}
- (void)setHasVerticalScroller:(BOOL)flag
{
flag = !!flag;
if (_hasVerticalScroller === flag)
return;
_hasVerticalScroller = flag;
if (_listDelegate)
[[_listDelegate scrollView] setHasVerticalScroller:_hasVerticalScroller];
}
- (CGSize)intercellSpacing
{
return [[_listDelegate tableView] intercellSpacing];
}
- (void)setIntercellSpacing:(CGSize)aSize
{
if (_intercellSpacing && CGSizeEqualToSize(aSize, _intercellSpacing))
return;
_intercellSpacing = aSize;
if (_listDelegate)
[[_listDelegate tableView] setIntercellSpacing:_intercellSpacing];
}
- (BOOL)isButtonBordered
{
return [self hasThemeState:CPComboBoxStateButtonBordered];
}
- (void)setButtonBordered:(BOOL)flag
{
if (!!flag)
[self setThemeState:CPComboBoxStateButtonBordered];
else
[self unsetThemeState:CPComboBoxStateButtonBordered];
}
- (float)itemHeight
{
return [[_listDelegate tableView] rowHeight];
}
- (void)setItemHeight:(float)itemHeight
{
if (itemHeight === _itemHeight)
return;
_itemHeight = itemHeight;
if (_listDelegate)
{
[[_listDelegate tableView] setRowHeight:_itemHeight];
// FIXME: This shouldn't be necessary, but CPTableView does not tile after setRowHeight
[[_listDelegate tableView] reloadData];
}
}
- (int)numberOfVisibleItems
{
return _numberOfVisibleItems;
}
- (void)setNumberOfVisibleItems:(int)visibleItems
{
// There should always be at least 1 visible item!
_numberOfVisibleItems = MAX(visibleItems, 1);
}
#pragma mark Setting a Delegate
- (id <CPComboBoxDelegate>)delegate
{
return [super delegate];
}
/*!
Sets the CPComboBox delegate. Note that although the Cocoa
docs say that the delegate must conform to the NSComboBoxDelegate
protocol, in actual fact it doesn't. Also note that the same
delegate may conform to the NSTextFieldDelegate protocol.
*/
- (void)setDelegate:(id <CPComboBoxDelegate>)aDelegate
{
var delegate = [self delegate];
if (aDelegate === delegate)
return;
_implementedDelegateComboBoxMethods = 0;
if (aDelegate)
{
if ([aDelegate respondsToSelector:@selector(comboBoxSelectionIsChanging:)])
_implementedDelegateComboBoxMethods |= CPComboBoxDelegate_comboBoxSelectionIsChanging_;
if ([aDelegate respondsToSelector:@selector(comboBoxSelectionDidChange:)])
_implementedDelegateComboBoxMethods |= CPComboBoxDelegate_comboBoxSelectionDidChange_;
if ([aDelegate respondsToSelector:@selector(comboBoxWillPopUp:)])
_implementedDelegateComboBoxMethods |= CPComboBoxDelegate_comboBoxWillPopUp_;
if ([aDelegate respondsToSelector:@selector(comboBoxWillDismiss:)])
_implementedDelegateComboBoxMethods |= CPComboBoxDelegate_comboBoxWillDismiss_;
}
[super setDelegate:aDelegate];
}
#pragma mark Setting a Data Source
- (id <CPComboBoxDataSource>)dataSource
{
if (!_usesDataSource)
[self _dataSourceWarningForMethod:_cmd condition:NO];
return _dataSource;
}
- (void)setDataSource:(id <CPComboBoxDataSource>)aSource
{
if (!_usesDataSource)
{
[self _dataSourceWarningForMethod:_cmd condition:NO];
}
else if (_dataSource !== aSource)
{
if (![aSource respondsToSelector:@selector(numberOfItemsInComboBox:)] ||
![aSource respondsToSelector:@selector(comboBox:objectValueForItemAtIndex:)])
{
CPLog.warn("Illegal %s data source (%s). Must implement numberOfItemsInComboBox: and comboBox:objectValueForItemAtIndex:", [self className], [aSource description]);
}
else
{
_dataSource = aSource;
}
}
}
- (BOOL)usesDataSource
{
return _usesDataSource;
}
- (void)setUsesDataSource:(BOOL)flag
{
flag = !!flag;
if (_usesDataSource === flag)
return;
_usesDataSource = flag;
// Cocoa empties the internal item list if usesDataSource is YES
if (_usesDataSource)
[_items removeAllObjects];
[self reloadData];
}
#pragma mark Working with an Internal List
- (void)addItemsWithObjectValues:(CPArray)objects
{
[_items addObjectsFromArray:objects];
[self reloadDataSourceForSelector:_cmd];
}
- (void)addItemWithObjectValue:(id)anObject
{
[_items addObject:anObject];
[self reloadDataSourceForSelector:_cmd];
}
- (void)insertItemWithObjectValue:(id)anObject atIndex:(int)anIndex
{
// Issue the warning first, because removeObjectAtIndex may raise
if (_usesDataSource)
[self _dataSourceWarningForMethod:_cmd condition:YES];
[_items insertObject:anObject atIndex:anIndex];
[self reloadData];
}
/*!
Returns the internal array of items. NOTE: Unlike Cocoa the array is mutable,
since all arrays in Objective-J are mutable. But you should treat it as
an immutable array. Do <b>NOT</b> attempt to change the returned array in any way.
If usesDataSource is YES, a warning is logged and an empty array is returned.
*/
- (CPArray)objectValues
{
if (_usesDataSource)
[self _dataSourceWarningForMethod:_cmd condition:YES];
return _items;
}
- (void)removeAllItems
{
[_items removeAllObjects];
[self reloadDataSourceForSelector:_cmd];
}
- (void)removeItemAtIndex:(int)index
{
// Issue the warning first, because removeObjectAtIndex may raise
if (_usesDataSource)
[self _dataSourceWarningForMethod:_cmd condition:YES];
[_items removeObjectAtIndex:index];
[self reloadData];
}
- (void)removeItemWithObjectValue:(id)anObject
{
[_items removeObject:anObject];
[self reloadDataSourceForSelector:_cmd];
}
- (int)numberOfItems
{
if (_usesDataSource)
return [_dataSource numberOfItemsInComboBox:self];
else
return _items.length;
}
#pragma mark Manipulating the Displayed List
/*!
Returns the delegate to be used when creating the pop up list.
*/
- (_CPPopUpList)listDelegate
{
return _listDelegate;
}
/*!
Sets the delegate to be used when creating the pop up list.
By default this is _CPPopUpList. If you are using a subclass
of _CPPopUpList, call this method with your subclass.
*/
- (void)setListDelegate:(_CPPopUpList)aDelegate
{
if (_listDelegate === aDelegate)
return;
[self _removeObserversForListDelegate:_listDelegate];
_listDelegate = aDelegate;
// We only add the observers if the CPComboBox is displayed
if ([self window])
[self _addObserversForListDelegate:_listDelegate]
// Apply our text style to the list
[_listDelegate setFont:[self font]];
[_listDelegate setAlignment:[self alignment]];
[[_listDelegate scrollView] setHasVerticalScroller:_hasVerticalScroller];
if (_intercellSpacing)
[[_listDelegate tableView] setIntercellSpacing:_intercellSpacing];
if (_itemHeight)
[[_listDelegate tableView] setRowHeight:_itemHeight];
}
- (void)_addObserversForListDelegate:(_CPPopUpList)aDelegate
{
if (!aDelegate)
return;
var defaultCenter = [CPNotificationCenter defaultCenter];
[defaultCenter addObserver:self
selector:@selector(comboBoxWillPopUp:)
name:_CPPopUpListWillPopUpNotification
object:aDelegate];
[defaultCenter addObserver:self
selector:@selector(comboBoxWillDismiss:)
name:_CPPopUpListWillDismissNotification
object:aDelegate];
[defaultCenter addObserver:self
selector:@selector(listDidDismiss:)
name:_CPPopUpListDidDismissNotification
object:aDelegate];
[defaultCenter addObserver:self
selector:@selector(itemWasClicked:)
name:_CPPopUpListItemWasClickedNotification
object:aDelegate];
[[aDelegate scrollView] setHasVerticalScroller:_hasVerticalScroller];
var tableView = [aDelegate tableView];
[defaultCenter addObserver:self
selector:@selector(comboBoxSelectionIsChanging:)
name:CPTableViewSelectionIsChangingNotification
object:tableView];
[defaultCenter addObserver:self
selector:@selector(comboBoxSelectionDidChange:)
name:CPTableViewSelectionDidChangeNotification
object:tableView];
}
- (void)_removeObserversForListDelegate:(_CPPopUpList)aDelegate
{
if (!aDelegate)
return;
var defaultCenter = [CPNotificationCenter defaultCenter];
[defaultCenter removeObserver:self name:_CPPopUpListWillPopUpNotification object:aDelegate];
[defaultCenter removeObserver:self name:_CPPopUpListWillDismissNotification object:aDelegate];
[defaultCenter removeObserver:self name:_CPPopUpListDidDismissNotification object:aDelegate];
[defaultCenter removeObserver:self name:_CPPopUpListItemWasClickedNotification object:aDelegate];
var oldTableView = [aDelegate tableView];
if (oldTableView)
{
[defaultCenter removeObserver:self name:CPTableViewSelectionIsChangingNotification object:oldTableView];
[defaultCenter removeObserver:self name:CPTableViewSelectionDidChangeNotification object:oldTableView];
}
}
- (int)indexOfItemWithObjectValue:(id)anObject
{
if (_usesDataSource)
[self _dataSourceWarningForMethod:_cmd condition:YES];
return [_items indexOfObject:anObject];
}
- (id)itemObjectValueAtIndex:(int)index
{
if (_usesDataSource)
[self _dataSourceWarningForMethod:_cmd condition:YES];
return [_items objectAtIndex:index];
}
- (void)noteNumberOfItemsChanged
{
[[_listDelegate tableView] noteNumberOfRowsChanged];
}
- (void)scrollItemAtIndexToTop:(int)index
{
[_listDelegate scrollItemAtIndexToTop:index];
}
- (void)scrollItemAtIndexToVisible:(int)index
{
[[_listDelegate tableView] scrollRowToVisible:index];
}
- (void)reloadData
{
[[_listDelegate tableView] reloadData];
}
/*! @ignore */
- (void)popUpList
{
if (!_listDelegate)
[self setListDelegate:[[_CPPopUpList alloc] initWithDataSource:self]];
// Note the offset here is 1 less than the focus ring width because the outer edge
// of the focus ring is very transparent and it looks better if the list is closer.
if (CPComboBoxFocusRingWidth < 0)
{
var inset = [self currentValueForThemeAttribute:@"border-inset"];
CPComboBoxFocusRingWidth = inset.bottom;
}
[_listDelegate popUpRelativeToRect:[self _borderFrame] view:self offset:CPComboBoxFocusRingWidth - 1];
[self _selectMatchingItem];
}
/*! @ignore */
- (BOOL)listIsVisible
{
return _listDelegate ? [_listDelegate isVisible] : NO;
}
/*! @ignore */
- (void)reloadDataSourceForSelector:(SEL)cmd
{
if (_usesDataSource)
[self _dataSourceWarningForMethod:cmd condition:YES]
else
[self reloadData];
}
/*!
If the list is non-empty, sets the value of the field from the currently selected value of the list
and returns YES. If the list is empty or the list has no selected item, returns NO.
@ignore
*/
- (BOOL)takeStringValueFromList
{
if (_usesDataSource && _dataSource && [_dataSource numberOfItemsInComboBox:self] === 0)
return NO;
var selectedStringValue = [_listDelegate selectedStringValue];
if (selectedStringValue === nil)
return NO;
else
_selectedStringValue = selectedStringValue;
[self setStringValue:_selectedStringValue];
[self _reverseSetBinding];
return YES;
}
/*!
The receiver receives this notification when the list is closed.
@ignore
*/
- (void)listDidDismiss:(CPNotification)aNotification
{
[[self window] makeFirstResponder:self];
}
/*!
The receiver receives this notification when an item in the list is clicked.
@ignore
*/
- (void)itemWasClicked:(CPNotification)aNotification
{
[self takeStringValueFromList];
[self sendAction:[self action] to:[self target]];
}
#pragma mark Manipulating the Selection
- (void)deselectItemAtIndex:(int)index
{
var table = [_listDelegate tableView],
row = [table selectedRow];
if (row !== index)
return;
[table deselectRow:index];
}
- (int)indexOfSelectedItem
{
return [[_listDelegate tableView] selectedRow];
}
- (id)objectValueOfSelectedItem
{
var row = [[_listDelegate tableView] selectedRow];
if (row >= 0)
{
if (_usesDataSource)
[self _dataSourceWarningForMethod:_cmd condition:YES];
return _items[row];
}
return nil;
}
- (void)selectItemAtIndex:(int)index
{
var table = [_listDelegate tableView],
row = [table selectedRow];
if (row === index)
return;
[table selectRowIndexes:[CPIndexSet indexSetWithIndex:index] byExtendingSelection:NO];
}
- (void)selectItemWithObjectValue:(id)anObject
{
var index = [self indexOfItemWithObjectValue:anObject];
if (index !== CPNotFound)
[self selectItemAtIndex:index];
}
#pragma mark Completing the Text Field
- (BOOL)completes
{
return _completes;
}
- (void)setCompletes:(BOOL)flag
{
_completes = !!flag;
}
- (CPString)completedString:(CPString)substring
{
if (_usesDataSource)
return [self comboBoxCompletedString:substring];
else
{
var index = [_items indexOfObjectPassingTest:CPComboBoxCompletionTest context:substring];
return index !== CPNotFound ? _items[index] : nil;
}
}
/*!
Returns whether the combo box forces the user to enter or select
an item that is in the item list.
*/
- (BOOL)forceSelection
{
return _forceSelection;
}
/*!
Sets whether the combo box forces the user to enter or select
an item that is in the item list. If \c flag is \c YES and the user enters a value
that is not in the list, when the field loses focus it will revert
to the previous value. If \c flag is \c NO, the user can enter any value they wish.
Note that this flag is ignored if \ref setStringValue or \ref setObjectValue are
called directly.
*/
- (void)setForceSelection:(BOOL)flag
{
_forceSelection = !!flag;
}
#pragma mark CPTextField Delegate Methods and Overrides
/*! @ignore */
- (BOOL)sendAction:(SEL)anAction to:(id)anObject
{
// When the action is sent, be sure to get the value and close the list.
// This covers the case where the action is triggered by pressing a key
// that triggers the text field action.
if ([self listIsVisible])
{
[self takeStringValueFromList];
[_listDelegate close];
}
return [super sendAction:anAction to:anObject];
}
/*! @ignore */
- (void)setObjectValue:(id)object
{
[super setObjectValue:object];
_selectedStringValue = [self stringValue];
}
/*! @ignore */
- (void)interpretKeyEvents:(CPArray)events
{
var theEvent = events[0];
// Only if characters are added at the end of the value can completion occur
_canComplete = NO;
if (_completes)
{
if (![theEvent _couldBeKeyEquivalent] && [theEvent characters].charAt(0) !== CPDeleteCharacter)
{
var value = [self _inputElement].value,
selectedRange = [self selectedRange];
_canComplete = CPMaxRange(selectedRange) === value.length;
}
}
[super interpretKeyEvents:events];
}
/*! @ignore */
- (void)paste:(id)sender
{
if (_completes)
{
// Completion can occur only if pasting at the end of the value
var value = [self _inputElement].value,
selectedRange = [self selectedRange];
_canComplete = CPMaxRange(selectedRange) === value.length;
}
else
_canComplete = NO;
[super paste:sender];
}
/*! @ignore */
- (void)textDidChange:(CPNotification)aNotification
{
/*
Completion is attempted iff:
- _completes is YES
- Characters were added at the end of the value
*/
var uncompletedString = [self stringValue],
newString = uncompletedString;
if (_completes && _canComplete)
{
newString = [self completedString:uncompletedString];
if (newString && newString.length > uncompletedString.length)
{
[self setStringValue:newString];
[self setSelectedRange:CPMakeRange(uncompletedString.length, newString.length - uncompletedString.length)];
}
}
[self _selectMatchingItem];
_canComplete = NO;
[super textDidChange:aNotification];
}
/*!
Override of CPView -performKeyEquivalent
@ignore
*/
- (BOOL)performKeyEquivalent:(CPEvent)anEvent
{
if ([[self window] firstResponder] === self)
{
var key = [anEvent charactersIgnoringModifiers];
switch (key)
{
case CPDownArrowFunctionKey:
if (![self listIsVisible])
{
[self popUpList];
return YES;
}
break;
case CPEscapeFunctionKey:
if ([self listIsVisible])
{
// If we are forcing a selection and the user has entered a value which is not
// in the list, revert to the most recent valid value.
if (_forceSelection && ([self _inputElement].value !== _selectedStringValue))
[self setStringValue:_selectedStringValue];
}
break;
}
if ([_listDelegate performKeyEquivalent:anEvent])
return YES;
}
return [super performKeyEquivalent:anEvent];
}
/*! @ignore */
- (BOOL)resignFirstResponder
{
var buttonCausedResign = _popUpButtonCausedResign;
_popUpButtonCausedResign = NO;
/*
If the list or popup button is clicked, we lose focus. The list will refuse first responder,
and we refuse to resign. But we still have to manually restore the focus to the input element.
*/
var shouldResign = !buttonCausedResign && (!_listDelegate || [_listDelegate controllingViewShouldResign]);
if (!shouldResign)
{
#if PLATFORM(DOM)
// In FireFox this needs to be done in setTimeout, otherwise there is no caret
// We have to save the input element now, when we lose focus it will change.
var element = [self _inputElement];
window.setTimeout(function() { element.focus(); }, 0);
#endif
return NO;
}
// The list was not clicked, we need to close it now
[_listDelegate close];
// If the field is empty, allow it to remain empty.
// Otherwise restore the most recently selected value if forcing selection.
var value = [self stringValue];
if (value)
{
if (_forceSelection && ![value isEqual:_selectedStringValue])
[self setStringValue:_selectedStringValue];
}
else
_selectedStringValue = @"";
return [super resignFirstResponder];
}
- (void)setFont:(CPFont)aFont
{
[super setFont:aFont];
if (_listDelegate)
[_listDelegate setFont:aFont];
}
- (void)setAlignment:(CPTextAlignment)alignment
{
[super setAlignment:alignment];
if (_listDelegate)
[_listDelegate setAlignment:alignment];
}
#pragma mark Pop Up Button Layout
- (CGRect)popupButtonRectForBounds:(CGRect)bounds
{
var borderInset = [self currentValueForThemeAttribute:@"border-inset"],
buttonSize = [self currentValueForThemeAttribute:@"popup-button-size"];
bounds.origin.x = CGRectGetMaxX(bounds) - borderInset.right - buttonSize.width;
bounds.origin.y += borderInset.top;
bounds.size.width = buttonSize.width;
bounds.size.height = buttonSize.height;
return bounds;
}
- (CGRect)rectForEphemeralSubviewNamed:(CPString)aName
{
if (aName === "popup-button-view")
return [self popupButtonRectForBounds:[self bounds]];
return [super rectForEphemeralSubviewNamed:aName];
}
- (CPView)createEphemeralSubviewNamed:(CPString)aName
{
if (aName === "popup-button-view")
{
var view = [[_CPComboBoxPopUpButton alloc] initWithFrame:CGRectMakeZero() comboBox:self];
return view;
}
return [super createEphemeralSubviewNamed:aName];
}
- (void)layoutSubviews
{
[super layoutSubviews];
var popupButtonView = [self layoutEphemeralSubviewNamed:@"popup-button-view"
positioned:CPWindowAbove
relativeToEphemeralSubviewNamed:@"content-view"];
}
#pragma mark Internal Helpers
/*! @ignore */
- (void)_dataSourceWarningForMethod:(SEL)cmd condition:(CPString)flag
{
CPLog.warn("-[%s %s] should not be called when usesDataSource is set to %s", [self className], cmd, flag ? "YES" : "NO");
}
/*!
Select the item that matches the current value of the combobox.
@ignore
*/
- (void)_selectMatchingItem
{
var index = CPNotFound,
stringValue = [self stringValue];
if (_usesDataSource)
{
if (_dataSource && [_dataSource respondsToSelector:@selector(comboBox:indexOfItemWithStringValue:)])
index = [_dataSource comboBox:self indexOfItemWithStringValue:stringValue]
}
else
{
index = [self indexOfItemWithObjectValue:stringValue];
}
[_listDelegate selectRow:index];
// selectRow scrolls the row to visible, if a row is selected scroll it to the top
if (index !== CPNotFound)
{
[_listDelegate scrollItemAtIndexToTop:index];
_selectedStringValue = stringValue;
}
}
/*!
Calculate the frame in base coordinates that will nestle just below the visible border of the text field.
@ignore
*/
- (CGRect)_borderFrame
{
var inset = [self currentValueForThemeAttribute:@"border-inset"],
frame = [self bounds];
frame.origin.x += inset.left;
frame.origin.y += inset.top;
frame.size.width -= inset.left + inset.right;
frame.size.height -= inset.top + inset.bottom;
return frame;
}
/* @ignore */
- (void)_popUpButtonWasClicked
{
if (![self isEnabled])
return;
// If we are currently the first responder, we will be asked to resign when the list pops up.
// Set a flag to let resignResponder know that the button was clicked and we should not resign.
var firstResponder = [[self window] firstResponder];
_popUpButtonCausedResign = firstResponder === self;
if ([self listIsVisible])
[_listDelegate close];
else
{