...

Source file src/gitlab.com/tslocum/cview/list.go

Documentation: gitlab.com/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  	sync.RWMutex
   198  }
   199  
   200  // NewList returns a new form.
   201  func NewList() *List {
   202  	l := &List{
   203  		Box:                     NewBox(),
   204  		showSecondaryText:       true,
   205  		scrollBarVisibility:     ScrollBarAuto,
   206  		mainTextColor:           Styles.PrimaryTextColor,
   207  		secondaryTextColor:      Styles.TertiaryTextColor,
   208  		shortcutColor:           Styles.SecondaryTextColor,
   209  		selectedTextColor:       Styles.PrimitiveBackgroundColor,
   210  		scrollBarColor:          Styles.ScrollBarColor,
   211  		selectedBackgroundColor: Styles.PrimaryTextColor,
   212  	}
   213  
   214  	l.ContextMenu = NewContextMenu(l)
   215  	l.focus = l
   216  
   217  	return l
   218  }
   219  
   220  // SetCurrentItem sets the currently selected item by its index, starting at 0
   221  // for the first item. If a negative index is provided, items are referred to
   222  // from the back (-1 = last item, -2 = second-to-last item, and so on). Out of
   223  // range indices are clamped to the beginning/end.
   224  //
   225  // Calling this function triggers a "changed" event if the selection changes.
   226  func (l *List) SetCurrentItem(index int) {
   227  	l.Lock()
   228  
   229  	if index < 0 {
   230  		index = len(l.items) + index
   231  	}
   232  	if index >= len(l.items) {
   233  		index = len(l.items) - 1
   234  	}
   235  	if index < 0 {
   236  		index = 0
   237  	}
   238  
   239  	previousItem := l.currentItem
   240  	l.currentItem = index
   241  
   242  	l.updateOffset()
   243  
   244  	if index != previousItem && index < len(l.items) && l.changed != nil {
   245  		item := l.items[index]
   246  		l.Unlock()
   247  		l.changed(index, item)
   248  	} else {
   249  		l.Unlock()
   250  	}
   251  }
   252  
   253  // GetCurrentItem returns the currently selected list item,
   254  // Returns nil if no item is selected.
   255  func (l *List) GetCurrentItem() *ListItem {
   256  	l.RLock()
   257  	defer l.RUnlock()
   258  
   259  	if len(l.items) == 0 || l.currentItem >= len(l.items) {
   260  		return nil
   261  	}
   262  	return l.items[l.currentItem]
   263  }
   264  
   265  // GetCurrentItemIndex returns the index of the currently selected list item,
   266  // starting at 0 for the first item and its struct.
   267  func (l *List) GetCurrentItemIndex() int {
   268  	l.RLock()
   269  	defer l.RUnlock()
   270  	return l.currentItem
   271  }
   272  
   273  // GetItems returns all list items.
   274  func (l *List) GetItems() []*ListItem {
   275  	l.RLock()
   276  	defer l.RUnlock()
   277  	return l.items
   278  }
   279  
   280  // RemoveItem removes the item with the given index (starting at 0) from the
   281  // list. If a negative index is provided, items are referred to from the back
   282  // (-1 = last item, -2 = second-to-last item, and so on). Out of range indices
   283  // are clamped to the beginning/end, i.e. unless the list is empty, an item is
   284  // always removed.
   285  //
   286  // The currently selected item is shifted accordingly. If it is the one that is
   287  // removed, a "changed" event is fired.
   288  func (l *List) RemoveItem(index int) {
   289  	l.Lock()
   290  
   291  	if len(l.items) == 0 {
   292  		l.Unlock()
   293  		return
   294  	}
   295  
   296  	// Adjust index.
   297  	if index < 0 {
   298  		index = len(l.items) + index
   299  	}
   300  	if index >= len(l.items) {
   301  		index = len(l.items) - 1
   302  	}
   303  	if index < 0 {
   304  		index = 0
   305  	}
   306  
   307  	// Remove item.
   308  	l.items = append(l.items[:index], l.items[index+1:]...)
   309  
   310  	// If there is nothing left, we're done.
   311  	if len(l.items) == 0 {
   312  		l.Unlock()
   313  		return
   314  	}
   315  
   316  	// Shift current item.
   317  	previousItem := l.currentItem
   318  	if l.currentItem >= index && l.currentItem > 0 {
   319  		l.currentItem--
   320  	}
   321  
   322  	// Fire "changed" event for removed items.
   323  	if previousItem == index && index < len(l.items) && l.changed != nil {
   324  		item := l.items[l.currentItem]
   325  		l.Unlock()
   326  		l.changed(l.currentItem, item)
   327  	} else {
   328  		l.Unlock()
   329  	}
   330  }
   331  
   332  // SetOffset sets the number of list items and columns by which the list is
   333  // scrolled down/to the right.
   334  func (l *List) SetOffset(items, columns int) {
   335  	l.Lock()
   336  	defer l.Unlock()
   337  
   338  	if items < 0 {
   339  		items = 0
   340  	}
   341  	if columns < 0 {
   342  		columns = 0
   343  	}
   344  
   345  	l.itemOffset, l.columnOffset = items, columns
   346  }
   347  
   348  // GetOffset returns the number of list items and columns by which the list is
   349  // scrolled down/to the right.
   350  func (l *List) GetOffset() (int, int) {
   351  	l.Lock()
   352  	defer l.Unlock()
   353  
   354  	return l.itemOffset, l.columnOffset
   355  }
   356  
   357  // SetMainTextColor sets the color of the items' main text.
   358  func (l *List) SetMainTextColor(color tcell.Color) {
   359  	l.Lock()
   360  	defer l.Unlock()
   361  
   362  	l.mainTextColor = color
   363  }
   364  
   365  // SetSecondaryTextColor sets the color of the items' secondary text.
   366  func (l *List) SetSecondaryTextColor(color tcell.Color) {
   367  	l.Lock()
   368  	defer l.Unlock()
   369  
   370  	l.secondaryTextColor = color
   371  }
   372  
   373  // SetShortcutColor sets the color of the items' shortcut.
   374  func (l *List) SetShortcutColor(color tcell.Color) {
   375  	l.Lock()
   376  	defer l.Unlock()
   377  
   378  	l.shortcutColor = color
   379  }
   380  
   381  // SetSelectedTextColor sets the text color of selected items.
   382  func (l *List) SetSelectedTextColor(color tcell.Color) {
   383  	l.Lock()
   384  	defer l.Unlock()
   385  
   386  	l.selectedTextColor = color
   387  }
   388  
   389  // SetSelectedTextAttributes sets the style attributes of selected items.
   390  func (l *List) SetSelectedTextAttributes(attr tcell.AttrMask) {
   391  	l.Lock()
   392  	defer l.Unlock()
   393  
   394  	l.selectedTextAttributes = attr
   395  }
   396  
   397  // SetSelectedBackgroundColor sets the background color of selected items.
   398  func (l *List) SetSelectedBackgroundColor(color tcell.Color) {
   399  	l.Lock()
   400  	defer l.Unlock()
   401  
   402  	l.selectedBackgroundColor = color
   403  }
   404  
   405  // SetSelectedFocusOnly sets a flag which determines when the currently selected
   406  // list item is highlighted. If set to true, selected items are only highlighted
   407  // when the list has focus. If set to false, they are always highlighted.
   408  func (l *List) SetSelectedFocusOnly(focusOnly bool) {
   409  	l.Lock()
   410  	defer l.Unlock()
   411  
   412  	l.selectedFocusOnly = focusOnly
   413  }
   414  
   415  // SetSelectedAlwaysVisible sets a flag which determines whether the currently
   416  // selected list item must remain visible when scrolling.
   417  func (l *List) SetSelectedAlwaysVisible(alwaysVisible bool) {
   418  	l.Lock()
   419  	defer l.Unlock()
   420  
   421  	l.selectedAlwaysVisible = alwaysVisible
   422  }
   423  
   424  // SetSelectedAlwaysCentered sets a flag which determines whether the currently
   425  // selected list item must remain centered when scrolling.
   426  func (l *List) SetSelectedAlwaysCentered(alwaysCentered bool) {
   427  	l.Lock()
   428  	defer l.Unlock()
   429  
   430  	l.selectedAlwaysCentered = alwaysCentered
   431  }
   432  
   433  // SetHighlightFullLine sets a flag which determines whether the colored
   434  // background of selected items spans the entire width of the view. If set to
   435  // true, the highlight spans the entire view. If set to false, only the text of
   436  // the selected item from beginning to end is highlighted.
   437  func (l *List) SetHighlightFullLine(highlight bool) {
   438  	l.Lock()
   439  	defer l.Unlock()
   440  
   441  	l.highlightFullLine = highlight
   442  }
   443  
   444  // ShowSecondaryText determines whether or not to show secondary item texts.
   445  func (l *List) ShowSecondaryText(show bool) {
   446  	l.Lock()
   447  	defer l.Unlock()
   448  
   449  	l.showSecondaryText = show
   450  	return
   451  }
   452  
   453  // SetScrollBarVisibility specifies the display of the scroll bar.
   454  func (l *List) SetScrollBarVisibility(visibility ScrollBarVisibility) {
   455  	l.Lock()
   456  	defer l.Unlock()
   457  
   458  	l.scrollBarVisibility = visibility
   459  }
   460  
   461  // SetScrollBarColor sets the color of the scroll bar.
   462  func (l *List) SetScrollBarColor(color tcell.Color) {
   463  	l.Lock()
   464  	defer l.Unlock()
   465  
   466  	l.scrollBarColor = color
   467  }
   468  
   469  // SetHover sets the flag that determines whether hovering over an item will
   470  // highlight it (without triggering callbacks set with SetSelectedFunc).
   471  func (l *List) SetHover(hover bool) {
   472  	l.Lock()
   473  	defer l.Unlock()
   474  
   475  	l.hover = hover
   476  }
   477  
   478  // SetWrapAround sets the flag that determines whether navigating the list will
   479  // wrap around. That is, navigating downwards on the last item will move the
   480  // selection to the first item (similarly in the other direction). If set to
   481  // false, the selection won't change when navigating downwards on the last item
   482  // or navigating upwards on the first item.
   483  func (l *List) SetWrapAround(wrapAround bool) {
   484  	l.Lock()
   485  	defer l.Unlock()
   486  
   487  	l.wrapAround = wrapAround
   488  }
   489  
   490  // SetChangedFunc sets the function which is called when the user navigates to
   491  // a list item. The function receives the item's index in the list of items
   492  // (starting with 0) and the list item.
   493  //
   494  // This function is also called when the first item is added or when
   495  // SetCurrentItem() is called.
   496  func (l *List) SetChangedFunc(handler func(index int, item *ListItem)) {
   497  	l.Lock()
   498  	defer l.Unlock()
   499  
   500  	l.changed = handler
   501  }
   502  
   503  // SetSelectedFunc sets the function which is called when the user selects a
   504  // list item by pressing Enter on the current selection. The function receives
   505  // the item's index in the list of items (starting with 0) and its struct.
   506  func (l *List) SetSelectedFunc(handler func(int, *ListItem)) {
   507  	l.Lock()
   508  	defer l.Unlock()
   509  
   510  	l.selected = handler
   511  }
   512  
   513  // SetDoneFunc sets a function which is called when the user presses the Escape
   514  // key.
   515  func (l *List) SetDoneFunc(handler func()) {
   516  	l.Lock()
   517  	defer l.Unlock()
   518  
   519  	l.done = handler
   520  }
   521  
   522  // AddItem calls InsertItem() with an index of -1.
   523  func (l *List) AddItem(item *ListItem) {
   524  	l.InsertItem(-1, item)
   525  }
   526  
   527  // InsertItem adds a new item to the list at the specified index. An index of 0
   528  // will insert the item at the beginning, an index of 1 before the second item,
   529  // and so on. An index of GetItemCount() or higher will insert the item at the
   530  // end of the list. Negative indices are also allowed: An index of -1 will
   531  // insert the item at the end of the list, an index of -2 before the last item,
   532  // and so on. An index of -GetItemCount()-1 or lower will insert the item at the
   533  // beginning.
   534  //
   535  // An item has a main text which will be highlighted when selected. It also has
   536  // a secondary text which is shown underneath the main text (if it is set to
   537  // visible) but which may remain empty.
   538  //
   539  // The shortcut is a key binding. If the specified rune is entered, the item
   540  // is selected immediately. Set to 0 for no binding.
   541  //
   542  // The "selected" callback will be invoked when the user selects the item. You
   543  // may provide nil if no such callback is needed or if all events are handled
   544  // through the selected callback set with SetSelectedFunc().
   545  //
   546  // The currently selected item will shift its position accordingly. If the list
   547  // was previously empty, a "changed" event is fired because the new item becomes
   548  // selected.
   549  func (l *List) InsertItem(index int, item *ListItem) {
   550  	l.Lock()
   551  
   552  	// Shift index to range.
   553  	if index < 0 {
   554  		index = len(l.items) + index + 1
   555  	}
   556  	if index < 0 {
   557  		index = 0
   558  	} else if index > len(l.items) {
   559  		index = len(l.items)
   560  	}
   561  
   562  	// Shift current item.
   563  	if l.currentItem < len(l.items) && l.currentItem >= index {
   564  		l.currentItem++
   565  	}
   566  
   567  	// Insert item (make space for the new item, then shift and insert).
   568  	l.items = append(l.items, nil)
   569  	if index < len(l.items)-1 { // -1 because l.items has already grown by one item.
   570  		copy(l.items[index+1:], l.items[index:])
   571  	}
   572  	l.items[index] = item
   573  
   574  	// Fire a "change" event for the first item in the list.
   575  	if len(l.items) == 1 && l.changed != nil {
   576  		item := l.items[0]
   577  		l.Unlock()
   578  		l.changed(0, item)
   579  	} else {
   580  		l.Unlock()
   581  	}
   582  }
   583  
   584  // GetItem returns the ListItem at the given index.
   585  // Returns nil when index is out of bounds.
   586  func (l *List) GetItem(index int) *ListItem {
   587  	if index > len(l.items)-1 {
   588  		return nil
   589  	}
   590  	return l.items[index]
   591  }
   592  
   593  // GetItemCount returns the number of items in the list.
   594  func (l *List) GetItemCount() int {
   595  	l.RLock()
   596  	defer l.RUnlock()
   597  
   598  	return len(l.items)
   599  }
   600  
   601  // GetItemText returns an item's texts (main and secondary). Panics if the index
   602  // is out of range.
   603  func (l *List) GetItemText(index int) (main, secondary string) {
   604  	l.RLock()
   605  	defer l.RUnlock()
   606  	return string(l.items[index].mainText), string(l.items[index].secondaryText)
   607  }
   608  
   609  // SetItemText sets an item's main and secondary text. Panics if the index is
   610  // out of range.
   611  func (l *List) SetItemText(index int, main, secondary string) {
   612  	l.Lock()
   613  	defer l.Unlock()
   614  
   615  	item := l.items[index]
   616  	item.mainText = []byte(main)
   617  	item.secondaryText = []byte(secondary)
   618  }
   619  
   620  // SetItemEnabled sets whether an item is selectable. Panics if the index is
   621  // out of range.
   622  func (l *List) SetItemEnabled(index int, enabled bool) {
   623  	l.Lock()
   624  	defer l.Unlock()
   625  
   626  	item := l.items[index]
   627  	item.disabled = !enabled
   628  }
   629  
   630  // FindItems searches the main and secondary texts for the given strings and
   631  // returns a list of item indices in which those strings are found. One of the
   632  // two search strings may be empty, it will then be ignored. Indices are always
   633  // returned in ascending order.
   634  //
   635  // If mustContainBoth is set to true, mainSearch must be contained in the main
   636  // text AND secondarySearch must be contained in the secondary text. If it is
   637  // false, only one of the two search strings must be contained.
   638  //
   639  // Set ignoreCase to true for case-insensitive search.
   640  func (l *List) FindItems(mainSearch, secondarySearch string, mustContainBoth, ignoreCase bool) (indices []int) {
   641  	l.RLock()
   642  	defer l.RUnlock()
   643  
   644  	if mainSearch == "" && secondarySearch == "" {
   645  		return
   646  	}
   647  
   648  	if ignoreCase {
   649  		mainSearch = strings.ToLower(mainSearch)
   650  		secondarySearch = strings.ToLower(secondarySearch)
   651  	}
   652  
   653  	mainSearchBytes := []byte(mainSearch)
   654  	secondarySearchBytes := []byte(secondarySearch)
   655  
   656  	for index, item := range l.items {
   657  		mainText := item.mainText
   658  		secondaryText := item.secondaryText
   659  		if ignoreCase {
   660  			mainText = bytes.ToLower(mainText)
   661  			secondaryText = bytes.ToLower(secondaryText)
   662  		}
   663  
   664  		// strings.Contains() always returns true for a "" search.
   665  		mainContained := bytes.Contains(mainText, mainSearchBytes)
   666  		secondaryContained := bytes.Contains(secondaryText, secondarySearchBytes)
   667  		if mustContainBoth && mainContained && secondaryContained ||
   668  			!mustContainBoth && (len(mainText) > 0 && mainContained || len(secondaryText) > 0 && secondaryContained) {
   669  			indices = append(indices, index)
   670  		}
   671  	}
   672  
   673  	return
   674  }
   675  
   676  // Clear removes all items from the list.
   677  func (l *List) Clear() {
   678  	l.Lock()
   679  	defer l.Unlock()
   680  
   681  	l.items = nil
   682  	l.currentItem = 0
   683  	l.itemOffset = 0
   684  	l.columnOffset = 0
   685  }
   686  
   687  // Focus is called by the application when the primitive receives focus.
   688  func (l *List) Focus(delegate func(p Primitive)) {
   689  	l.Box.Focus(delegate)
   690  	if l.ContextMenu.open {
   691  		delegate(l.ContextMenu.list)
   692  	}
   693  }
   694  
   695  // HasFocus returns whether or not this primitive has focus.
   696  func (l *List) HasFocus() bool {
   697  	if l.ContextMenu.open {
   698  		return l.ContextMenu.list.HasFocus()
   699  	}
   700  
   701  	l.RLock()
   702  	defer l.RUnlock()
   703  	return l.hasFocus
   704  }
   705  
   706  // Transform modifies the current selection.
   707  func (l *List) Transform(tr Transformation) {
   708  	l.Lock()
   709  
   710  	previousItem := l.currentItem
   711  
   712  	l.transform(tr)
   713  
   714  	if l.currentItem != previousItem && l.currentItem < len(l.items) && l.changed != nil {
   715  		item := l.items[l.currentItem]
   716  		l.Unlock()
   717  		l.changed(l.currentItem, item)
   718  	} else {
   719  		l.Unlock()
   720  	}
   721  }
   722  
   723  func (l *List) transform(tr Transformation) {
   724  	var decreasing bool
   725  
   726  	pageItems := l.height
   727  	if l.showSecondaryText {
   728  		pageItems /= 2
   729  	}
   730  	if pageItems < 1 {
   731  		pageItems = 1
   732  	}
   733  
   734  	switch tr {
   735  	case TransformFirstItem:
   736  		l.currentItem = 0
   737  		l.itemOffset = 0
   738  		decreasing = true
   739  	case TransformLastItem:
   740  		l.currentItem = len(l.items) - 1
   741  	case TransformPreviousItem:
   742  		l.currentItem--
   743  		decreasing = true
   744  	case TransformNextItem:
   745  		l.currentItem++
   746  	case TransformPreviousPage:
   747  		l.currentItem -= pageItems
   748  		decreasing = true
   749  	case TransformNextPage:
   750  		l.currentItem += pageItems
   751  		l.itemOffset += pageItems
   752  	}
   753  
   754  	for i := 0; i < len(l.items); i++ {
   755  		if l.currentItem < 0 {
   756  			if l.wrapAround {
   757  				l.currentItem = len(l.items) - 1
   758  			} else {
   759  				l.currentItem = 0
   760  				l.itemOffset = 0
   761  			}
   762  		} else if l.currentItem >= len(l.items) {
   763  			if l.wrapAround {
   764  				l.currentItem = 0
   765  				l.itemOffset = 0
   766  			} else {
   767  				l.currentItem = len(l.items) - 1
   768  			}
   769  		}
   770  
   771  		item := l.items[l.currentItem]
   772  		if !item.disabled && (item.shortcut > 0 || len(item.mainText) > 0 || len(item.secondaryText) > 0) {
   773  			break
   774  		}
   775  
   776  		if decreasing {
   777  			l.currentItem--
   778  		} else {
   779  			l.currentItem++
   780  		}
   781  	}
   782  
   783  	l.updateOffset()
   784  }
   785  
   786  func (l *List) updateOffset() {
   787  	_, _, _, l.height = l.GetInnerRect()
   788  
   789  	h := l.height
   790  	if l.selectedAlwaysCentered {
   791  		h /= 2
   792  	}
   793  
   794  	if l.currentItem < l.itemOffset {
   795  		l.itemOffset = l.currentItem
   796  	} else if l.showSecondaryText {
   797  		if 2*(l.currentItem-l.itemOffset) >= h-1 {
   798  			l.itemOffset = (2*l.currentItem + 3 - h) / 2
   799  		}
   800  	} else {
   801  		if l.currentItem-l.itemOffset >= h {
   802  			l.itemOffset = l.currentItem + 1 - h
   803  		}
   804  	}
   805  
   806  	if l.showSecondaryText {
   807  		if l.itemOffset > len(l.items)-(l.height/2) {
   808  			l.itemOffset = len(l.items) - l.height/2
   809  		}
   810  	} else {
   811  		if l.itemOffset > len(l.items)-l.height {
   812  			l.itemOffset = len(l.items) - l.height
   813  		}
   814  	}
   815  
   816  	if l.itemOffset < 0 {
   817  		l.itemOffset = 0
   818  	}
   819  
   820  	// Maximum width of item text
   821  	maxWidth := 0
   822  	for _, option := range l.items {
   823  		strWidth := TaggedTextWidth(option.mainText)
   824  		secondaryWidth := TaggedTextWidth(option.secondaryText)
   825  		if secondaryWidth > strWidth {
   826  			strWidth = secondaryWidth
   827  		}
   828  		if option.shortcut != 0 {
   829  			strWidth += 4
   830  		}
   831  
   832  		if strWidth > maxWidth {
   833  			maxWidth = strWidth
   834  		}
   835  	}
   836  
   837  	// Additional width for scroll bar
   838  	addWidth := 0
   839  	if l.scrollBarVisibility == ScrollBarAlways ||
   840  		(l.scrollBarVisibility == ScrollBarAuto &&
   841  			((!l.showSecondaryText && len(l.items) > l.innerHeight) ||
   842  				(l.showSecondaryText && len(l.items) > l.innerHeight/2))) {
   843  		addWidth = 1
   844  	}
   845  
   846  	if l.columnOffset > (maxWidth-l.innerWidth)+addWidth {
   847  		l.columnOffset = (maxWidth - l.innerWidth) + addWidth
   848  	}
   849  	if l.columnOffset < 0 {
   850  		l.columnOffset = 0
   851  	}
   852  }
   853  
   854  // Draw draws this primitive onto the screen.
   855  func (l *List) Draw(screen tcell.Screen) {
   856  	if !l.GetVisible() {
   857  		return
   858  	}
   859  
   860  	l.Box.Draw(screen)
   861  	hasFocus := l.GetFocusable().HasFocus()
   862  
   863  	l.Lock()
   864  	defer l.Unlock()
   865  
   866  	// Determine the dimensions.
   867  	x, y, width, height := l.GetInnerRect()
   868  	bottomLimit := y + height
   869  
   870  	l.height = height
   871  
   872  	screenWidth, _ := screen.Size()
   873  	scrollBarHeight := height
   874  	scrollBarX := x + (width - 1) + l.paddingLeft + l.paddingRight
   875  	if scrollBarX > screenWidth-1 {
   876  		scrollBarX = screenWidth - 1
   877  	}
   878  
   879  	// Halve scroll bar height when drawing two lines per list item.
   880  	if l.showSecondaryText {
   881  		scrollBarHeight /= 2
   882  	}
   883  
   884  	// Do we show any shortcuts?
   885  	var showShortcuts bool
   886  	for _, item := range l.items {
   887  		if item.shortcut != 0 {
   888  			showShortcuts = true
   889  			x += 4
   890  			width -= 4
   891  			break
   892  		}
   893  	}
   894  
   895  	// Adjust offset to keep the current selection in view.
   896  	if l.selectedAlwaysVisible || l.selectedAlwaysCentered {
   897  		l.updateOffset()
   898  	}
   899  
   900  	scrollBarCursor := int(float64(len(l.items)) * (float64(l.itemOffset) / float64(len(l.items)-height)))
   901  
   902  	// Draw the list items.
   903  	for index, item := range l.items {
   904  		if index < l.itemOffset {
   905  			continue
   906  		}
   907  
   908  		if y >= bottomLimit {
   909  			break
   910  		}
   911  
   912  		mainText := item.mainText
   913  		secondaryText := item.secondaryText
   914  		if l.columnOffset > 0 {
   915  			if l.columnOffset < len(mainText) {
   916  				mainText = mainText[l.columnOffset:]
   917  			} else {
   918  				mainText = nil
   919  			}
   920  			if l.columnOffset < len(secondaryText) {
   921  				secondaryText = secondaryText[l.columnOffset:]
   922  			} else {
   923  				secondaryText = nil
   924  			}
   925  		}
   926  
   927  		if len(item.mainText) == 0 && len(item.secondaryText) == 0 && item.shortcut == 0 { // Divider
   928  			Print(screen, bytes.Repeat([]byte(string(tcell.RuneHLine)), width+l.paddingLeft+l.paddingRight), x-l.paddingLeft, y, width+l.paddingLeft+l.paddingRight, AlignLeft, l.mainTextColor)
   929  
   930  			RenderScrollBar(screen, l.scrollBarVisibility, scrollBarX, y, scrollBarHeight, len(l.items), scrollBarCursor, index-l.itemOffset, l.hasFocus, l.scrollBarColor)
   931  			y++
   932  			continue
   933  		} else if item.disabled {
   934  			// Shortcuts.
   935  			if showShortcuts && item.shortcut != 0 {
   936  				Print(screen, []byte(fmt.Sprintf("(%c)", item.shortcut)), x-5, y, 4, AlignRight, tcell.ColorDarkSlateGray.TrueColor())
   937  			}
   938  
   939  			// Main text.
   940  			Print(screen, mainText, x, y, width, AlignLeft, tcell.ColorGray.TrueColor())
   941  
   942  			RenderScrollBar(screen, l.scrollBarVisibility, scrollBarX, y, scrollBarHeight, len(l.items), scrollBarCursor, index-l.itemOffset, l.hasFocus, l.scrollBarColor)
   943  			y++
   944  			continue
   945  		}
   946  
   947  		// Shortcuts.
   948  		if showShortcuts && item.shortcut != 0 {
   949  			Print(screen, []byte(fmt.Sprintf("(%c)", item.shortcut)), x-5, y, 4, AlignRight, l.shortcutColor)
   950  		}
   951  
   952  		// Main text.
   953  		Print(screen, mainText, x, y, width, AlignLeft, l.mainTextColor)
   954  
   955  		// Background color of selected text.
   956  		if index == l.currentItem && (!l.selectedFocusOnly || hasFocus) {
   957  			textWidth := width
   958  			if !l.highlightFullLine {
   959  				if w := TaggedTextWidth(mainText); w < textWidth {
   960  					textWidth = w
   961  				}
   962  			}
   963  
   964  			for bx := 0; bx < textWidth; bx++ {
   965  				m, c, style, _ := screen.GetContent(x+bx, y)
   966  				fg, _, _ := style.Decompose()
   967  				if fg == l.mainTextColor {
   968  					fg = l.selectedTextColor
   969  				}
   970  				style = SetAttributes(style.Background(l.selectedBackgroundColor).Foreground(fg), l.selectedTextAttributes)
   971  				screen.SetContent(x+bx, y, m, c, style)
   972  			}
   973  		}
   974  
   975  		RenderScrollBar(screen, l.scrollBarVisibility, scrollBarX, y, scrollBarHeight, len(l.items), scrollBarCursor, index-l.itemOffset, l.hasFocus, l.scrollBarColor)
   976  
   977  		y++
   978  
   979  		if y >= bottomLimit {
   980  			break
   981  		}
   982  
   983  		// Secondary text.
   984  		if l.showSecondaryText {
   985  			Print(screen, secondaryText, x, y, width, AlignLeft, l.secondaryTextColor)
   986  
   987  			RenderScrollBar(screen, l.scrollBarVisibility, scrollBarX, y, scrollBarHeight, len(l.items), scrollBarCursor, index-l.itemOffset, l.hasFocus, l.scrollBarColor)
   988  
   989  			y++
   990  		}
   991  	}
   992  
   993  	// Overdraw scroll bar when necessary.
   994  	for y < bottomLimit {
   995  		RenderScrollBar(screen, l.scrollBarVisibility, scrollBarX, y, scrollBarHeight, len(l.items), scrollBarCursor, bottomLimit-y, l.hasFocus, l.scrollBarColor)
   996  
   997  		y++
   998  	}
   999  
  1000  	// Draw context menu.
  1001  	if hasFocus && l.ContextMenu.open {
  1002  		ctx := l.ContextMenuList()
  1003  
  1004  		x, y, width, height = l.GetInnerRect()
  1005  
  1006  		// What's the longest option text?
  1007  		maxWidth := 0
  1008  		for _, option := range ctx.items {
  1009  			strWidth := TaggedTextWidth(option.mainText)
  1010  			if option.shortcut != 0 {
  1011  				strWidth += 4
  1012  			}
  1013  			if strWidth > maxWidth {
  1014  				maxWidth = strWidth
  1015  			}
  1016  		}
  1017  
  1018  		lheight := len(ctx.items)
  1019  		lwidth := maxWidth
  1020  
  1021  		// Add space for borders
  1022  		lwidth += 2
  1023  		lheight += 2
  1024  
  1025  		lwidth += ctx.paddingLeft + ctx.paddingRight
  1026  		lheight += ctx.paddingTop + ctx.paddingBottom
  1027  
  1028  		cx, cy := l.ContextMenu.x, l.ContextMenu.y
  1029  		if cx < 0 || cy < 0 {
  1030  			offsetX := 7
  1031  			if showShortcuts {
  1032  				offsetX += 4
  1033  			}
  1034  			offsetY := l.currentItem
  1035  			if l.showSecondaryText {
  1036  				offsetY *= 2
  1037  			}
  1038  			x, y, _, _ := l.GetInnerRect()
  1039  			cx, cy = x+offsetX, y+offsetY
  1040  		}
  1041  
  1042  		_, sheight := screen.Size()
  1043  		if cy+lheight >= sheight && cy-2 > lheight-cy {
  1044  			for i := (cy + lheight) - sheight; i > 0; i-- {
  1045  				cy--
  1046  				if cy+lheight < sheight {
  1047  					break
  1048  				}
  1049  			}
  1050  			if cy < 0 {
  1051  				cy = 0
  1052  			}
  1053  		}
  1054  		if cy+lheight >= sheight {
  1055  			lheight = sheight - cy
  1056  		}
  1057  
  1058  		if ctx.scrollBarVisibility == ScrollBarAlways || (ctx.scrollBarVisibility == ScrollBarAuto && len(ctx.items) > lheight) {
  1059  			lwidth++ // Add space for scroll bar
  1060  		}
  1061  
  1062  		ctx.SetRect(cx, cy, lwidth, lheight)
  1063  		ctx.Draw(screen)
  1064  	}
  1065  }
  1066  
  1067  // InputHandler returns the handler for this primitive.
  1068  func (l *List) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
  1069  	return l.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
  1070  		l.Lock()
  1071  
  1072  		if HitShortcut(event, Keys.Cancel) {
  1073  			if l.ContextMenu.open {
  1074  				l.Unlock()
  1075  
  1076  				l.ContextMenu.hide(setFocus)
  1077  				return
  1078  			}
  1079  
  1080  			if l.done != nil {
  1081  				l.Unlock()
  1082  				l.done()
  1083  			} else {
  1084  				l.Unlock()
  1085  			}
  1086  			return
  1087  		} else if HitShortcut(event, Keys.Select, Keys.Select2) {
  1088  			if l.currentItem >= 0 && l.currentItem < len(l.items) {
  1089  				item := l.items[l.currentItem]
  1090  				if !item.disabled {
  1091  					if item.selected != nil {
  1092  						l.Unlock()
  1093  						item.selected()
  1094  						l.Lock()
  1095  					}
  1096  					if l.selected != nil {
  1097  						l.Unlock()
  1098  						l.selected(l.currentItem, item)
  1099  						l.Lock()
  1100  					}
  1101  				}
  1102  			}
  1103  		} else if HitShortcut(event, Keys.ShowContextMenu) {
  1104  			defer l.ContextMenu.show(l.currentItem, -1, -1, setFocus)
  1105  		} else if len(l.items) == 0 {
  1106  			l.Unlock()
  1107  			return
  1108  		}
  1109  
  1110  		if event.Key() == tcell.KeyRune {
  1111  			ch := event.Rune()
  1112  			if ch != ' ' {
  1113  				// It's not a space bar. Is it a shortcut?
  1114  				for index, item := range l.items {
  1115  					if !item.disabled && item.shortcut == ch {
  1116  						// We have a shortcut.
  1117  						l.currentItem = index
  1118  
  1119  						item := l.items[l.currentItem]
  1120  						if item.selected != nil {
  1121  							l.Unlock()
  1122  							item.selected()
  1123  							l.Lock()
  1124  						}
  1125  						if l.selected != nil {
  1126  							l.Unlock()
  1127  							l.selected(l.currentItem, item)
  1128  							l.Lock()
  1129  						}
  1130  
  1131  						l.Unlock()
  1132  						return
  1133  					}
  1134  				}
  1135  			}
  1136  		}
  1137  
  1138  		previousItem := l.currentItem
  1139  
  1140  		if HitShortcut(event, Keys.MoveFirst, Keys.MoveFirst2) {
  1141  			l.transform(TransformFirstItem)
  1142  		} else if HitShortcut(event, Keys.MoveLast, Keys.MoveLast2) {
  1143  			l.transform(TransformLastItem)
  1144  		} else if HitShortcut(event, Keys.MoveUp, Keys.MoveUp2, Keys.MovePreviousField) {
  1145  			l.transform(TransformPreviousItem)
  1146  		} else if HitShortcut(event, Keys.MoveDown, Keys.MoveDown2, Keys.MoveNextField) {
  1147  			l.transform(TransformNextItem)
  1148  		} else if HitShortcut(event, Keys.MoveLeft, Keys.MoveLeft2) {
  1149  			l.columnOffset--
  1150  			l.updateOffset()
  1151  		} else if HitShortcut(event, Keys.MoveRight, Keys.MoveRight2) {
  1152  			l.columnOffset++
  1153  			l.updateOffset()
  1154  		} else if HitShortcut(event, Keys.MovePreviousPage) {
  1155  			l.transform(TransformPreviousPage)
  1156  		} else if HitShortcut(event, Keys.MoveNextPage) {
  1157  			l.transform(TransformNextPage)
  1158  		}
  1159  
  1160  		if l.currentItem != previousItem && l.currentItem < len(l.items) && l.changed != nil {
  1161  			item := l.items[l.currentItem]
  1162  			l.Unlock()
  1163  			l.changed(l.currentItem, item)
  1164  		} else {
  1165  			l.Unlock()
  1166  		}
  1167  	})
  1168  }
  1169  
  1170  // indexAtY returns the index of the list item found at the given Y position
  1171  // or a negative value if there is no such list item.
  1172  func (l *List) indexAtY(y int) int {
  1173  	_, rectY, _, height := l.GetInnerRect()
  1174  	if y < rectY || y >= rectY+height {
  1175  		return -1
  1176  	}
  1177  
  1178  	index := y - rectY
  1179  	if l.showSecondaryText {
  1180  		index /= 2
  1181  	}
  1182  	index += l.itemOffset
  1183  
  1184  	if index >= len(l.items) {
  1185  		return -1
  1186  	}
  1187  	return index
  1188  }
  1189  
  1190  // indexAtPoint returns the index of the list item found at the given position
  1191  // or a negative value if there is no such list item.
  1192  func (l *List) indexAtPoint(x, y int) int {
  1193  	rectX, rectY, width, height := l.GetInnerRect()
  1194  	if x < rectX || x >= rectX+width || y < rectY || y >= rectY+height {
  1195  		return -1
  1196  	}
  1197  
  1198  	index := y - rectY
  1199  	if l.showSecondaryText {
  1200  		index /= 2
  1201  	}
  1202  	index += l.itemOffset
  1203  
  1204  	if index >= len(l.items) {
  1205  		return -1
  1206  	}
  1207  	return index
  1208  }
  1209  
  1210  // MouseHandler returns the mouse handler for this primitive.
  1211  func (l *List) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
  1212  	return l.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
  1213  		l.Lock()
  1214  
  1215  		// Pass events to context menu.
  1216  		if l.ContextMenuVisible() && l.ContextMenuList().InRect(event.Position()) {
  1217  			defer l.ContextMenuList().MouseHandler()(action, event, setFocus)
  1218  			consumed = true
  1219  			l.Unlock()
  1220  			return
  1221  		}
  1222  
  1223  		if !l.InRect(event.Position()) {
  1224  			l.Unlock()
  1225  			return false, nil
  1226  		}
  1227  
  1228  		// Process mouse event.
  1229  		switch action {
  1230  		case MouseLeftClick:
  1231  			if l.ContextMenuVisible() {
  1232  				defer l.ContextMenu.hide(setFocus)
  1233  				consumed = true
  1234  				l.Unlock()
  1235  				return
  1236  			}
  1237  
  1238  			l.Unlock()
  1239  			setFocus(l)
  1240  			l.Lock()
  1241  
  1242  			index := l.indexAtPoint(event.Position())
  1243  			if index != -1 {
  1244  				item := l.items[index]
  1245  				if !item.disabled {
  1246  					l.currentItem = index
  1247  					if item.selected != nil {
  1248  						l.Unlock()
  1249  						item.selected()
  1250  						l.Lock()
  1251  					}
  1252  					if l.selected != nil {
  1253  						l.Unlock()
  1254  						l.selected(index, item)
  1255  						l.Lock()
  1256  					}
  1257  					if index != l.currentItem && l.changed != nil {
  1258  						l.Unlock()
  1259  						l.changed(index, item)
  1260  						l.Lock()
  1261  					}
  1262  				}
  1263  			}
  1264  			consumed = true
  1265  		case MouseMiddleClick:
  1266  			if l.ContextMenu.open {
  1267  				defer l.ContextMenu.hide(setFocus)
  1268  				consumed = true
  1269  				l.Unlock()
  1270  				return
  1271  			}
  1272  		case MouseRightDown:
  1273  			if len(l.ContextMenuList().items) == 0 {
  1274  				l.Unlock()
  1275  				return
  1276  			}
  1277  
  1278  			x, y := event.Position()
  1279  
  1280  			index := l.indexAtPoint(event.Position())
  1281  			if index != -1 {
  1282  				item := l.items[index]
  1283  				if !item.disabled {
  1284  					l.currentItem = index
  1285  					if index != l.currentItem && l.changed != nil {
  1286  						l.Unlock()
  1287  						l.changed(index, item)
  1288  						l.Lock()
  1289  					}
  1290  				}
  1291  			}
  1292  
  1293  			defer l.ContextMenu.show(l.currentItem, x, y, setFocus)
  1294  			l.ContextMenu.drag = true
  1295  			consumed = true
  1296  		case MouseMove:
  1297  			if l.hover {
  1298  				_, y := event.Position()
  1299  				index := l.indexAtY(y)
  1300  				if index >= 0 {
  1301  					item := l.items[index]
  1302  					if !item.disabled {
  1303  						l.currentItem = index
  1304  					}
  1305  				}
  1306  
  1307  				consumed = true
  1308  			}
  1309  		case MouseScrollUp:
  1310  			if l.itemOffset > 0 {
  1311  				l.itemOffset--
  1312  			}
  1313  			consumed = true
  1314  		case MouseScrollDown:
  1315  			lines := len(l.items) - l.itemOffset
  1316  			if l.showSecondaryText {
  1317  				lines *= 2
  1318  			}
  1319  			if _, _, _, height := l.GetInnerRect(); lines > height {
  1320  				l.itemOffset++
  1321  			}
  1322  			consumed = true
  1323  		}
  1324  
  1325  		l.Unlock()
  1326  		return
  1327  	})
  1328  }
  1329  

View as plain text