...

Source file src/code.rocketnine.space/tslocum/cview/list.go

Documentation: code.rocketnine.space/tslocum/cview

     1  package cview
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"strings"
     7  	"sync"
     8  
     9  	"github.com/gdamore/tcell/v2"
    10  )
    11  
    12  // ListItem represents an item in a List.
    13  type ListItem struct {
    14  	disabled      bool        // Whether or not the list item is selectable.
    15  	mainText      []byte      // The main text of the list item.
    16  	secondaryText []byte      // A secondary text to be shown underneath the main text.
    17  	shortcut      rune        // The key to select the list item directly, 0 if there is no shortcut.
    18  	selected      func()      // The optional function which is called when the item is selected.
    19  	reference     interface{} // An optional reference object.
    20  
    21  	sync.RWMutex
    22  }
    23  
    24  // NewListItem returns a new item for a list.
    25  func NewListItem(mainText string) *ListItem {
    26  	return &ListItem{
    27  		mainText: []byte(mainText),
    28  	}
    29  }
    30  
    31  // SetMainBytes sets the main text of the list item.
    32  func (l *ListItem) SetMainBytes(val []byte) {
    33  	l.Lock()
    34  	defer l.Unlock()
    35  
    36  	l.mainText = val
    37  }
    38  
    39  // SetMainText sets the main text of the list item.
    40  func (l *ListItem) SetMainText(val string) {
    41  	l.SetMainBytes([]byte(val))
    42  }
    43  
    44  // GetMainBytes returns the item's main text.
    45  func (l *ListItem) GetMainBytes() []byte {
    46  	l.RLock()
    47  	defer l.RUnlock()
    48  
    49  	return l.mainText
    50  }
    51  
    52  // GetMainText returns the item's main text.
    53  func (l *ListItem) GetMainText() string {
    54  	return string(l.GetMainBytes())
    55  }
    56  
    57  // SetSecondaryBytes sets a secondary text to be shown underneath the main text.
    58  func (l *ListItem) SetSecondaryBytes(val []byte) {
    59  	l.Lock()
    60  	defer l.Unlock()
    61  
    62  	l.secondaryText = val
    63  }
    64  
    65  // SetSecondaryText sets a secondary text to be shown underneath the main text.
    66  func (l *ListItem) SetSecondaryText(val string) {
    67  	l.SetSecondaryBytes([]byte(val))
    68  }
    69  
    70  // GetSecondaryBytes returns the item's secondary text.
    71  func (l *ListItem) GetSecondaryBytes() []byte {
    72  	l.RLock()
    73  	defer l.RUnlock()
    74  
    75  	return l.secondaryText
    76  }
    77  
    78  // GetSecondaryText returns the item's secondary text.
    79  func (l *ListItem) GetSecondaryText() string {
    80  	return string(l.GetSecondaryBytes())
    81  }
    82  
    83  // SetShortcut sets the key to select the ListItem directly, 0 if there is no shortcut.
    84  func (l *ListItem) SetShortcut(val rune) {
    85  	l.Lock()
    86  	defer l.Unlock()
    87  
    88  	l.shortcut = val
    89  }
    90  
    91  // GetShortcut returns the ListItem's shortcut.
    92  func (l *ListItem) GetShortcut() rune {
    93  	l.RLock()
    94  	defer l.RUnlock()
    95  
    96  	return l.shortcut
    97  }
    98  
    99  // SetSelectedFunc sets a function which is called when the ListItem is selected.
   100  func (l *ListItem) SetSelectedFunc(handler func()) {
   101  	l.Lock()
   102  	defer l.Unlock()
   103  
   104  	l.selected = handler
   105  }
   106  
   107  // SetReference allows you to store a reference of any type in the item
   108  func (l *ListItem) SetReference(val interface{}) {
   109  	l.Lock()
   110  	defer l.Unlock()
   111  
   112  	l.reference = val
   113  }
   114  
   115  // GetReference returns the item's reference object.
   116  func (l *ListItem) GetReference() interface{} {
   117  	l.RLock()
   118  	defer l.RUnlock()
   119  
   120  	return l.reference
   121  }
   122  
   123  // List displays rows of items, each of which can be selected.
   124  type List struct {
   125  	*Box
   126  	*ContextMenu
   127  
   128  	// The items of the list.
   129  	items []*ListItem
   130  
   131  	// The index of the currently selected item.
   132  	currentItem int
   133  
   134  	// Whether or not to show the secondary item texts.
   135  	showSecondaryText bool
   136  
   137  	// The item main text color.
   138  	mainTextColor tcell.Color
   139  
   140  	// The item secondary text color.
   141  	secondaryTextColor tcell.Color
   142  
   143  	// The item shortcut text color.
   144  	shortcutColor tcell.Color
   145  
   146  	// The text color for selected items.
   147  	selectedTextColor tcell.Color
   148  
   149  	// The style attributes for selected items.
   150  	selectedTextAttributes tcell.AttrMask
   151  
   152  	// Visibility of the scroll bar.
   153  	scrollBarVisibility ScrollBarVisibility
   154  
   155  	// The scroll bar color.
   156  	scrollBarColor tcell.Color
   157  
   158  	// The background color for selected items.
   159  	selectedBackgroundColor tcell.Color
   160  
   161  	// If true, the selection is only shown when the list has focus.
   162  	selectedFocusOnly bool
   163  
   164  	// If true, the selection must remain visible when scrolling.
   165  	selectedAlwaysVisible bool
   166  
   167  	// If true, the selection must remain centered when scrolling.
   168  	selectedAlwaysCentered bool
   169  
   170  	// If true, the entire row is highlighted when selected.
   171  	highlightFullLine bool
   172  
   173  	// Whether or not navigating the list will wrap around.
   174  	wrapAround bool
   175  
   176  	// Whether or not hovering over an item will highlight it.
   177  	hover bool
   178  
   179  	// The number of list items and columns by which the list is scrolled
   180  	// down/to the right.
   181  	itemOffset, columnOffset int
   182  
   183  	// An optional function which is called when the user has navigated to a list
   184  	// item.
   185  	changed func(index int, item *ListItem)
   186  
   187  	// An optional function which is called when a list item was selected. This
   188  	// function will be called even if the list item defines its own callback.
   189  	selected func(index int, item *ListItem)
   190  
   191  	// An optional function which is called when the user presses the Escape key.
   192  	done func()
   193  
   194  	// The height of the list the last time it was drawn.
   195  	height int
   196  
   197  	// Prefix and suffix strings drawn for unselected elements.
   198  	unselectedPrefix, unselectedSuffix []byte
   199  
   200  	// Prefix and suffix strings drawn for selected elements.
   201  	selectedPrefix, selectedSuffix []byte
   202  
   203  	// Maximum prefix and suffix width.
   204  	prefixWidth, suffixWidth int
   205  
   206  	sync.RWMutex
   207  }
   208  
   209  // NewList returns a new form.
   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  // SetCurrentItem sets the currently selected item by its index, starting at 0
   230  // for the first item. If a negative index is provided, items are referred to
   231  // from the back (-1 = last item, -2 = second-to-last item, and so on). Out of
   232  // range indices are clamped to the beginning/end.
   233  //
   234  // Calling this function triggers a "changed" event if the selection changes.
   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  // GetCurrentItem returns the currently selected list item,
   263  // Returns nil if no item is selected.
   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  // GetCurrentItemIndex returns the index of the currently selected list item,
   275  // starting at 0 for the first item and its struct.
   276  func (l *List) GetCurrentItemIndex() int {
   277  	l.RLock()
   278  	defer l.RUnlock()
   279  	return l.currentItem
   280  }
   281  
   282  // GetItems returns all list items.
   283  func (l *List) GetItems() []*ListItem {
   284  	l.RLock()
   285  	defer l.RUnlock()
   286  	return l.items
   287  }
   288  
   289  // RemoveItem removes the item with the given index (starting at 0) from the
   290  // list. If a negative index is provided, items are referred to from the back
   291  // (-1 = last item, -2 = second-to-last item, and so on). Out of range indices
   292  // are clamped to the beginning/end, i.e. unless the list is empty, an item is
   293  // always removed.
   294  //
   295  // The currently selected item is shifted accordingly. If it is the one that is
   296  // removed, a "changed" event is fired.
   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  	// Adjust index.
   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  	// Remove item.
   317  	l.items = append(l.items[:index], l.items[index+1:]...)
   318  
   319  	// If there is nothing left, we're done.
   320  	if len(l.items) == 0 {
   321  		l.Unlock()
   322  		return
   323  	}
   324  
   325  	// Shift current item.
   326  	previousItem := l.currentItem
   327  	if l.currentItem >= index && l.currentItem > 0 {
   328  		l.currentItem--
   329  	}
   330  
   331  	// Fire "changed" event for removed items.
   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  // SetOffset sets the number of list items and columns by which the list is
   342  // scrolled down/to the right.
   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  // GetOffset returns the number of list items and columns by which the list is
   358  // scrolled down/to the right.
   359  func (l *List) GetOffset() (int, int) {
   360  	l.Lock()
   361  	defer l.Unlock()
   362  
   363  	return l.itemOffset, l.columnOffset
   364  }
   365  
   366  // SetMainTextColor sets the color of the items' main text.
   367  func (l *List) SetMainTextColor(color tcell.Color) {
   368  	l.Lock()
   369  	defer l.Unlock()
   370  
   371  	l.mainTextColor = color
   372  }
   373  
   374  // SetSecondaryTextColor sets the color of the items' secondary text.
   375  func (l *List) SetSecondaryTextColor(color tcell.Color) {
   376  	l.Lock()
   377  	defer l.Unlock()
   378  
   379  	l.secondaryTextColor = color
   380  }
   381  
   382  // SetShortcutColor sets the color of the items' shortcut.
   383  func (l *List) SetShortcutColor(color tcell.Color) {
   384  	l.Lock()
   385  	defer l.Unlock()
   386  
   387  	l.shortcutColor = color
   388  }
   389  
   390  // SetSelectedTextColor sets the text color of selected items.
   391  func (l *List) SetSelectedTextColor(color tcell.Color) {
   392  	l.Lock()
   393  	defer l.Unlock()
   394  
   395  	l.selectedTextColor = color
   396  }
   397  
   398  // SetSelectedTextAttributes sets the style attributes of selected items.
   399  func (l *List) SetSelectedTextAttributes(attr tcell.AttrMask) {
   400  	l.Lock()
   401  	defer l.Unlock()
   402  
   403  	l.selectedTextAttributes = attr
   404  }
   405  
   406  // SetSelectedBackgroundColor sets the background color of selected items.
   407  func (l *List) SetSelectedBackgroundColor(color tcell.Color) {
   408  	l.Lock()
   409  	defer l.Unlock()
   410  
   411  	l.selectedBackgroundColor = color
   412  }
   413  
   414  // SetSelectedFocusOnly sets a flag which determines when the currently selected
   415  // list item is highlighted. If set to true, selected items are only highlighted
   416  // when the list has focus. If set to false, they are always highlighted.
   417  func (l *List) SetSelectedFocusOnly(focusOnly bool) {
   418  	l.Lock()
   419  	defer l.Unlock()
   420  
   421  	l.selectedFocusOnly = focusOnly
   422  }
   423  
   424  // SetSelectedAlwaysVisible sets a flag which determines whether the currently
   425  // selected list item must remain visible when scrolling.
   426  func (l *List) SetSelectedAlwaysVisible(alwaysVisible bool) {
   427  	l.Lock()
   428  	defer l.Unlock()
   429  
   430  	l.selectedAlwaysVisible = alwaysVisible
   431  }
   432  
   433  // SetSelectedAlwaysCentered sets a flag which determines whether the currently
   434  // selected list item must remain centered when scrolling.
   435  func (l *List) SetSelectedAlwaysCentered(alwaysCentered bool) {
   436  	l.Lock()
   437  	defer l.Unlock()
   438  
   439  	l.selectedAlwaysCentered = alwaysCentered
   440  }
   441  
   442  // SetHighlightFullLine sets a flag which determines whether the colored
   443  // background of selected items spans the entire width of the view. If set to
   444  // true, the highlight spans the entire view. If set to false, only the text of
   445  // the selected item from beginning to end is highlighted.
   446  func (l *List) SetHighlightFullLine(highlight bool) {
   447  	l.Lock()
   448  	defer l.Unlock()
   449  
   450  	l.highlightFullLine = highlight
   451  }
   452  
   453  // ShowSecondaryText determines whether or not to show secondary item texts.
   454  func (l *List) ShowSecondaryText(show bool) {
   455  	l.Lock()
   456  	defer l.Unlock()
   457  
   458  	l.showSecondaryText = show
   459  	return
   460  }
   461  
   462  // SetScrollBarVisibility specifies the display of the scroll bar.
   463  func (l *List) SetScrollBarVisibility(visibility ScrollBarVisibility) {
   464  	l.Lock()
   465  	defer l.Unlock()
   466  
   467  	l.scrollBarVisibility = visibility
   468  }
   469  
   470  // SetScrollBarColor sets the color of the scroll bar.
   471  func (l *List) SetScrollBarColor(color tcell.Color) {
   472  	l.Lock()
   473  	defer l.Unlock()
   474  
   475  	l.scrollBarColor = color
   476  }
   477  
   478  // SetHover sets the flag that determines whether hovering over an item will
   479  // highlight it (without triggering callbacks set with SetSelectedFunc).
   480  func (l *List) SetHover(hover bool) {
   481  	l.Lock()
   482  	defer l.Unlock()
   483  
   484  	l.hover = hover
   485  }
   486  
   487  // SetWrapAround sets the flag that determines whether navigating the list will
   488  // wrap around. That is, navigating downwards on the last item will move the
   489  // selection to the first item (similarly in the other direction). If set to
   490  // false, the selection won't change when navigating downwards on the last item
   491  // or navigating upwards on the first item.
   492  func (l *List) SetWrapAround(wrapAround bool) {
   493  	l.Lock()
   494  	defer l.Unlock()
   495  
   496  	l.wrapAround = wrapAround
   497  }
   498  
   499  // SetChangedFunc sets the function which is called when the user navigates to
   500  // a list item. The function receives the item's index in the list of items
   501  // (starting with 0) and the list item.
   502  //
   503  // This function is also called when the first item is added or when
   504  // SetCurrentItem() is called.
   505  func (l *List) SetChangedFunc(handler func(index int, item *ListItem)) {
   506  	l.Lock()
   507  	defer l.Unlock()
   508  
   509  	l.changed = handler
   510  }
   511  
   512  // SetSelectedFunc sets the function which is called when the user selects a
   513  // list item by pressing Enter on the current selection. The function receives
   514  // the item's index in the list of items (starting with 0) and its struct.
   515  func (l *List) SetSelectedFunc(handler func(int, *ListItem)) {
   516  	l.Lock()
   517  	defer l.Unlock()
   518  
   519  	l.selected = handler
   520  }
   521  
   522  // SetDoneFunc sets a function which is called when the user presses the Escape
   523  // key.
   524  func (l *List) SetDoneFunc(handler func()) {
   525  	l.Lock()
   526  	defer l.Unlock()
   527  
   528  	l.done = handler
   529  }
   530  
   531  // AddItem calls InsertItem() with an index of -1.
   532  func (l *List) AddItem(item *ListItem) {
   533  	l.InsertItem(-1, item)
   534  }
   535  
   536  // InsertItem adds a new item to the list at the specified index. An index of 0
   537  // will insert the item at the beginning, an index of 1 before the second item,
   538  // and so on. An index of GetItemCount() or higher will insert the item at the
   539  // end of the list. Negative indices are also allowed: An index of -1 will
   540  // insert the item at the end of the list, an index of -2 before the last item,
   541  // and so on. An index of -GetItemCount()-1 or lower will insert the item at the
   542  // beginning.
   543  //
   544  // An item has a main text which will be highlighted when selected. It also has
   545  // a secondary text which is shown underneath the main text (if it is set to
   546  // visible) but which may remain empty.
   547  //
   548  // The shortcut is a key binding. If the specified rune is entered, the item
   549  // is selected immediately. Set to 0 for no binding.
   550  //
   551  // The "selected" callback will be invoked when the user selects the item. You
   552  // may provide nil if no such callback is needed or if all events are handled
   553  // through the selected callback set with SetSelectedFunc().
   554  //
   555  // The currently selected item will shift its position accordingly. If the list
   556  // was previously empty, a "changed" event is fired because the new item becomes
   557  // selected.
   558  func (l *List) InsertItem(index int, item *ListItem) {
   559  	l.Lock()
   560  
   561  	// Shift index to range.
   562  	if index < 0 {
   563  		index = len(l.items) + index + 1
   564  	}
   565  	if index < 0 {
   566  		index = 0
   567  	} else if index > len(l.items) {
   568  		index = len(l.items)
   569  	}
   570  
   571  	// Shift current item.
   572  	if l.currentItem < len(l.items) && l.currentItem >= index {
   573  		l.currentItem++
   574  	}
   575  
   576  	// Insert item (make space for the new item, then shift and insert).
   577  	l.items = append(l.items, nil)
   578  	if index < len(l.items)-1 { // -1 because l.items has already grown by one item.
   579  		copy(l.items[index+1:], l.items[index:])
   580  	}
   581  	l.items[index] = item
   582  
   583  	// Fire a "change" event for the first item in the list.
   584  	if len(l.items) == 1 && l.changed != nil {
   585  		item := l.items[0]
   586  		l.Unlock()
   587  		l.changed(0, item)
   588  	} else {
   589  		l.Unlock()
   590  	}
   591  }
   592  
   593  // GetItem returns the ListItem at the given index.
   594  // Returns nil when index is out of bounds.
   595  func (l *List) GetItem(index int) *ListItem {
   596  	if index > len(l.items)-1 {
   597  		return nil
   598  	}
   599  	return l.items[index]
   600  }
   601  
   602  // GetItemCount returns the number of items in the list.
   603  func (l *List) GetItemCount() int {
   604  	l.RLock()
   605  	defer l.RUnlock()
   606  
   607  	return len(l.items)
   608  }
   609  
   610  // GetItemText returns an item's texts (main and secondary). Panics if the index
   611  // is out of range.
   612  func (l *List) GetItemText(index int) (main, secondary string) {
   613  	l.RLock()
   614  	defer l.RUnlock()
   615  	return string(l.items[index].mainText), string(l.items[index].secondaryText)
   616  }
   617  
   618  // SetItemText sets an item's main and secondary text. Panics if the index is
   619  // out of range.
   620  func (l *List) SetItemText(index int, main, secondary string) {
   621  	l.Lock()
   622  	defer l.Unlock()
   623  
   624  	item := l.items[index]
   625  	item.mainText = []byte(main)
   626  	item.secondaryText = []byte(secondary)
   627  }
   628  
   629  // SetItemEnabled sets whether an item is selectable. Panics if the index is
   630  // out of range.
   631  func (l *List) SetItemEnabled(index int, enabled bool) {
   632  	l.Lock()
   633  	defer l.Unlock()
   634  
   635  	item := l.items[index]
   636  	item.disabled = !enabled
   637  }
   638  
   639  // SetIndicators is used to set prefix and suffix indicators for selected and unselected items.
   640  func (l *List) SetIndicators(selectedPrefix, selectedSuffix, unselectedPrefix, unselectedSuffix string) {
   641  	l.Lock()
   642  	defer l.Unlock()
   643  	l.selectedPrefix = []byte(selectedPrefix)
   644  	l.selectedSuffix = []byte(selectedSuffix)
   645  	l.unselectedPrefix = []byte(unselectedPrefix)
   646  	l.unselectedSuffix = []byte(unselectedSuffix)
   647  	l.prefixWidth = len(selectedPrefix)
   648  	if len(unselectedPrefix) > l.prefixWidth {
   649  		l.prefixWidth = len(unselectedPrefix)
   650  	}
   651  	l.suffixWidth = len(selectedSuffix)
   652  	if len(unselectedSuffix) > l.suffixWidth {
   653  		l.suffixWidth = len(unselectedSuffix)
   654  	}
   655  }
   656  
   657  // FindItems searches the main and secondary texts for the given strings and
   658  // returns a list of item indices in which those strings are found. One of the
   659  // two search strings may be empty, it will then be ignored. Indices are always
   660  // returned in ascending order.
   661  //
   662  // If mustContainBoth is set to true, mainSearch must be contained in the main
   663  // text AND secondarySearch must be contained in the secondary text. If it is
   664  // false, only one of the two search strings must be contained.
   665  //
   666  // Set ignoreCase to true for case-insensitive search.
   667  func (l *List) FindItems(mainSearch, secondarySearch string, mustContainBoth, ignoreCase bool) (indices []int) {
   668  	l.RLock()
   669  	defer l.RUnlock()
   670  
   671  	if mainSearch == "" && secondarySearch == "" {
   672  		return
   673  	}
   674  
   675  	if ignoreCase {
   676  		mainSearch = strings.ToLower(mainSearch)
   677  		secondarySearch = strings.ToLower(secondarySearch)
   678  	}
   679  
   680  	mainSearchBytes := []byte(mainSearch)
   681  	secondarySearchBytes := []byte(secondarySearch)
   682  
   683  	for index, item := range l.items {
   684  		mainText := item.mainText
   685  		secondaryText := item.secondaryText
   686  		if ignoreCase {
   687  			mainText = bytes.ToLower(mainText)
   688  			secondaryText = bytes.ToLower(secondaryText)
   689  		}
   690  
   691  		// strings.Contains() always returns true for a "" search.
   692  		mainContained := bytes.Contains(mainText, mainSearchBytes)
   693  		secondaryContained := bytes.Contains(secondaryText, secondarySearchBytes)
   694  		if mustContainBoth && mainContained && secondaryContained ||
   695  			!mustContainBoth && (len(mainText) > 0 && mainContained || len(secondaryText) > 0 && secondaryContained) {
   696  			indices = append(indices, index)
   697  		}
   698  	}
   699  
   700  	return
   701  }
   702  
   703  // Clear removes all items from the list.
   704  func (l *List) Clear() {
   705  	l.Lock()
   706  	defer l.Unlock()
   707  
   708  	l.items = nil
   709  	l.currentItem = 0
   710  	l.itemOffset = 0
   711  	l.columnOffset = 0
   712  }
   713  
   714  // Focus is called by the application when the primitive receives focus.
   715  func (l *List) Focus(delegate func(p Primitive)) {
   716  	l.Box.Focus(delegate)
   717  	if l.ContextMenu.open {
   718  		delegate(l.ContextMenu.list)
   719  	}
   720  }
   721  
   722  // HasFocus returns whether or not this primitive has focus.
   723  func (l *List) HasFocus() bool {
   724  	if l.ContextMenu.open {
   725  		return l.ContextMenu.list.HasFocus()
   726  	}
   727  
   728  	l.RLock()
   729  	defer l.RUnlock()
   730  	return l.hasFocus
   731  }
   732  
   733  // Transform modifies the current selection.
   734  func (l *List) Transform(tr Transformation) {
   735  	l.Lock()
   736  
   737  	previousItem := l.currentItem
   738  
   739  	l.transform(tr)
   740  
   741  	if l.currentItem != previousItem && l.currentItem < len(l.items) && l.changed != nil {
   742  		item := l.items[l.currentItem]
   743  		l.Unlock()
   744  		l.changed(l.currentItem, item)
   745  	} else {
   746  		l.Unlock()
   747  	}
   748  }
   749  
   750  func (l *List) transform(tr Transformation) {
   751  	var decreasing bool
   752  
   753  	pageItems := l.height
   754  	if l.showSecondaryText {
   755  		pageItems /= 2
   756  	}
   757  	if pageItems < 1 {
   758  		pageItems = 1
   759  	}
   760  
   761  	switch tr {
   762  	case TransformFirstItem:
   763  		l.currentItem = 0
   764  		l.itemOffset = 0
   765  		decreasing = true
   766  	case TransformLastItem:
   767  		l.currentItem = len(l.items) - 1
   768  	case TransformPreviousItem:
   769  		l.currentItem--
   770  		decreasing = true
   771  	case TransformNextItem:
   772  		l.currentItem++
   773  	case TransformPreviousPage:
   774  		l.currentItem -= pageItems
   775  		decreasing = true
   776  	case TransformNextPage:
   777  		l.currentItem += pageItems
   778  		l.itemOffset += pageItems
   779  	}
   780  
   781  	for i := 0; i < len(l.items); i++ {
   782  		if l.currentItem < 0 {
   783  			if l.wrapAround {
   784  				l.currentItem = len(l.items) - 1
   785  			} else {
   786  				l.currentItem = 0
   787  				l.itemOffset = 0
   788  			}
   789  		} else if l.currentItem >= len(l.items) {
   790  			if l.wrapAround {
   791  				l.currentItem = 0
   792  				l.itemOffset = 0
   793  			} else {
   794  				l.currentItem = len(l.items) - 1
   795  			}
   796  		}
   797  
   798  		item := l.items[l.currentItem]
   799  		if !item.disabled && (item.shortcut > 0 || len(item.mainText) > 0 || len(item.secondaryText) > 0) {
   800  			break
   801  		}
   802  
   803  		if decreasing {
   804  			l.currentItem--
   805  		} else {
   806  			l.currentItem++
   807  		}
   808  	}
   809  
   810  	l.updateOffset()
   811  }
   812  
   813  func (l *List) updateOffset() {
   814  	_, _, _, l.height = l.GetInnerRect()
   815  
   816  	h := l.height
   817  	if l.selectedAlwaysCentered {
   818  		h /= 2
   819  	}
   820  
   821  	if l.currentItem < l.itemOffset {
   822  		l.itemOffset = l.currentItem
   823  	} else if l.showSecondaryText {
   824  		if 2*(l.currentItem-l.itemOffset) >= h-1 {
   825  			l.itemOffset = (2*l.currentItem + 3 - h) / 2
   826  		}
   827  	} else {
   828  		if l.currentItem-l.itemOffset >= h {
   829  			l.itemOffset = l.currentItem + 1 - h
   830  		}
   831  	}
   832  
   833  	if l.showSecondaryText {
   834  		if l.itemOffset > len(l.items)-(l.height/2) {
   835  			l.itemOffset = len(l.items) - l.height/2
   836  		}
   837  	} else {
   838  		if l.itemOffset > len(l.items)-l.height {
   839  			l.itemOffset = len(l.items) - l.height
   840  		}
   841  	}
   842  
   843  	if l.itemOffset < 0 {
   844  		l.itemOffset = 0
   845  	}
   846  
   847  	// Maximum width of item text
   848  	maxWidth := 0
   849  	for _, option := range l.items {
   850  		strWidth := TaggedTextWidth(option.mainText)
   851  		secondaryWidth := TaggedTextWidth(option.secondaryText)
   852  		if secondaryWidth > strWidth {
   853  			strWidth = secondaryWidth
   854  		}
   855  		if option.shortcut != 0 {
   856  			strWidth += 4
   857  		}
   858  
   859  		if strWidth > maxWidth {
   860  			maxWidth = strWidth
   861  		}
   862  	}
   863  
   864  	// Additional width for scroll bar
   865  	addWidth := 0
   866  	if l.scrollBarVisibility == ScrollBarAlways ||
   867  		(l.scrollBarVisibility == ScrollBarAuto &&
   868  			((!l.showSecondaryText && len(l.items) > l.innerHeight) ||
   869  				(l.showSecondaryText && len(l.items) > l.innerHeight/2))) {
   870  		addWidth = 1
   871  	}
   872  
   873  	if l.columnOffset > (maxWidth-l.innerWidth)+addWidth {
   874  		l.columnOffset = (maxWidth - l.innerWidth) + addWidth
   875  	}
   876  	if l.columnOffset < 0 {
   877  		l.columnOffset = 0
   878  	}
   879  }
   880  
   881  // Draw draws this primitive onto the screen.
   882  func (l *List) Draw(screen tcell.Screen) {
   883  	if !l.GetVisible() {
   884  		return
   885  	}
   886  
   887  	l.Box.Draw(screen)
   888  	hasFocus := l.GetFocusable().HasFocus()
   889  
   890  	l.Lock()
   891  	defer l.Unlock()
   892  
   893  	// Determine the dimensions.
   894  	x, y, width, height := l.GetInnerRect()
   895  	leftEdge := x
   896  	fullWidth := width + l.paddingLeft + l.paddingRight + l.prefixWidth + l.suffixWidth
   897  	bottomLimit := y + height
   898  
   899  	l.height = height
   900  
   901  	screenWidth, _ := screen.Size()
   902  	scrollBarHeight := height
   903  	scrollBarX := x + (width - 1) + l.paddingLeft + l.paddingRight
   904  	if scrollBarX > screenWidth-1 {
   905  		scrollBarX = screenWidth - 1
   906  	}
   907  
   908  	// Halve scroll bar height when drawing two lines per list item.
   909  	if l.showSecondaryText {
   910  		scrollBarHeight /= 2
   911  	}
   912  
   913  	// Do we show any shortcuts?
   914  	var showShortcuts bool
   915  	for _, item := range l.items {
   916  		if item.shortcut != 0 {
   917  			showShortcuts = true
   918  			x += 4
   919  			width -= 4
   920  			break
   921  		}
   922  	}
   923  
   924  	// Adjust offset to keep the current selection in view.
   925  	if l.selectedAlwaysVisible || l.selectedAlwaysCentered {
   926  		l.updateOffset()
   927  	}
   928  
   929  	scrollBarCursor := int(float64(len(l.items)) * (float64(l.itemOffset) / float64(len(l.items)-height)))
   930  
   931  	// Draw the list items.
   932  	for index, item := range l.items {
   933  		if index < l.itemOffset {
   934  			continue
   935  		}
   936  
   937  		if y >= bottomLimit {
   938  			break
   939  		}
   940  
   941  		mainText := item.mainText
   942  		secondaryText := item.secondaryText
   943  		if l.columnOffset > 0 {
   944  			if l.columnOffset < len(mainText) {
   945  				mainText = mainText[l.columnOffset:]
   946  			} else {
   947  				mainText = nil
   948  			}
   949  			if l.columnOffset < len(secondaryText) {
   950  				secondaryText = secondaryText[l.columnOffset:]
   951  			} else {
   952  				secondaryText = nil
   953  			}
   954  		}
   955  
   956  		if len(item.mainText) == 0 && len(item.secondaryText) == 0 && item.shortcut == 0 { // Divider
   957  			Print(screen, []byte(string(tcell.RuneLTee)), leftEdge-2, y, 1, AlignLeft, l.mainTextColor)
   958  			Print(screen, bytes.Repeat([]byte(string(tcell.RuneHLine)), fullWidth), leftEdge-1, y, fullWidth, AlignLeft, l.mainTextColor)
   959  			Print(screen, []byte(string(tcell.RuneRTee)), leftEdge+fullWidth-1, y, 1, AlignLeft, l.mainTextColor)
   960  
   961  			RenderScrollBar(screen, l.scrollBarVisibility, scrollBarX, y, scrollBarHeight, len(l.items), scrollBarCursor, index-l.itemOffset, l.hasFocus, l.scrollBarColor)
   962  			y++
   963  			continue
   964  		}
   965  
   966  		if index == l.currentItem {
   967  			if len(l.selectedPrefix) > 0 {
   968  				mainText = append(l.selectedPrefix, mainText...)
   969  			}
   970  			if len(l.selectedSuffix) > 0 {
   971  				mainText = append(mainText, l.selectedSuffix...)
   972  			}
   973  
   974  		} else {
   975  			if len(l.unselectedPrefix) > 0 {
   976  				mainText = append(l.unselectedPrefix, mainText...)
   977  			}
   978  			if len(l.unselectedSuffix) > 0 {
   979  				mainText = append(mainText, l.unselectedSuffix...)
   980  			}
   981  		}
   982  		if item.disabled {
   983  			// Shortcuts.
   984  			if showShortcuts && item.shortcut != 0 {
   985  				Print(screen, []byte(fmt.Sprintf("(%c)", item.shortcut)), x-5, y, 4, AlignRight, tcell.ColorDarkSlateGray.TrueColor())
   986  			}
   987  
   988  			// Main text.
   989  			Print(screen, mainText, x, y, width, AlignLeft, tcell.ColorGray.TrueColor())
   990  
   991  			RenderScrollBar(screen, l.scrollBarVisibility, scrollBarX, y, scrollBarHeight, len(l.items), scrollBarCursor, index-l.itemOffset, l.hasFocus, l.scrollBarColor)
   992  			y++
   993  			continue
   994  		}
   995  
   996  		// Shortcuts.
   997  		if showShortcuts && item.shortcut != 0 {
   998  			Print(screen, []byte(fmt.Sprintf("(%c)", item.shortcut)), x-5, y, 4, AlignRight, l.shortcutColor)
   999  		}
  1000  
  1001  		// Main text.
  1002  		Print(screen, mainText, x, y, width, AlignLeft, l.mainTextColor)
  1003  
  1004  		// Background color of selected text.
  1005  		if index == l.currentItem && (!l.selectedFocusOnly || hasFocus) {
  1006  			textWidth := width
  1007  			if !l.highlightFullLine {
  1008  				if w := TaggedTextWidth(mainText); w < textWidth {
  1009  					textWidth = w
  1010  				}
  1011  			}
  1012  
  1013  			for bx := 0; bx < textWidth; bx++ {
  1014  				m, c, style, _ := screen.GetContent(x+bx, y)
  1015  				fg, _, _ := style.Decompose()
  1016  				if fg == l.mainTextColor {
  1017  					fg = l.selectedTextColor
  1018  				}
  1019  				style = SetAttributes(style.Background(l.selectedBackgroundColor).Foreground(fg), l.selectedTextAttributes)
  1020  				screen.SetContent(x+bx, y, m, c, style)
  1021  			}
  1022  		}
  1023  
  1024  		RenderScrollBar(screen, l.scrollBarVisibility, scrollBarX, y, scrollBarHeight, len(l.items), scrollBarCursor, index-l.itemOffset, l.hasFocus, l.scrollBarColor)
  1025  
  1026  		y++
  1027  
  1028  		if y >= bottomLimit {
  1029  			break
  1030  		}
  1031  
  1032  		// Secondary text.
  1033  		if l.showSecondaryText {
  1034  			Print(screen, secondaryText, x, y, width, AlignLeft, l.secondaryTextColor)
  1035  
  1036  			RenderScrollBar(screen, l.scrollBarVisibility, scrollBarX, y, scrollBarHeight, len(l.items), scrollBarCursor, index-l.itemOffset, l.hasFocus, l.scrollBarColor)
  1037  
  1038  			y++
  1039  		}
  1040  	}
  1041  
  1042  	// Overdraw scroll bar when necessary.
  1043  	for y < bottomLimit {
  1044  		RenderScrollBar(screen, l.scrollBarVisibility, scrollBarX, y, scrollBarHeight, len(l.items), scrollBarCursor, bottomLimit-y, l.hasFocus, l.scrollBarColor)
  1045  
  1046  		y++
  1047  	}
  1048  
  1049  	// Draw context menu.
  1050  	if hasFocus && l.ContextMenu.open {
  1051  		ctx := l.ContextMenuList()
  1052  
  1053  		x, y, width, height = l.GetInnerRect()
  1054  
  1055  		// What's the longest option text?
  1056  		maxWidth := 0
  1057  		for _, option := range ctx.items {
  1058  			strWidth := TaggedTextWidth(option.mainText)
  1059  			if option.shortcut != 0 {
  1060  				strWidth += 4
  1061  			}
  1062  			if strWidth > maxWidth {
  1063  				maxWidth = strWidth
  1064  			}
  1065  		}
  1066  
  1067  		lheight := len(ctx.items)
  1068  		lwidth := maxWidth
  1069  
  1070  		// Add space for borders
  1071  		lwidth += 2
  1072  		lheight += 2
  1073  
  1074  		lwidth += ctx.paddingLeft + ctx.paddingRight
  1075  		lheight += ctx.paddingTop + ctx.paddingBottom
  1076  
  1077  		cx, cy := l.ContextMenu.x, l.ContextMenu.y
  1078  		if cx < 0 || cy < 0 {
  1079  			offsetX := 7
  1080  			if showShortcuts {
  1081  				offsetX += 4
  1082  			}
  1083  			offsetY := l.currentItem
  1084  			if l.showSecondaryText {
  1085  				offsetY *= 2
  1086  			}
  1087  			x, y, _, _ := l.GetInnerRect()
  1088  			cx, cy = x+offsetX, y+offsetY
  1089  		}
  1090  
  1091  		_, sheight := screen.Size()
  1092  		if cy+lheight >= sheight && cy-2 > lheight-cy {
  1093  			for i := (cy + lheight) - sheight; i > 0; i-- {
  1094  				cy--
  1095  				if cy+lheight < sheight {
  1096  					break
  1097  				}
  1098  			}
  1099  			if cy < 0 {
  1100  				cy = 0
  1101  			}
  1102  		}
  1103  		if cy+lheight >= sheight {
  1104  			lheight = sheight - cy
  1105  		}
  1106  
  1107  		if ctx.scrollBarVisibility == ScrollBarAlways || (ctx.scrollBarVisibility == ScrollBarAuto && len(ctx.items) > lheight) {
  1108  			lwidth++ // Add space for scroll bar
  1109  		}
  1110  
  1111  		ctx.SetRect(cx, cy, lwidth, lheight)
  1112  		ctx.Draw(screen)
  1113  	}
  1114  }
  1115  
  1116  // InputHandler returns the handler for this primitive.
  1117  func (l *List) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
  1118  	return l.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
  1119  		l.Lock()
  1120  
  1121  		if HitShortcut(event, Keys.Cancel) {
  1122  			if l.ContextMenu.open {
  1123  				l.Unlock()
  1124  
  1125  				l.ContextMenu.hide(setFocus)
  1126  				return
  1127  			}
  1128  
  1129  			if l.done != nil {
  1130  				l.Unlock()
  1131  				l.done()
  1132  			} else {
  1133  				l.Unlock()
  1134  			}
  1135  			return
  1136  		} else if HitShortcut(event, Keys.Select, Keys.Select2) {
  1137  			if l.currentItem >= 0 && l.currentItem < len(l.items) {
  1138  				item := l.items[l.currentItem]
  1139  				if !item.disabled {
  1140  					if item.selected != nil {
  1141  						l.Unlock()
  1142  						item.selected()
  1143  						l.Lock()
  1144  					}
  1145  					if l.selected != nil {
  1146  						l.Unlock()
  1147  						l.selected(l.currentItem, item)
  1148  						l.Lock()
  1149  					}
  1150  				}
  1151  			}
  1152  		} else if HitShortcut(event, Keys.ShowContextMenu) {
  1153  			defer l.ContextMenu.show(l.currentItem, -1, -1, setFocus)
  1154  		} else if len(l.items) == 0 {
  1155  			l.Unlock()
  1156  			return
  1157  		}
  1158  
  1159  		if event.Key() == tcell.KeyRune {
  1160  			ch := event.Rune()
  1161  			if ch != ' ' {
  1162  				// It's not a space bar. Is it a shortcut?
  1163  				for index, item := range l.items {
  1164  					if !item.disabled && item.shortcut == ch {
  1165  						// We have a shortcut.
  1166  						l.currentItem = index
  1167  
  1168  						item := l.items[l.currentItem]
  1169  						if item.selected != nil {
  1170  							l.Unlock()
  1171  							item.selected()
  1172  							l.Lock()
  1173  						}
  1174  						if l.selected != nil {
  1175  							l.Unlock()
  1176  							l.selected(l.currentItem, item)
  1177  							l.Lock()
  1178  						}
  1179  
  1180  						l.Unlock()
  1181  						return
  1182  					}
  1183  				}
  1184  			}
  1185  		}
  1186  
  1187  		previousItem := l.currentItem
  1188  
  1189  		if HitShortcut(event, Keys.MoveFirst, Keys.MoveFirst2) {
  1190  			l.transform(TransformFirstItem)
  1191  		} else if HitShortcut(event, Keys.MoveLast, Keys.MoveLast2) {
  1192  			l.transform(TransformLastItem)
  1193  		} else if HitShortcut(event, Keys.MoveUp, Keys.MoveUp2) {
  1194  			l.transform(TransformPreviousItem)
  1195  		} else if HitShortcut(event, Keys.MoveDown, Keys.MoveDown2) {
  1196  			l.transform(TransformNextItem)
  1197  		} else if HitShortcut(event, Keys.MoveLeft, Keys.MoveLeft2) {
  1198  			l.columnOffset--
  1199  			l.updateOffset()
  1200  		} else if HitShortcut(event, Keys.MoveRight, Keys.MoveRight2) {
  1201  			l.columnOffset++
  1202  			l.updateOffset()
  1203  		} else if HitShortcut(event, Keys.MovePreviousPage) {
  1204  			l.transform(TransformPreviousPage)
  1205  		} else if HitShortcut(event, Keys.MoveNextPage) {
  1206  			l.transform(TransformNextPage)
  1207  		}
  1208  
  1209  		if l.currentItem != previousItem && l.currentItem < len(l.items) && l.changed != nil {
  1210  			item := l.items[l.currentItem]
  1211  			l.Unlock()
  1212  			l.changed(l.currentItem, item)
  1213  		} else {
  1214  			l.Unlock()
  1215  		}
  1216  	})
  1217  }
  1218  
  1219  // indexAtY returns the index of the list item found at the given Y position
  1220  // or a negative value if there is no such list item.
  1221  func (l *List) indexAtY(y int) int {
  1222  	_, rectY, _, height := l.GetInnerRect()
  1223  	if y < rectY || y >= rectY+height {
  1224  		return -1
  1225  	}
  1226  
  1227  	index := y - rectY
  1228  	if l.showSecondaryText {
  1229  		index /= 2
  1230  	}
  1231  	index += l.itemOffset
  1232  
  1233  	if index >= len(l.items) {
  1234  		return -1
  1235  	}
  1236  	return index
  1237  }
  1238  
  1239  // indexAtPoint returns the index of the list item found at the given position
  1240  // or a negative value if there is no such list item.
  1241  func (l *List) indexAtPoint(x, y int) int {
  1242  	rectX, rectY, width, height := l.GetInnerRect()
  1243  	if x < rectX || x >= rectX+width || y < rectY || y >= rectY+height {
  1244  		return -1
  1245  	}
  1246  
  1247  	index := y - rectY
  1248  	if l.showSecondaryText {
  1249  		index /= 2
  1250  	}
  1251  	index += l.itemOffset
  1252  
  1253  	if index >= len(l.items) {
  1254  		return -1
  1255  	}
  1256  	return index
  1257  }
  1258  
  1259  // MouseHandler returns the mouse handler for this primitive.
  1260  func (l *List) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
  1261  	return l.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
  1262  		l.Lock()
  1263  
  1264  		// Pass events to context menu.
  1265  		if l.ContextMenuVisible() && l.ContextMenuList().InRect(event.Position()) {
  1266  			defer l.ContextMenuList().MouseHandler()(action, event, setFocus)
  1267  			consumed = true
  1268  			l.Unlock()
  1269  			return
  1270  		}
  1271  
  1272  		if !l.InRect(event.Position()) {
  1273  			l.Unlock()
  1274  			return false, nil
  1275  		}
  1276  
  1277  		// Process mouse event.
  1278  		switch action {
  1279  		case MouseLeftClick:
  1280  			if l.ContextMenuVisible() {
  1281  				defer l.ContextMenu.hide(setFocus)
  1282  				consumed = true
  1283  				l.Unlock()
  1284  				return
  1285  			}
  1286  
  1287  			l.Unlock()
  1288  			setFocus(l)
  1289  			l.Lock()
  1290  
  1291  			index := l.indexAtPoint(event.Position())
  1292  			if index != -1 {
  1293  				item := l.items[index]
  1294  				if !item.disabled {
  1295  					l.currentItem = index
  1296  					if item.selected != nil {
  1297  						l.Unlock()
  1298  						item.selected()
  1299  						l.Lock()
  1300  					}
  1301  					if l.selected != nil {
  1302  						l.Unlock()
  1303  						l.selected(index, item)
  1304  						l.Lock()
  1305  					}
  1306  					if index != l.currentItem && l.changed != nil {
  1307  						l.Unlock()
  1308  						l.changed(index, item)
  1309  						l.Lock()
  1310  					}
  1311  				}
  1312  			}
  1313  			consumed = true
  1314  		case MouseMiddleClick:
  1315  			if l.ContextMenu.open {
  1316  				defer l.ContextMenu.hide(setFocus)
  1317  				consumed = true
  1318  				l.Unlock()
  1319  				return
  1320  			}
  1321  		case MouseRightDown:
  1322  			if len(l.ContextMenuList().items) == 0 {
  1323  				l.Unlock()
  1324  				return
  1325  			}
  1326  
  1327  			x, y := event.Position()
  1328  
  1329  			index := l.indexAtPoint(event.Position())
  1330  			if index != -1 {
  1331  				item := l.items[index]
  1332  				if !item.disabled {
  1333  					l.currentItem = index
  1334  					if index != l.currentItem && l.changed != nil {
  1335  						l.Unlock()
  1336  						l.changed(index, item)
  1337  						l.Lock()
  1338  					}
  1339  				}
  1340  			}
  1341  
  1342  			defer l.ContextMenu.show(l.currentItem, x, y, setFocus)
  1343  			l.ContextMenu.drag = true
  1344  			consumed = true
  1345  		case MouseMove:
  1346  			if l.hover {
  1347  				_, y := event.Position()
  1348  				index := l.indexAtY(y)
  1349  				if index >= 0 {
  1350  					item := l.items[index]
  1351  					if !item.disabled {
  1352  						l.currentItem = index
  1353  					}
  1354  				}
  1355  
  1356  				consumed = true
  1357  			}
  1358  		case MouseScrollUp:
  1359  			if l.itemOffset > 0 {
  1360  				l.itemOffset--
  1361  			}
  1362  			consumed = true
  1363  		case MouseScrollDown:
  1364  			lines := len(l.items) - l.itemOffset
  1365  			if l.showSecondaryText {
  1366  				lines *= 2
  1367  			}
  1368  			if _, _, _, height := l.GetInnerRect(); lines > height {
  1369  				l.itemOffset++
  1370  			}
  1371  			consumed = true
  1372  		}
  1373  
  1374  		l.Unlock()
  1375  		return
  1376  	})
  1377  }
  1378  

View as plain text