...

Source file src/codeberg.org/tslocum/cview/list.go

Documentation: codeberg.org/tslocum/cview

     1  package cview
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"strings"
     7  	"sync"
     8  
     9  	"github.com/gdamore/tcell/v3"
    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  }
   460  
   461  // SetScrollBarVisibility specifies the display of the scroll bar.
   462  func (l *List) SetScrollBarVisibility(visibility ScrollBarVisibility) {
   463  	l.Lock()
   464  	defer l.Unlock()
   465  
   466  	l.scrollBarVisibility = visibility
   467  }
   468  
   469  // SetScrollBarColor sets the color of the scroll bar.
   470  func (l *List) SetScrollBarColor(color tcell.Color) {
   471  	l.Lock()
   472  	defer l.Unlock()
   473  
   474  	l.scrollBarColor = color
   475  }
   476  
   477  // SetHover sets the flag that determines whether hovering over an item will
   478  // highlight it (without triggering callbacks set with SetSelectedFunc).
   479  func (l *List) SetHover(hover bool) {
   480  	l.Lock()
   481  	defer l.Unlock()
   482  
   483  	l.hover = hover
   484  }
   485  
   486  // SetWrapAround sets the flag that determines whether navigating the list will
   487  // wrap around. That is, navigating downwards on the last item will move the
   488  // selection to the first item (similarly in the other direction). If set to
   489  // false, the selection won't change when navigating downwards on the last item
   490  // or navigating upwards on the first item.
   491  func (l *List) SetWrapAround(wrapAround bool) {
   492  	l.Lock()
   493  	defer l.Unlock()
   494  
   495  	l.wrapAround = wrapAround
   496  }
   497  
   498  // SetChangedFunc sets the function which is called when the user navigates to
   499  // a list item. The function receives the item's index in the list of items
   500  // (starting with 0) and the list item.
   501  //
   502  // This function is also called when the first item is added or when
   503  // SetCurrentItem() is called.
   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  // SetSelectedFunc sets the function which is called when the user selects a
   512  // list item by pressing Enter on the current selection. The function receives
   513  // the item's index in the list of items (starting with 0) and its struct.
   514  func (l *List) SetSelectedFunc(handler func(int, *ListItem)) {
   515  	l.Lock()
   516  	defer l.Unlock()
   517  
   518  	l.selected = handler
   519  }
   520  
   521  // SetDoneFunc sets a function which is called when the user presses the Escape
   522  // key.
   523  func (l *List) SetDoneFunc(handler func()) {
   524  	l.Lock()
   525  	defer l.Unlock()
   526  
   527  	l.done = handler
   528  }
   529  
   530  // AddItem calls InsertItem() with an index of -1.
   531  func (l *List) AddItem(item *ListItem) {
   532  	l.InsertItem(-1, item)
   533  }
   534  
   535  // InsertItem adds a new item to the list at the specified index. An index of 0
   536  // will insert the item at the beginning, an index of 1 before the second item,
   537  // and so on. An index of GetItemCount() or higher will insert the item at the
   538  // end of the list. Negative indices are also allowed: An index of -1 will
   539  // insert the item at the end of the list, an index of -2 before the last item,
   540  // and so on. An index of -GetItemCount()-1 or lower will insert the item at the
   541  // beginning.
   542  //
   543  // An item has a main text which will be highlighted when selected. It also has
   544  // a secondary text which is shown underneath the main text (if it is set to
   545  // visible) but which may remain empty.
   546  //
   547  // The shortcut is a key binding. If the specified rune is entered, the item
   548  // is selected immediately. Set to 0 for no binding.
   549  //
   550  // The "selected" callback will be invoked when the user selects the item. You
   551  // may provide nil if no such callback is needed or if all events are handled
   552  // through the selected callback set with SetSelectedFunc().
   553  //
   554  // The currently selected item will shift its position accordingly. If the list
   555  // was previously empty, a "changed" event is fired because the new item becomes
   556  // selected.
   557  func (l *List) InsertItem(index int, item *ListItem) {
   558  	l.Lock()
   559  
   560  	// Shift index to range.
   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  	// Shift current item.
   571  	if l.currentItem < len(l.items) && l.currentItem >= index {
   572  		l.currentItem++
   573  	}
   574  
   575  	// Insert item (make space for the new item, then shift and insert).
   576  	l.items = append(l.items, nil)
   577  	if index < len(l.items)-1 { // -1 because l.items has already grown by one item.
   578  		copy(l.items[index+1:], l.items[index:])
   579  	}
   580  	l.items[index] = item
   581  
   582  	// Fire a "change" event for the first item in the list.
   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  // GetItem returns the ListItem at the given index.
   593  // Returns nil when index is out of bounds.
   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  // GetItemCount returns the number of items in the list.
   602  func (l *List) GetItemCount() int {
   603  	l.RLock()
   604  	defer l.RUnlock()
   605  
   606  	return len(l.items)
   607  }
   608  
   609  // GetItemText returns an item's texts (main and secondary). Panics if the index
   610  // is out of range.
   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  // SetItemText sets an item's main and secondary text. Panics if the index is
   618  // out of range.
   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  // SetItemEnabled sets whether an item is selectable. Panics if the index is
   629  // out of range.
   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  // SetIndicators is used to set prefix and suffix indicators for selected and unselected items.
   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  // FindItems searches the main and secondary texts for the given strings and
   657  // returns a list of item indices in which those strings are found. One of the
   658  // two search strings may be empty, it will then be ignored. Indices are always
   659  // returned in ascending order.
   660  //
   661  // If mustContainBoth is set to true, mainSearch must be contained in the main
   662  // text AND secondarySearch must be contained in the secondary text. If it is
   663  // false, only one of the two search strings must be contained.
   664  //
   665  // Set ignoreCase to true for case-insensitive search.
   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  		// strings.Contains() always returns true for a "" search.
   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  // Clear removes all items from the list.
   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  // Focus is called by the application when the primitive receives focus.
   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  // HasFocus returns whether or not this primitive has focus.
   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  // Transform modifies the current selection.
   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  	// Maximum width of item text
   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  	// Additional width for scroll bar
   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  // Draw draws this primitive onto the screen.
   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  	// Determine the dimensions.
   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  	// Halve scroll bar height when drawing two lines per list item.
   908  	if l.showSecondaryText {
   909  		scrollBarHeight /= 2
   910  	}
   911  
   912  	// Do we show any shortcuts?
   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  	// Adjust offset to keep the current selection in view.
   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  	// Draw the list items.
   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 { // Divider
   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  			// Shortcuts.
   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  			// Main text.
   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  		// Shortcuts.
   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  		// Main text.
  1001  		Print(screen, mainText, x, y, width, AlignLeft, l.mainTextColor)
  1002  
  1003  		// Background color of selected text.
  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  		// Secondary text.
  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  	// Overdraw scroll bar when necessary.
  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  	// Draw context menu.
  1049  	if hasFocus && l.ContextMenu.open {
  1050  		ctx := l.ContextMenuList()
  1051  
  1052  		// What's the longest option text?
  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  		// Add space for borders
  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++ // Add space for scroll bar
  1106  		}
  1107  
  1108  		ctx.SetRect(cx, cy, lwidth, lheight)
  1109  		ctx.Draw(screen)
  1110  	}
  1111  }
  1112  
  1113  // InputHandler returns the handler for this primitive.
  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  				// It's not a space bar. Is it a shortcut?
  1160  				for index, item := range l.items {
  1161  					if !item.disabled && str == string(item.shortcut) {
  1162  						// We have a shortcut.
  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  // indexAtY returns the index of the list item found at the given Y position
  1217  // or a negative value if there is no such list item.
  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  // indexAtPoint returns the index of the list item found at the given position
  1237  // or a negative value if there is no such list item.
  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  // MouseHandler returns the mouse handler for this primitive.
  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  		// Pass events to context menu.
  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  		// Process mouse event.
  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