1 package cview
2
3 import (
4 "bytes"
5 "fmt"
6 "strings"
7 "sync"
8
9 "github.com/gdamore/tcell/v3"
10 )
11
12
13 type ListItem struct {
14 disabled bool
15 mainText []byte
16 secondaryText []byte
17 shortcut rune
18 selected func()
19 reference interface{}
20
21 sync.RWMutex
22 }
23
24
25 func NewListItem(mainText string) *ListItem {
26 return &ListItem{
27 mainText: []byte(mainText),
28 }
29 }
30
31
32 func (l *ListItem) SetMainBytes(val []byte) {
33 l.Lock()
34 defer l.Unlock()
35
36 l.mainText = val
37 }
38
39
40 func (l *ListItem) SetMainText(val string) {
41 l.SetMainBytes([]byte(val))
42 }
43
44
45 func (l *ListItem) GetMainBytes() []byte {
46 l.RLock()
47 defer l.RUnlock()
48
49 return l.mainText
50 }
51
52
53 func (l *ListItem) GetMainText() string {
54 return string(l.GetMainBytes())
55 }
56
57
58 func (l *ListItem) SetSecondaryBytes(val []byte) {
59 l.Lock()
60 defer l.Unlock()
61
62 l.secondaryText = val
63 }
64
65
66 func (l *ListItem) SetSecondaryText(val string) {
67 l.SetSecondaryBytes([]byte(val))
68 }
69
70
71 func (l *ListItem) GetSecondaryBytes() []byte {
72 l.RLock()
73 defer l.RUnlock()
74
75 return l.secondaryText
76 }
77
78
79 func (l *ListItem) GetSecondaryText() string {
80 return string(l.GetSecondaryBytes())
81 }
82
83
84 func (l *ListItem) SetShortcut(val rune) {
85 l.Lock()
86 defer l.Unlock()
87
88 l.shortcut = val
89 }
90
91
92 func (l *ListItem) GetShortcut() rune {
93 l.RLock()
94 defer l.RUnlock()
95
96 return l.shortcut
97 }
98
99
100 func (l *ListItem) SetSelectedFunc(handler func()) {
101 l.Lock()
102 defer l.Unlock()
103
104 l.selected = handler
105 }
106
107
108 func (l *ListItem) SetReference(val interface{}) {
109 l.Lock()
110 defer l.Unlock()
111
112 l.reference = val
113 }
114
115
116 func (l *ListItem) GetReference() interface{} {
117 l.RLock()
118 defer l.RUnlock()
119
120 return l.reference
121 }
122
123
124 type List struct {
125 *Box
126 *ContextMenu
127
128
129 items []*ListItem
130
131
132 currentItem int
133
134
135 showSecondaryText bool
136
137
138 mainTextColor tcell.Color
139
140
141 secondaryTextColor tcell.Color
142
143
144 shortcutColor tcell.Color
145
146
147 selectedTextColor tcell.Color
148
149
150 selectedTextAttributes tcell.AttrMask
151
152
153 scrollBarVisibility ScrollBarVisibility
154
155
156 scrollBarColor tcell.Color
157
158
159 selectedBackgroundColor tcell.Color
160
161
162 selectedFocusOnly bool
163
164
165 selectedAlwaysVisible bool
166
167
168 selectedAlwaysCentered bool
169
170
171 highlightFullLine bool
172
173
174 wrapAround bool
175
176
177 hover bool
178
179
180
181 itemOffset, columnOffset int
182
183
184
185 changed func(index int, item *ListItem)
186
187
188
189 selected func(index int, item *ListItem)
190
191
192 done func()
193
194
195 height int
196
197
198 unselectedPrefix, unselectedSuffix []byte
199
200
201 selectedPrefix, selectedSuffix []byte
202
203
204 prefixWidth, suffixWidth int
205
206 sync.RWMutex
207 }
208
209
210 func NewList() *List {
211 l := &List{
212 Box: NewBox(),
213 showSecondaryText: true,
214 scrollBarVisibility: ScrollBarAuto,
215 mainTextColor: Styles.PrimaryTextColor,
216 secondaryTextColor: Styles.TertiaryTextColor,
217 shortcutColor: Styles.SecondaryTextColor,
218 selectedTextColor: Styles.PrimitiveBackgroundColor,
219 scrollBarColor: Styles.ScrollBarColor,
220 selectedBackgroundColor: Styles.PrimaryTextColor,
221 }
222
223 l.ContextMenu = NewContextMenu(l)
224 l.focus = l
225
226 return l
227 }
228
229
230
231
232
233
234
235 func (l *List) SetCurrentItem(index int) {
236 l.Lock()
237
238 if index < 0 {
239 index = len(l.items) + index
240 }
241 if index >= len(l.items) {
242 index = len(l.items) - 1
243 }
244 if index < 0 {
245 index = 0
246 }
247
248 previousItem := l.currentItem
249 l.currentItem = index
250
251 l.updateOffset()
252
253 if index != previousItem && index < len(l.items) && l.changed != nil {
254 item := l.items[index]
255 l.Unlock()
256 l.changed(index, item)
257 } else {
258 l.Unlock()
259 }
260 }
261
262
263
264 func (l *List) GetCurrentItem() *ListItem {
265 l.RLock()
266 defer l.RUnlock()
267
268 if len(l.items) == 0 || l.currentItem >= len(l.items) {
269 return nil
270 }
271 return l.items[l.currentItem]
272 }
273
274
275
276 func (l *List) GetCurrentItemIndex() int {
277 l.RLock()
278 defer l.RUnlock()
279 return l.currentItem
280 }
281
282
283 func (l *List) GetItems() []*ListItem {
284 l.RLock()
285 defer l.RUnlock()
286 return l.items
287 }
288
289
290
291
292
293
294
295
296
297 func (l *List) RemoveItem(index int) {
298 l.Lock()
299
300 if len(l.items) == 0 {
301 l.Unlock()
302 return
303 }
304
305
306 if index < 0 {
307 index = len(l.items) + index
308 }
309 if index >= len(l.items) {
310 index = len(l.items) - 1
311 }
312 if index < 0 {
313 index = 0
314 }
315
316
317 l.items = append(l.items[:index], l.items[index+1:]...)
318
319
320 if len(l.items) == 0 {
321 l.Unlock()
322 return
323 }
324
325
326 previousItem := l.currentItem
327 if l.currentItem >= index && l.currentItem > 0 {
328 l.currentItem--
329 }
330
331
332 if previousItem == index && index < len(l.items) && l.changed != nil {
333 item := l.items[l.currentItem]
334 l.Unlock()
335 l.changed(l.currentItem, item)
336 } else {
337 l.Unlock()
338 }
339 }
340
341
342
343 func (l *List) SetOffset(items, columns int) {
344 l.Lock()
345 defer l.Unlock()
346
347 if items < 0 {
348 items = 0
349 }
350 if columns < 0 {
351 columns = 0
352 }
353
354 l.itemOffset, l.columnOffset = items, columns
355 }
356
357
358
359 func (l *List) GetOffset() (int, int) {
360 l.Lock()
361 defer l.Unlock()
362
363 return l.itemOffset, l.columnOffset
364 }
365
366
367 func (l *List) SetMainTextColor(color tcell.Color) {
368 l.Lock()
369 defer l.Unlock()
370
371 l.mainTextColor = color
372 }
373
374
375 func (l *List) SetSecondaryTextColor(color tcell.Color) {
376 l.Lock()
377 defer l.Unlock()
378
379 l.secondaryTextColor = color
380 }
381
382
383 func (l *List) SetShortcutColor(color tcell.Color) {
384 l.Lock()
385 defer l.Unlock()
386
387 l.shortcutColor = color
388 }
389
390
391 func (l *List) SetSelectedTextColor(color tcell.Color) {
392 l.Lock()
393 defer l.Unlock()
394
395 l.selectedTextColor = color
396 }
397
398
399 func (l *List) SetSelectedTextAttributes(attr tcell.AttrMask) {
400 l.Lock()
401 defer l.Unlock()
402
403 l.selectedTextAttributes = attr
404 }
405
406
407 func (l *List) SetSelectedBackgroundColor(color tcell.Color) {
408 l.Lock()
409 defer l.Unlock()
410
411 l.selectedBackgroundColor = color
412 }
413
414
415
416
417 func (l *List) SetSelectedFocusOnly(focusOnly bool) {
418 l.Lock()
419 defer l.Unlock()
420
421 l.selectedFocusOnly = focusOnly
422 }
423
424
425
426 func (l *List) SetSelectedAlwaysVisible(alwaysVisible bool) {
427 l.Lock()
428 defer l.Unlock()
429
430 l.selectedAlwaysVisible = alwaysVisible
431 }
432
433
434
435 func (l *List) SetSelectedAlwaysCentered(alwaysCentered bool) {
436 l.Lock()
437 defer l.Unlock()
438
439 l.selectedAlwaysCentered = alwaysCentered
440 }
441
442
443
444
445
446 func (l *List) SetHighlightFullLine(highlight bool) {
447 l.Lock()
448 defer l.Unlock()
449
450 l.highlightFullLine = highlight
451 }
452
453
454 func (l *List) ShowSecondaryText(show bool) {
455 l.Lock()
456 defer l.Unlock()
457
458 l.showSecondaryText = show
459 }
460
461
462 func (l *List) SetScrollBarVisibility(visibility ScrollBarVisibility) {
463 l.Lock()
464 defer l.Unlock()
465
466 l.scrollBarVisibility = visibility
467 }
468
469
470 func (l *List) SetScrollBarColor(color tcell.Color) {
471 l.Lock()
472 defer l.Unlock()
473
474 l.scrollBarColor = color
475 }
476
477
478
479 func (l *List) SetHover(hover bool) {
480 l.Lock()
481 defer l.Unlock()
482
483 l.hover = hover
484 }
485
486
487
488
489
490
491 func (l *List) SetWrapAround(wrapAround bool) {
492 l.Lock()
493 defer l.Unlock()
494
495 l.wrapAround = wrapAround
496 }
497
498
499
500
501
502
503
504 func (l *List) SetChangedFunc(handler func(index int, item *ListItem)) {
505 l.Lock()
506 defer l.Unlock()
507
508 l.changed = handler
509 }
510
511
512
513
514 func (l *List) SetSelectedFunc(handler func(int, *ListItem)) {
515 l.Lock()
516 defer l.Unlock()
517
518 l.selected = handler
519 }
520
521
522
523 func (l *List) SetDoneFunc(handler func()) {
524 l.Lock()
525 defer l.Unlock()
526
527 l.done = handler
528 }
529
530
531 func (l *List) AddItem(item *ListItem) {
532 l.InsertItem(-1, item)
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 func (l *List) InsertItem(index int, item *ListItem) {
558 l.Lock()
559
560
561 if index < 0 {
562 index = len(l.items) + index + 1
563 }
564 if index < 0 {
565 index = 0
566 } else if index > len(l.items) {
567 index = len(l.items)
568 }
569
570
571 if l.currentItem < len(l.items) && l.currentItem >= index {
572 l.currentItem++
573 }
574
575
576 l.items = append(l.items, nil)
577 if index < len(l.items)-1 {
578 copy(l.items[index+1:], l.items[index:])
579 }
580 l.items[index] = item
581
582
583 if len(l.items) == 1 && l.changed != nil {
584 item := l.items[0]
585 l.Unlock()
586 l.changed(0, item)
587 } else {
588 l.Unlock()
589 }
590 }
591
592
593
594 func (l *List) GetItem(index int) *ListItem {
595 if index > len(l.items)-1 {
596 return nil
597 }
598 return l.items[index]
599 }
600
601
602 func (l *List) GetItemCount() int {
603 l.RLock()
604 defer l.RUnlock()
605
606 return len(l.items)
607 }
608
609
610
611 func (l *List) GetItemText(index int) (main, secondary string) {
612 l.RLock()
613 defer l.RUnlock()
614 return string(l.items[index].mainText), string(l.items[index].secondaryText)
615 }
616
617
618
619 func (l *List) SetItemText(index int, main, secondary string) {
620 l.Lock()
621 defer l.Unlock()
622
623 item := l.items[index]
624 item.mainText = []byte(main)
625 item.secondaryText = []byte(secondary)
626 }
627
628
629
630 func (l *List) SetItemEnabled(index int, enabled bool) {
631 l.Lock()
632 defer l.Unlock()
633
634 item := l.items[index]
635 item.disabled = !enabled
636 }
637
638
639 func (l *List) SetIndicators(selectedPrefix, selectedSuffix, unselectedPrefix, unselectedSuffix string) {
640 l.Lock()
641 defer l.Unlock()
642 l.selectedPrefix = []byte(selectedPrefix)
643 l.selectedSuffix = []byte(selectedSuffix)
644 l.unselectedPrefix = []byte(unselectedPrefix)
645 l.unselectedSuffix = []byte(unselectedSuffix)
646 l.prefixWidth = len(selectedPrefix)
647 if len(unselectedPrefix) > l.prefixWidth {
648 l.prefixWidth = len(unselectedPrefix)
649 }
650 l.suffixWidth = len(selectedSuffix)
651 if len(unselectedSuffix) > l.suffixWidth {
652 l.suffixWidth = len(unselectedSuffix)
653 }
654 }
655
656
657
658
659
660
661
662
663
664
665
666 func (l *List) FindItems(mainSearch, secondarySearch string, mustContainBoth, ignoreCase bool) (indices []int) {
667 l.RLock()
668 defer l.RUnlock()
669
670 if mainSearch == "" && secondarySearch == "" {
671 return
672 }
673
674 if ignoreCase {
675 mainSearch = strings.ToLower(mainSearch)
676 secondarySearch = strings.ToLower(secondarySearch)
677 }
678
679 mainSearchBytes := []byte(mainSearch)
680 secondarySearchBytes := []byte(secondarySearch)
681
682 for index, item := range l.items {
683 mainText := item.mainText
684 secondaryText := item.secondaryText
685 if ignoreCase {
686 mainText = bytes.ToLower(mainText)
687 secondaryText = bytes.ToLower(secondaryText)
688 }
689
690
691 mainContained := bytes.Contains(mainText, mainSearchBytes)
692 secondaryContained := bytes.Contains(secondaryText, secondarySearchBytes)
693 if mustContainBoth && mainContained && secondaryContained ||
694 !mustContainBoth && (len(mainText) > 0 && mainContained || len(secondaryText) > 0 && secondaryContained) {
695 indices = append(indices, index)
696 }
697 }
698
699 return
700 }
701
702
703 func (l *List) Clear() {
704 l.Lock()
705 defer l.Unlock()
706
707 l.items = nil
708 l.currentItem = 0
709 l.itemOffset = 0
710 l.columnOffset = 0
711 }
712
713
714 func (l *List) Focus(delegate func(p Primitive)) {
715 l.Box.Focus(delegate)
716 if l.ContextMenu.open {
717 delegate(l.ContextMenu.list)
718 }
719 }
720
721
722 func (l *List) HasFocus() bool {
723 if l.ContextMenu.open {
724 return l.ContextMenu.list.HasFocus()
725 }
726
727 l.RLock()
728 defer l.RUnlock()
729 return l.hasFocus
730 }
731
732
733 func (l *List) Transform(tr Transformation) {
734 l.Lock()
735
736 previousItem := l.currentItem
737
738 l.transform(tr)
739
740 if l.currentItem != previousItem && l.currentItem < len(l.items) && l.changed != nil {
741 item := l.items[l.currentItem]
742 l.Unlock()
743 l.changed(l.currentItem, item)
744 } else {
745 l.Unlock()
746 }
747 }
748
749 func (l *List) transform(tr Transformation) {
750 var decreasing bool
751
752 pageItems := l.height
753 if l.showSecondaryText {
754 pageItems /= 2
755 }
756 if pageItems < 1 {
757 pageItems = 1
758 }
759
760 switch tr {
761 case TransformFirstItem:
762 l.currentItem = 0
763 l.itemOffset = 0
764 decreasing = true
765 case TransformLastItem:
766 l.currentItem = len(l.items) - 1
767 case TransformPreviousItem:
768 l.currentItem--
769 decreasing = true
770 case TransformNextItem:
771 l.currentItem++
772 case TransformPreviousPage:
773 l.currentItem -= pageItems
774 decreasing = true
775 case TransformNextPage:
776 l.currentItem += pageItems
777 l.itemOffset += pageItems
778 }
779
780 for i := 0; i < len(l.items); i++ {
781 if l.currentItem < 0 {
782 if l.wrapAround {
783 l.currentItem = len(l.items) - 1
784 } else {
785 l.currentItem = 0
786 l.itemOffset = 0
787 }
788 } else if l.currentItem >= len(l.items) {
789 if l.wrapAround {
790 l.currentItem = 0
791 l.itemOffset = 0
792 } else {
793 l.currentItem = len(l.items) - 1
794 }
795 }
796
797 item := l.items[l.currentItem]
798 if !item.disabled && (item.shortcut > 0 || len(item.mainText) > 0 || len(item.secondaryText) > 0) {
799 break
800 }
801
802 if decreasing {
803 l.currentItem--
804 } else {
805 l.currentItem++
806 }
807 }
808
809 l.updateOffset()
810 }
811
812 func (l *List) updateOffset() {
813 _, _, _, l.height = l.GetInnerRect()
814
815 h := l.height
816 if l.selectedAlwaysCentered {
817 h /= 2
818 }
819
820 if l.currentItem < l.itemOffset {
821 l.itemOffset = l.currentItem
822 } else if l.showSecondaryText {
823 if 2*(l.currentItem-l.itemOffset) >= h-1 {
824 l.itemOffset = (2*l.currentItem + 3 - h) / 2
825 }
826 } else {
827 if l.currentItem-l.itemOffset >= h {
828 l.itemOffset = l.currentItem + 1 - h
829 }
830 }
831
832 if l.showSecondaryText {
833 if l.itemOffset > len(l.items)-(l.height/2) {
834 l.itemOffset = len(l.items) - l.height/2
835 }
836 } else {
837 if l.itemOffset > len(l.items)-l.height {
838 l.itemOffset = len(l.items) - l.height
839 }
840 }
841
842 if l.itemOffset < 0 {
843 l.itemOffset = 0
844 }
845
846
847 maxWidth := 0
848 for _, option := range l.items {
849 strWidth := TaggedTextWidth(option.mainText)
850 secondaryWidth := TaggedTextWidth(option.secondaryText)
851 if secondaryWidth > strWidth {
852 strWidth = secondaryWidth
853 }
854 if option.shortcut != 0 {
855 strWidth += 4
856 }
857
858 if strWidth > maxWidth {
859 maxWidth = strWidth
860 }
861 }
862
863
864 addWidth := 0
865 if l.scrollBarVisibility == ScrollBarAlways ||
866 (l.scrollBarVisibility == ScrollBarAuto &&
867 ((!l.showSecondaryText && len(l.items) > l.innerHeight) ||
868 (l.showSecondaryText && len(l.items) > l.innerHeight/2))) {
869 addWidth = 1
870 }
871
872 if l.columnOffset > (maxWidth-l.innerWidth)+addWidth {
873 l.columnOffset = (maxWidth - l.innerWidth) + addWidth
874 }
875 if l.columnOffset < 0 {
876 l.columnOffset = 0
877 }
878 }
879
880
881 func (l *List) Draw(screen tcell.Screen) {
882 if !l.GetVisible() {
883 return
884 }
885
886 l.Box.Draw(screen)
887 hasFocus := l.GetFocusable().HasFocus()
888
889 l.Lock()
890 defer l.Unlock()
891
892
893 x, y, width, height := l.GetInnerRect()
894 leftEdge := x
895 fullWidth := width + l.paddingLeft + l.paddingRight + l.prefixWidth + l.suffixWidth
896 bottomLimit := y + height
897
898 l.height = height
899
900 screenWidth, _ := screen.Size()
901 scrollBarHeight := height
902 scrollBarX := x + (width - 1) + l.paddingLeft + l.paddingRight
903 if scrollBarX > screenWidth-1 {
904 scrollBarX = screenWidth - 1
905 }
906
907
908 if l.showSecondaryText {
909 scrollBarHeight /= 2
910 }
911
912
913 var showShortcuts bool
914 for _, item := range l.items {
915 if item.shortcut != 0 {
916 showShortcuts = true
917 x += 4
918 width -= 4
919 break
920 }
921 }
922
923
924 if l.selectedAlwaysVisible || l.selectedAlwaysCentered {
925 l.updateOffset()
926 }
927
928 scrollBarCursor := int(float64(len(l.items)) * (float64(l.itemOffset) / float64(len(l.items)-height)))
929
930
931 for index, item := range l.items {
932 if index < l.itemOffset {
933 continue
934 }
935
936 if y >= bottomLimit {
937 break
938 }
939
940 mainText := item.mainText
941 secondaryText := item.secondaryText
942 if l.columnOffset > 0 {
943 if l.columnOffset < len(mainText) {
944 mainText = mainText[l.columnOffset:]
945 } else {
946 mainText = nil
947 }
948 if l.columnOffset < len(secondaryText) {
949 secondaryText = secondaryText[l.columnOffset:]
950 } else {
951 secondaryText = nil
952 }
953 }
954
955 if len(item.mainText) == 0 && len(item.secondaryText) == 0 && item.shortcut == 0 {
956 Print(screen, []byte(string(tcell.RuneLTee)), leftEdge-2, y, 1, AlignLeft, l.mainTextColor)
957 Print(screen, bytes.Repeat([]byte(string(tcell.RuneHLine)), fullWidth), leftEdge-1, y, fullWidth, AlignLeft, l.mainTextColor)
958 Print(screen, []byte(string(tcell.RuneRTee)), leftEdge+fullWidth-1, y, 1, AlignLeft, l.mainTextColor)
959
960 RenderScrollBar(screen, l.scrollBarVisibility, scrollBarX, y, scrollBarHeight, len(l.items), scrollBarCursor, index-l.itemOffset, l.hasFocus, l.scrollBarColor)
961 y++
962 continue
963 }
964
965 if index == l.currentItem {
966 if len(l.selectedPrefix) > 0 {
967 mainText = append(l.selectedPrefix, mainText...)
968 }
969 if len(l.selectedSuffix) > 0 {
970 mainText = append(mainText, l.selectedSuffix...)
971 }
972
973 } else {
974 if len(l.unselectedPrefix) > 0 {
975 mainText = append(l.unselectedPrefix, mainText...)
976 }
977 if len(l.unselectedSuffix) > 0 {
978 mainText = append(mainText, l.unselectedSuffix...)
979 }
980 }
981 if item.disabled {
982
983 if showShortcuts && item.shortcut != 0 {
984 Print(screen, []byte(fmt.Sprintf("(%c)", item.shortcut)), x-5, y, 4, AlignRight, tcell.ColorDarkSlateGray.TrueColor())
985 }
986
987
988 Print(screen, mainText, x, y, width, AlignLeft, tcell.ColorGray.TrueColor())
989
990 RenderScrollBar(screen, l.scrollBarVisibility, scrollBarX, y, scrollBarHeight, len(l.items), scrollBarCursor, index-l.itemOffset, l.hasFocus, l.scrollBarColor)
991 y++
992 continue
993 }
994
995
996 if showShortcuts && item.shortcut != 0 {
997 Print(screen, []byte(fmt.Sprintf("(%c)", item.shortcut)), x-5, y, 4, AlignRight, l.shortcutColor)
998 }
999
1000
1001 Print(screen, mainText, x, y, width, AlignLeft, l.mainTextColor)
1002
1003
1004 if index == l.currentItem && (!l.selectedFocusOnly || hasFocus) {
1005 textWidth := width
1006 if !l.highlightFullLine {
1007 if w := TaggedTextWidth(mainText); w < textWidth {
1008 textWidth = w
1009 }
1010 }
1011
1012 for bx := 0; bx < textWidth; bx++ {
1013 m, style, _ := screen.Get(x+bx, y)
1014 fg := style.GetForeground()
1015 if fg == l.mainTextColor {
1016 fg = l.selectedTextColor
1017 }
1018 style = SetAttributes(style.Background(l.selectedBackgroundColor).Foreground(fg), l.selectedTextAttributes)
1019 screen.Put(x+bx, y, m, style)
1020 }
1021 }
1022
1023 RenderScrollBar(screen, l.scrollBarVisibility, scrollBarX, y, scrollBarHeight, len(l.items), scrollBarCursor, index-l.itemOffset, l.hasFocus, l.scrollBarColor)
1024
1025 y++
1026
1027 if y >= bottomLimit {
1028 break
1029 }
1030
1031
1032 if l.showSecondaryText {
1033 Print(screen, secondaryText, x, y, width, AlignLeft, l.secondaryTextColor)
1034
1035 RenderScrollBar(screen, l.scrollBarVisibility, scrollBarX, y, scrollBarHeight, len(l.items), scrollBarCursor, index-l.itemOffset, l.hasFocus, l.scrollBarColor)
1036
1037 y++
1038 }
1039 }
1040
1041
1042 for y < bottomLimit {
1043 RenderScrollBar(screen, l.scrollBarVisibility, scrollBarX, y, scrollBarHeight, len(l.items), scrollBarCursor, bottomLimit-y, l.hasFocus, l.scrollBarColor)
1044
1045 y++
1046 }
1047
1048
1049 if hasFocus && l.ContextMenu.open {
1050 ctx := l.ContextMenuList()
1051
1052
1053 maxWidth := 0
1054 for _, option := range ctx.items {
1055 strWidth := TaggedTextWidth(option.mainText)
1056 if option.shortcut != 0 {
1057 strWidth += 4
1058 }
1059 if strWidth > maxWidth {
1060 maxWidth = strWidth
1061 }
1062 }
1063
1064 lheight := len(ctx.items)
1065 lwidth := maxWidth
1066
1067
1068 lwidth += 2
1069 lheight += 2
1070
1071 lwidth += ctx.paddingLeft + ctx.paddingRight
1072 lheight += ctx.paddingTop + ctx.paddingBottom
1073
1074 cx, cy := l.ContextMenu.x, l.ContextMenu.y
1075 if cx < 0 || cy < 0 {
1076 offsetX := 7
1077 if showShortcuts {
1078 offsetX += 4
1079 }
1080 offsetY := l.currentItem
1081 if l.showSecondaryText {
1082 offsetY *= 2
1083 }
1084 x, y, _, _ := l.GetInnerRect()
1085 cx, cy = x+offsetX, y+offsetY
1086 }
1087
1088 _, sheight := screen.Size()
1089 if cy+lheight >= sheight && cy-2 > lheight-cy {
1090 for i := (cy + lheight) - sheight; i > 0; i-- {
1091 cy--
1092 if cy+lheight < sheight {
1093 break
1094 }
1095 }
1096 if cy < 0 {
1097 cy = 0
1098 }
1099 }
1100 if cy+lheight >= sheight {
1101 lheight = sheight - cy
1102 }
1103
1104 if ctx.scrollBarVisibility == ScrollBarAlways || (ctx.scrollBarVisibility == ScrollBarAuto && len(ctx.items) > lheight) {
1105 lwidth++
1106 }
1107
1108 ctx.SetRect(cx, cy, lwidth, lheight)
1109 ctx.Draw(screen)
1110 }
1111 }
1112
1113
1114 func (l *List) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
1115 return l.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
1116 l.Lock()
1117
1118 if HitShortcut(event, Keys.Cancel) {
1119 if l.ContextMenu.open {
1120 l.Unlock()
1121
1122 l.ContextMenu.hide(setFocus)
1123 return
1124 }
1125
1126 if l.done != nil {
1127 l.Unlock()
1128 l.done()
1129 } else {
1130 l.Unlock()
1131 }
1132 return
1133 } else if HitShortcut(event, Keys.Select, Keys.Select2) {
1134 if l.currentItem >= 0 && l.currentItem < len(l.items) {
1135 item := l.items[l.currentItem]
1136 if !item.disabled {
1137 if item.selected != nil {
1138 l.Unlock()
1139 item.selected()
1140 l.Lock()
1141 }
1142 if l.selected != nil {
1143 l.Unlock()
1144 l.selected(l.currentItem, item)
1145 l.Lock()
1146 }
1147 }
1148 }
1149 } else if HitShortcut(event, Keys.ShowContextMenu) {
1150 defer l.ContextMenu.show(l.currentItem, -1, -1, setFocus)
1151 } else if len(l.items) == 0 {
1152 l.Unlock()
1153 return
1154 }
1155
1156 if event.Key() == tcell.KeyRune {
1157 str := event.Str()
1158 if str != " " {
1159
1160 for index, item := range l.items {
1161 if !item.disabled && str == string(item.shortcut) {
1162
1163 l.currentItem = index
1164
1165 item := l.items[l.currentItem]
1166 if item.selected != nil {
1167 l.Unlock()
1168 item.selected()
1169 l.Lock()
1170 }
1171 if l.selected != nil {
1172 l.Unlock()
1173 l.selected(l.currentItem, item)
1174 l.Lock()
1175 }
1176
1177 l.Unlock()
1178 return
1179 }
1180 }
1181 }
1182 }
1183
1184 previousItem := l.currentItem
1185
1186 if HitShortcut(event, Keys.MoveFirst, Keys.MoveFirst2) {
1187 l.transform(TransformFirstItem)
1188 } else if HitShortcut(event, Keys.MoveLast, Keys.MoveLast2) {
1189 l.transform(TransformLastItem)
1190 } else if HitShortcut(event, Keys.MoveUp, Keys.MoveUp2) {
1191 l.transform(TransformPreviousItem)
1192 } else if HitShortcut(event, Keys.MoveDown, Keys.MoveDown2) {
1193 l.transform(TransformNextItem)
1194 } else if HitShortcut(event, Keys.MoveLeft, Keys.MoveLeft2) {
1195 l.columnOffset--
1196 l.updateOffset()
1197 } else if HitShortcut(event, Keys.MoveRight, Keys.MoveRight2) {
1198 l.columnOffset++
1199 l.updateOffset()
1200 } else if HitShortcut(event, Keys.MovePreviousPage) {
1201 l.transform(TransformPreviousPage)
1202 } else if HitShortcut(event, Keys.MoveNextPage) {
1203 l.transform(TransformNextPage)
1204 }
1205
1206 if l.currentItem != previousItem && l.currentItem < len(l.items) && l.changed != nil {
1207 item := l.items[l.currentItem]
1208 l.Unlock()
1209 l.changed(l.currentItem, item)
1210 } else {
1211 l.Unlock()
1212 }
1213 })
1214 }
1215
1216
1217
1218 func (l *List) indexAtY(y int) int {
1219 _, rectY, _, height := l.GetInnerRect()
1220 if y < rectY || y >= rectY+height {
1221 return -1
1222 }
1223
1224 index := y - rectY
1225 if l.showSecondaryText {
1226 index /= 2
1227 }
1228 index += l.itemOffset
1229
1230 if index >= len(l.items) {
1231 return -1
1232 }
1233 return index
1234 }
1235
1236
1237
1238 func (l *List) indexAtPoint(x, y int) int {
1239 rectX, rectY, width, height := l.GetInnerRect()
1240 if x < rectX || x >= rectX+width || y < rectY || y >= rectY+height {
1241 return -1
1242 }
1243
1244 index := y - rectY
1245 if l.showSecondaryText {
1246 index /= 2
1247 }
1248 index += l.itemOffset
1249
1250 if index >= len(l.items) {
1251 return -1
1252 }
1253 return index
1254 }
1255
1256
1257 func (l *List) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
1258 return l.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
1259 l.Lock()
1260
1261
1262 if l.ContextMenuVisible() && l.ContextMenuList().InRect(event.Position()) {
1263 defer l.ContextMenuList().MouseHandler()(action, event, setFocus)
1264 consumed = true
1265 l.Unlock()
1266 return
1267 }
1268
1269 if !l.InRect(event.Position()) {
1270 l.Unlock()
1271 return false, nil
1272 }
1273
1274
1275 switch action {
1276 case MouseLeftClick:
1277 if l.ContextMenuVisible() {
1278 defer l.ContextMenu.hide(setFocus)
1279 consumed = true
1280 l.Unlock()
1281 return
1282 }
1283
1284 l.Unlock()
1285 setFocus(l)
1286 l.Lock()
1287
1288 index := l.indexAtPoint(event.Position())
1289 if index != -1 {
1290 item := l.items[index]
1291 if !item.disabled {
1292 l.currentItem = index
1293 if item.selected != nil {
1294 l.Unlock()
1295 item.selected()
1296 l.Lock()
1297 }
1298 if l.selected != nil {
1299 l.Unlock()
1300 l.selected(index, item)
1301 l.Lock()
1302 }
1303 if index != l.currentItem && l.changed != nil {
1304 l.Unlock()
1305 l.changed(index, item)
1306 l.Lock()
1307 }
1308 }
1309 }
1310 consumed = true
1311 case MouseMiddleClick:
1312 if l.ContextMenu.open {
1313 defer l.ContextMenu.hide(setFocus)
1314 consumed = true
1315 l.Unlock()
1316 return
1317 }
1318 case MouseRightDown:
1319 if len(l.ContextMenuList().items) == 0 {
1320 l.Unlock()
1321 return
1322 }
1323
1324 x, y := event.Position()
1325
1326 index := l.indexAtPoint(event.Position())
1327 if index != -1 {
1328 item := l.items[index]
1329 if !item.disabled {
1330 l.currentItem = index
1331 if index != l.currentItem && l.changed != nil {
1332 l.Unlock()
1333 l.changed(index, item)
1334 l.Lock()
1335 }
1336 }
1337 }
1338
1339 defer l.ContextMenu.show(l.currentItem, x, y, setFocus)
1340 l.ContextMenu.drag = true
1341 consumed = true
1342 case MouseMove:
1343 if l.hover {
1344 _, y := event.Position()
1345 index := l.indexAtY(y)
1346 if index >= 0 {
1347 item := l.items[index]
1348 if !item.disabled {
1349 l.currentItem = index
1350 }
1351 }
1352
1353 consumed = true
1354 }
1355 case MouseScrollUp:
1356 if l.itemOffset > 0 {
1357 l.itemOffset--
1358 }
1359 consumed = true
1360 case MouseScrollDown:
1361 lines := len(l.items) - l.itemOffset
1362 if l.showSecondaryText {
1363 lines *= 2
1364 }
1365 if _, _, _, height := l.GetInnerRect(); lines > height {
1366 l.itemOffset++
1367 }
1368 consumed = true
1369 }
1370
1371 l.Unlock()
1372 return
1373 })
1374 }
1375
View as plain text