...

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

Documentation: gitlab.com/tslocum/cview

     1  package cview
     2  
     3  import (
     4  	"strings"
     5  	"sync"
     6  
     7  	"github.com/gdamore/tcell/v2"
     8  	"github.com/mattn/go-runewidth"
     9  )
    10  
    11  // DropDownOption is one option that can be selected in a drop-down primitive.
    12  type DropDownOption struct {
    13  	text      string                                  // The text to be displayed in the drop-down.
    14  	selected  func(index int, option *DropDownOption) // The (optional) callback for when this option was selected.
    15  	reference interface{}                             // An optional reference object.
    16  
    17  	sync.RWMutex
    18  }
    19  
    20  // NewDropDownOption returns a new option for a dropdown.
    21  func NewDropDownOption(text string) *DropDownOption {
    22  	return &DropDownOption{text: text}
    23  }
    24  
    25  // GetText returns the text of this dropdown option.
    26  func (d *DropDownOption) GetText() string {
    27  	d.RLock()
    28  	defer d.RUnlock()
    29  
    30  	return d.text
    31  }
    32  
    33  // SetText returns the text of this dropdown option.
    34  func (d *DropDownOption) SetText(text string) {
    35  	d.text = text
    36  }
    37  
    38  // SetSelectedFunc sets the handler to be called when this option is selected.
    39  func (d *DropDownOption) SetSelectedFunc(handler func(index int, option *DropDownOption)) {
    40  	d.selected = handler
    41  }
    42  
    43  // GetReference returns the reference object of this dropdown option.
    44  func (d *DropDownOption) GetReference() interface{} {
    45  	d.RLock()
    46  	defer d.RUnlock()
    47  
    48  	return d.reference
    49  }
    50  
    51  // SetReference allows you to store a reference of any type in this option.
    52  func (d *DropDownOption) SetReference(reference interface{}) {
    53  	d.reference = reference
    54  }
    55  
    56  // DropDown implements a selection widget whose options become visible in a
    57  // drop-down list when activated.
    58  type DropDown struct {
    59  	*Box
    60  
    61  	// The options from which the user can choose.
    62  	options []*DropDownOption
    63  
    64  	// Strings to be placed before and after each drop-down option.
    65  	optionPrefix, optionSuffix string
    66  
    67  	// The index of the currently selected option. Negative if no option is
    68  	// currently selected.
    69  	currentOption int
    70  
    71  	// Strings to be placed before and after the current option.
    72  	currentOptionPrefix, currentOptionSuffix string
    73  
    74  	// The text to be displayed when no option has yet been selected.
    75  	noSelection string
    76  
    77  	// Set to true if the options are visible and selectable.
    78  	open bool
    79  
    80  	// The runes typed so far to directly access one of the list items.
    81  	prefix string
    82  
    83  	// The list element for the options.
    84  	list *List
    85  
    86  	// The text to be displayed before the input area.
    87  	label string
    88  
    89  	// The label color.
    90  	labelColor tcell.Color
    91  
    92  	// The label color when focused.
    93  	labelColorFocused tcell.Color
    94  
    95  	// The background color of the input area.
    96  	fieldBackgroundColor tcell.Color
    97  
    98  	// The background color of the input area when focused.
    99  	fieldBackgroundColorFocused tcell.Color
   100  
   101  	// The text color of the input area.
   102  	fieldTextColor tcell.Color
   103  
   104  	// The text color of the input area when focused.
   105  	fieldTextColorFocused tcell.Color
   106  
   107  	// The color for prefixes.
   108  	prefixTextColor tcell.Color
   109  
   110  	// The screen width of the label area. A value of 0 means use the width of
   111  	// the label text.
   112  	labelWidth int
   113  
   114  	// The screen width of the input area. A value of 0 means extend as much as
   115  	// possible.
   116  	fieldWidth int
   117  
   118  	// An optional function which is called when the user indicated that they
   119  	// are done selecting options. The key which was pressed is provided (tab,
   120  	// shift-tab, or escape).
   121  	done func(tcell.Key)
   122  
   123  	// A callback function set by the Form class and called when the user leaves
   124  	// this form item.
   125  	finished func(tcell.Key)
   126  
   127  	// A callback function which is called when the user changes the drop-down's
   128  	// selection.
   129  	selected func(index int, option *DropDownOption)
   130  
   131  	// Set to true when mouse dragging is in progress.
   132  	dragging bool
   133  
   134  	// The chars to show when the option's text gets shortened.
   135  	abbreviationChars string
   136  
   137  	// The symbol to draw at the end of the field.
   138  	dropDownSymbol rune
   139  
   140  	sync.RWMutex
   141  }
   142  
   143  // NewDropDown returns a new drop-down.
   144  func NewDropDown() *DropDown {
   145  	list := NewList()
   146  	list.ShowSecondaryText(false)
   147  	list.SetMainTextColor(Styles.PrimitiveBackgroundColor)
   148  	list.SetSelectedTextColor(Styles.PrimitiveBackgroundColor)
   149  	list.SetSelectedBackgroundColor(Styles.PrimaryTextColor)
   150  	list.SetHighlightFullLine(true)
   151  	list.SetBackgroundColor(Styles.MoreContrastBackgroundColor)
   152  
   153  	d := &DropDown{
   154  		Box:                         NewBox(),
   155  		currentOption:               -1,
   156  		list:                        list,
   157  		labelColor:                  Styles.SecondaryTextColor,
   158  		fieldBackgroundColor:        Styles.ContrastBackgroundColor,
   159  		fieldTextColor:              Styles.PrimaryTextColor,
   160  		prefixTextColor:             Styles.ContrastSecondaryTextColor,
   161  		dropDownSymbol:              Styles.DropDownSymbol,
   162  		abbreviationChars:           Styles.DropDownAbbreviationChars,
   163  		labelColorFocused:           ColorUnset,
   164  		fieldBackgroundColorFocused: ColorUnset,
   165  		fieldTextColorFocused:       ColorUnset,
   166  	}
   167  
   168  	d.focus = d
   169  
   170  	return d
   171  }
   172  
   173  // SetDropDownSymbolRune sets the rune to be drawn at the end of the dropdown field
   174  // to indicate that this field is a dropdown.
   175  func (d *DropDown) SetDropDownSymbolRune(symbol rune) {
   176  	d.Lock()
   177  	defer d.Unlock()
   178  	d.dropDownSymbol = symbol
   179  }
   180  
   181  // SetCurrentOption sets the index of the currently selected option. This may
   182  // be a negative value to indicate that no option is currently selected. Calling
   183  // this function will also trigger the "selected" callback (if there is one).
   184  func (d *DropDown) SetCurrentOption(index int) {
   185  	d.Lock()
   186  	defer d.Unlock()
   187  
   188  	if index >= 0 && index < len(d.options) {
   189  		d.currentOption = index
   190  		d.list.SetCurrentItem(index)
   191  		if d.selected != nil {
   192  			d.Unlock()
   193  			d.selected(index, d.options[index])
   194  			d.Lock()
   195  		}
   196  		if d.options[index].selected != nil {
   197  			d.Unlock()
   198  			d.options[index].selected(index, d.options[index])
   199  			d.Lock()
   200  		}
   201  	} else {
   202  		d.currentOption = -1
   203  		d.list.SetCurrentItem(0) // Set to 0 because -1 means "last item".
   204  		if d.selected != nil {
   205  			d.Unlock()
   206  			d.selected(-1, nil)
   207  			d.Lock()
   208  		}
   209  	}
   210  }
   211  
   212  // GetCurrentOption returns the index of the currently selected option as well
   213  // as the option itself. If no option was selected, -1 and nil is returned.
   214  func (d *DropDown) GetCurrentOption() (int, *DropDownOption) {
   215  	d.RLock()
   216  	defer d.RUnlock()
   217  
   218  	var option *DropDownOption
   219  	if d.currentOption >= 0 && d.currentOption < len(d.options) {
   220  		option = d.options[d.currentOption]
   221  	}
   222  	return d.currentOption, option
   223  }
   224  
   225  // SetTextOptions sets the text to be placed before and after each drop-down
   226  // option (prefix/suffix), the text placed before and after the currently
   227  // selected option (currentPrefix/currentSuffix) as well as the text to be
   228  // displayed when no option is currently selected. Per default, all of these
   229  // strings are empty.
   230  func (d *DropDown) SetTextOptions(prefix, suffix, currentPrefix, currentSuffix, noSelection string) {
   231  	d.Lock()
   232  	defer d.Unlock()
   233  
   234  	d.currentOptionPrefix = currentPrefix
   235  	d.currentOptionSuffix = currentSuffix
   236  	d.noSelection = noSelection
   237  	d.optionPrefix = prefix
   238  	d.optionSuffix = suffix
   239  	for index := 0; index < d.list.GetItemCount(); index++ {
   240  		d.list.SetItemText(index, prefix+d.options[index].text+suffix, "")
   241  	}
   242  }
   243  
   244  // SetLabel sets the text to be displayed before the input area.
   245  func (d *DropDown) SetLabel(label string) {
   246  	d.Lock()
   247  	defer d.Unlock()
   248  
   249  	d.label = label
   250  }
   251  
   252  // GetLabel returns the text to be displayed before the input area.
   253  func (d *DropDown) GetLabel() string {
   254  	d.RLock()
   255  	defer d.RUnlock()
   256  
   257  	return d.label
   258  }
   259  
   260  // SetLabelWidth sets the screen width of the label. A value of 0 will cause the
   261  // primitive to use the width of the label string.
   262  func (d *DropDown) SetLabelWidth(width int) {
   263  	d.Lock()
   264  	defer d.Unlock()
   265  
   266  	d.labelWidth = width
   267  }
   268  
   269  // SetLabelColor sets the color of the label.
   270  func (d *DropDown) SetLabelColor(color tcell.Color) {
   271  	d.Lock()
   272  	defer d.Unlock()
   273  
   274  	d.labelColor = color
   275  }
   276  
   277  // SetLabelColorFocused sets the color of the label when focused.
   278  func (d *DropDown) SetLabelColorFocused(color tcell.Color) {
   279  	d.Lock()
   280  	defer d.Unlock()
   281  
   282  	d.labelColorFocused = color
   283  }
   284  
   285  // SetFieldBackgroundColor sets the background color of the options area.
   286  func (d *DropDown) SetFieldBackgroundColor(color tcell.Color) {
   287  	d.Lock()
   288  	defer d.Unlock()
   289  
   290  	d.fieldBackgroundColor = color
   291  }
   292  
   293  // SetFieldBackgroundColorFocused sets the background color of the options area when focused.
   294  func (d *DropDown) SetFieldBackgroundColorFocused(color tcell.Color) {
   295  	d.Lock()
   296  	defer d.Unlock()
   297  
   298  	d.fieldBackgroundColorFocused = color
   299  }
   300  
   301  // SetFieldTextColor sets the text color of the options area.
   302  func (d *DropDown) SetFieldTextColor(color tcell.Color) {
   303  	d.Lock()
   304  	defer d.Unlock()
   305  
   306  	d.fieldTextColor = color
   307  }
   308  
   309  // SetFieldTextColorFocused sets the text color of the options area when focused.
   310  func (d *DropDown) SetFieldTextColorFocused(color tcell.Color) {
   311  	d.Lock()
   312  	defer d.Unlock()
   313  
   314  	d.fieldTextColorFocused = color
   315  }
   316  
   317  // SetDropDownTextColor sets text color of the drop-down list.
   318  func (d *DropDown) SetDropDownTextColor(color tcell.Color) {
   319  	d.Lock()
   320  	defer d.Unlock()
   321  
   322  	d.list.SetMainTextColor(color)
   323  }
   324  
   325  // SetDropDownBackgroundColor sets the background color of the drop-down list.
   326  func (d *DropDown) SetDropDownBackgroundColor(color tcell.Color) {
   327  	d.Lock()
   328  	defer d.Unlock()
   329  
   330  	d.list.SetBackgroundColor(color)
   331  }
   332  
   333  // SetDropDownSelectedTextColor sets the text color of the selected option in
   334  // the drop-down list.
   335  func (d *DropDown) SetDropDownSelectedTextColor(color tcell.Color) {
   336  	d.Lock()
   337  	defer d.Unlock()
   338  
   339  	d.list.SetSelectedTextColor(color)
   340  }
   341  
   342  // SetDropDownSelectedBackgroundColor sets the background color of the selected
   343  // option in the drop-down list.
   344  func (d *DropDown) SetDropDownSelectedBackgroundColor(color tcell.Color) {
   345  	d.Lock()
   346  	defer d.Unlock()
   347  
   348  	d.list.SetSelectedBackgroundColor(color)
   349  }
   350  
   351  // SetPrefixTextColor sets the color of the prefix string. The prefix string is
   352  // shown when the user starts typing text, which directly selects the first
   353  // option that starts with the typed string.
   354  func (d *DropDown) SetPrefixTextColor(color tcell.Color) {
   355  	d.Lock()
   356  	defer d.Unlock()
   357  
   358  	d.prefixTextColor = color
   359  }
   360  
   361  // SetFieldWidth sets the screen width of the options area. A value of 0 means
   362  // extend to as long as the longest option text.
   363  func (d *DropDown) SetFieldWidth(width int) {
   364  	d.Lock()
   365  	defer d.Unlock()
   366  
   367  	d.fieldWidth = width
   368  }
   369  
   370  // GetFieldHeight returns the height of the field.
   371  func (d *DropDown) GetFieldHeight() int {
   372  	return 1
   373  }
   374  
   375  // GetFieldWidth returns this primitive's field screen width.
   376  func (d *DropDown) GetFieldWidth() int {
   377  	d.RLock()
   378  	defer d.RUnlock()
   379  	return d.getFieldWidth()
   380  }
   381  
   382  func (d *DropDown) getFieldWidth() int {
   383  	if d.fieldWidth > 0 {
   384  		return d.fieldWidth
   385  	}
   386  	fieldWidth := 0
   387  	for _, option := range d.options {
   388  		width := TaggedStringWidth(option.text)
   389  		if width > fieldWidth {
   390  			fieldWidth = width
   391  		}
   392  	}
   393  	fieldWidth += len(d.optionPrefix) + len(d.optionSuffix)
   394  	fieldWidth += len(d.currentOptionPrefix) + len(d.currentOptionSuffix)
   395  	fieldWidth += 3 // space + dropDownSymbol + space
   396  	return fieldWidth
   397  }
   398  
   399  // AddOptionsSimple adds new selectable options to this drop-down.
   400  func (d *DropDown) AddOptionsSimple(options ...string) {
   401  	optionsToAdd := make([]*DropDownOption, len(options))
   402  	for i, option := range options {
   403  		optionsToAdd[i] = NewDropDownOption(option)
   404  	}
   405  	d.AddOptions(optionsToAdd...)
   406  }
   407  
   408  // AddOptions adds new selectable options to this drop-down.
   409  func (d *DropDown) AddOptions(options ...*DropDownOption) {
   410  	d.Lock()
   411  	defer d.Unlock()
   412  	d.addOptions(options...)
   413  }
   414  
   415  func (d *DropDown) addOptions(options ...*DropDownOption) {
   416  	d.options = append(d.options, options...)
   417  	for _, option := range options {
   418  		d.list.AddItem(NewListItem(d.optionPrefix + option.text + d.optionSuffix))
   419  	}
   420  }
   421  
   422  // SetOptionsSimple replaces all current options with the ones provided and installs
   423  // one callback function which is called when one of the options is selected.
   424  // It will be called with the option's index and the option itself
   425  // The "selected" parameter may be nil.
   426  func (d *DropDown) SetOptionsSimple(selected func(index int, option *DropDownOption), options ...string) {
   427  	optionsToSet := make([]*DropDownOption, len(options))
   428  	for i, option := range options {
   429  		optionsToSet[i] = NewDropDownOption(option)
   430  	}
   431  	d.SetOptions(selected, optionsToSet...)
   432  }
   433  
   434  // SetOptions replaces all current options with the ones provided and installs
   435  // one callback function which is called when one of the options is selected.
   436  // It will be called with the option's index and the option itself.
   437  // The "selected" parameter may be nil.
   438  func (d *DropDown) SetOptions(selected func(index int, option *DropDownOption), options ...*DropDownOption) {
   439  	d.Lock()
   440  	defer d.Unlock()
   441  
   442  	d.list.Clear()
   443  	d.options = nil
   444  	d.addOptions(options...)
   445  	d.selected = selected
   446  }
   447  
   448  // SetChangedFunc sets a handler which is called when the user changes the
   449  // focused drop-down option. The handler is provided with the selected option's
   450  // index and the option itself. If "no option" was selected, these values are
   451  // -1 and nil.
   452  func (d *DropDown) SetChangedFunc(handler func(index int, option *DropDownOption)) {
   453  	d.list.SetChangedFunc(func(index int, item *ListItem) {
   454  		handler(index, d.options[index])
   455  	})
   456  }
   457  
   458  // SetSelectedFunc sets a handler which is called when the user selects a
   459  // drop-down's option. This handler will be called in addition and prior to
   460  // an option's optional individual handler. The handler is provided with the
   461  // selected option's index and the option itself. If "no option" was selected, these values
   462  // are -1 and nil.
   463  func (d *DropDown) SetSelectedFunc(handler func(index int, option *DropDownOption)) {
   464  	d.Lock()
   465  	defer d.Unlock()
   466  
   467  	d.selected = handler
   468  }
   469  
   470  // SetDoneFunc sets a handler which is called when the user is done selecting
   471  // options. The callback function is provided with the key that was pressed,
   472  // which is one of the following:
   473  //
   474  //   - KeyEscape: Abort selection.
   475  //   - KeyTab: Move to the next field.
   476  //   - KeyBacktab: Move to the previous field.
   477  func (d *DropDown) SetDoneFunc(handler func(key tcell.Key)) {
   478  	d.Lock()
   479  	defer d.Unlock()
   480  
   481  	d.done = handler
   482  }
   483  
   484  // SetFinishedFunc sets a callback invoked when the user leaves this form item.
   485  func (d *DropDown) SetFinishedFunc(handler func(key tcell.Key)) {
   486  	d.Lock()
   487  	defer d.Unlock()
   488  
   489  	d.finished = handler
   490  }
   491  
   492  // Draw draws this primitive onto the screen.
   493  func (d *DropDown) Draw(screen tcell.Screen) {
   494  	d.Box.Draw(screen)
   495  	hasFocus := d.GetFocusable().HasFocus()
   496  
   497  	d.Lock()
   498  	defer d.Unlock()
   499  
   500  	// Select colors
   501  	labelColor := d.labelColor
   502  	fieldBackgroundColor := d.fieldBackgroundColor
   503  	fieldTextColor := d.fieldTextColor
   504  	if hasFocus {
   505  		if d.labelColorFocused != ColorUnset {
   506  			labelColor = d.labelColorFocused
   507  		}
   508  		if d.fieldBackgroundColorFocused != ColorUnset {
   509  			fieldBackgroundColor = d.fieldBackgroundColorFocused
   510  		}
   511  		if d.fieldTextColorFocused != ColorUnset {
   512  			fieldTextColor = d.fieldTextColorFocused
   513  		}
   514  	}
   515  
   516  	// Prepare.
   517  	x, y, width, height := d.GetInnerRect()
   518  	rightLimit := x + width
   519  	if height < 1 || rightLimit <= x {
   520  		return
   521  	}
   522  
   523  	// Draw label.
   524  	if d.labelWidth > 0 {
   525  		labelWidth := d.labelWidth
   526  		if labelWidth > rightLimit-x {
   527  			labelWidth = rightLimit - x
   528  		}
   529  		Print(screen, []byte(d.label), x, y, labelWidth, AlignLeft, labelColor)
   530  		x += labelWidth
   531  	} else {
   532  		_, drawnWidth := Print(screen, []byte(d.label), x, y, rightLimit-x, AlignLeft, labelColor)
   533  		x += drawnWidth
   534  	}
   535  
   536  	// What's the longest option text?
   537  	maxWidth := 0
   538  	optionWrapWidth := TaggedStringWidth(d.optionPrefix + d.optionSuffix)
   539  	for _, option := range d.options {
   540  		strWidth := TaggedStringWidth(option.text) + optionWrapWidth
   541  		if strWidth > maxWidth {
   542  			maxWidth = strWidth
   543  		}
   544  	}
   545  
   546  	// Draw selection area.
   547  	fieldWidth := d.getFieldWidth()
   548  	if fieldWidth == 0 {
   549  		fieldWidth = maxWidth
   550  		if d.currentOption < 0 {
   551  			noSelectionWidth := TaggedStringWidth(d.noSelection)
   552  			if noSelectionWidth > fieldWidth {
   553  				fieldWidth = noSelectionWidth
   554  			}
   555  		} else if d.currentOption < len(d.options) {
   556  			currentOptionWidth := TaggedStringWidth(d.currentOptionPrefix + d.options[d.currentOption].text + d.currentOptionSuffix)
   557  			if currentOptionWidth > fieldWidth {
   558  				fieldWidth = currentOptionWidth
   559  			}
   560  		}
   561  	}
   562  	if rightLimit-x < fieldWidth {
   563  		fieldWidth = rightLimit - x
   564  	}
   565  	fieldStyle := tcell.StyleDefault.Background(fieldBackgroundColor)
   566  	for index := 0; index < fieldWidth; index++ {
   567  		screen.SetContent(x+index, y, ' ', nil, fieldStyle)
   568  	}
   569  
   570  	// Draw selected text.
   571  	if d.open && len(d.prefix) > 0 {
   572  		// Show the prefix.
   573  		currentOptionPrefixWidth := TaggedStringWidth(d.currentOptionPrefix)
   574  		prefixWidth := runewidth.StringWidth(d.prefix)
   575  		listItemText := d.options[d.list.GetCurrentItemIndex()].text
   576  		Print(screen, []byte(d.currentOptionPrefix), x, y, fieldWidth, AlignLeft, fieldTextColor)
   577  		Print(screen, []byte(d.prefix), x+currentOptionPrefixWidth, y, fieldWidth-currentOptionPrefixWidth, AlignLeft, d.prefixTextColor)
   578  		if len(d.prefix) < len(listItemText) {
   579  			Print(screen, []byte(listItemText[len(d.prefix):]+d.currentOptionSuffix), x+prefixWidth+currentOptionPrefixWidth, y, fieldWidth-prefixWidth-currentOptionPrefixWidth, AlignLeft, fieldTextColor)
   580  		}
   581  	} else {
   582  		color := fieldTextColor
   583  		text := d.noSelection
   584  		if d.currentOption >= 0 && d.currentOption < len(d.options) {
   585  			text = d.currentOptionPrefix + d.options[d.currentOption].text + d.currentOptionSuffix
   586  		}
   587  		// Abbreviate text when not fitting
   588  		if fieldWidth > len(d.abbreviationChars)+3 && len(text) > fieldWidth {
   589  			text = text[0:fieldWidth-3-len(d.abbreviationChars)] + d.abbreviationChars
   590  		}
   591  
   592  		// Just show the current selection.
   593  		Print(screen, []byte(text), x, y, fieldWidth, AlignLeft, color)
   594  	}
   595  
   596  	// Draw drop-down symbol
   597  	screen.SetContent(x+fieldWidth-2, y, d.dropDownSymbol, nil, new(tcell.Style).Foreground(fieldTextColor).Background(fieldBackgroundColor))
   598  
   599  	// Draw options list.
   600  	if hasFocus && d.open {
   601  		// We prefer to drop-down but if there is no space, maybe drop up?
   602  		lx := x
   603  		ly := y + 1
   604  		lheight := len(d.options)
   605  		_, sheight := screen.Size()
   606  		if ly+lheight >= sheight && ly-2 > lheight-ly {
   607  			ly = y - lheight
   608  			if ly < 0 {
   609  				ly = 0
   610  			}
   611  		}
   612  		if ly+lheight >= sheight {
   613  			lheight = sheight - ly
   614  		}
   615  		lwidth := maxWidth
   616  		if d.list.scrollBarVisibility == ScrollBarAlways || (d.list.scrollBarVisibility == ScrollBarAuto && len(d.options) > lheight) {
   617  			lwidth++ // Add space for scroll bar
   618  		}
   619  		if lwidth < fieldWidth {
   620  			lwidth = fieldWidth
   621  		}
   622  		d.list.SetRect(lx, ly, lwidth, lheight)
   623  		d.list.Draw(screen)
   624  	}
   625  }
   626  
   627  // InputHandler returns the handler for this primitive.
   628  func (d *DropDown) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
   629  	return d.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
   630  		// Process key event.
   631  		switch key := event.Key(); key {
   632  		case tcell.KeyEnter, tcell.KeyRune, tcell.KeyDown:
   633  			d.Lock()
   634  			defer d.Unlock()
   635  
   636  			d.prefix = ""
   637  
   638  			// If the first key was a letter already, it becomes part of the prefix.
   639  			if r := event.Rune(); key == tcell.KeyRune && r != ' ' {
   640  				d.prefix += string(r)
   641  				d.evalPrefix()
   642  			}
   643  
   644  			d.openList(setFocus)
   645  		case tcell.KeyEscape, tcell.KeyTab, tcell.KeyBacktab:
   646  			if d.done != nil {
   647  				d.done(key)
   648  			}
   649  			if d.finished != nil {
   650  				d.finished(key)
   651  			}
   652  		}
   653  	})
   654  }
   655  
   656  // evalPrefix selects an item in the drop-down list based on the current prefix.
   657  func (d *DropDown) evalPrefix() {
   658  	if len(d.prefix) > 0 {
   659  		for index, option := range d.options {
   660  			if strings.HasPrefix(strings.ToLower(option.text), d.prefix) {
   661  				d.list.SetCurrentItem(index)
   662  				return
   663  			}
   664  		}
   665  
   666  		// Prefix does not match any item. Remove last rune.
   667  		r := []rune(d.prefix)
   668  		d.prefix = string(r[:len(r)-1])
   669  	}
   670  }
   671  
   672  // openList hands control over to the embedded List primitive.
   673  func (d *DropDown) openList(setFocus func(Primitive)) {
   674  	d.open = true
   675  	optionBefore := d.currentOption
   676  
   677  	d.list.SetSelectedFunc(func(index int, item *ListItem) {
   678  		if d.dragging {
   679  			return // If we're dragging the mouse, we don't want to trigger any events.
   680  		}
   681  
   682  		// An option was selected. Close the list again.
   683  		d.currentOption = index
   684  		d.closeList(setFocus)
   685  
   686  		// Trigger "selected" event.
   687  		if d.selected != nil {
   688  			d.selected(d.currentOption, d.options[d.currentOption])
   689  		}
   690  		if d.options[d.currentOption].selected != nil {
   691  			d.options[d.currentOption].selected(d.currentOption, d.options[d.currentOption])
   692  		}
   693  	})
   694  	d.list.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
   695  		if event.Key() == tcell.KeyRune {
   696  			d.prefix += string(event.Rune())
   697  			d.evalPrefix()
   698  		} else if event.Key() == tcell.KeyBackspace || event.Key() == tcell.KeyBackspace2 {
   699  			if len(d.prefix) > 0 {
   700  				r := []rune(d.prefix)
   701  				d.prefix = string(r[:len(r)-1])
   702  			}
   703  			d.evalPrefix()
   704  		} else if event.Key() == tcell.KeyEscape {
   705  			d.currentOption = optionBefore
   706  			d.list.SetCurrentItem(d.currentOption)
   707  			d.closeList(setFocus)
   708  			if d.selected != nil {
   709  				if d.currentOption > -1 {
   710  					d.selected(d.currentOption, d.options[d.currentOption])
   711  				}
   712  			}
   713  		} else {
   714  			d.prefix = ""
   715  		}
   716  
   717  		return event
   718  	})
   719  
   720  	setFocus(d.list)
   721  }
   722  
   723  // closeList closes the embedded List element by hiding it and removing focus
   724  // from it.
   725  func (d *DropDown) closeList(setFocus func(Primitive)) {
   726  	d.open = false
   727  	if d.list.HasFocus() {
   728  		setFocus(d)
   729  	}
   730  }
   731  
   732  // Focus is called by the application when the primitive receives focus.
   733  func (d *DropDown) Focus(delegate func(p Primitive)) {
   734  	d.Box.Focus(delegate)
   735  	if d.open {
   736  		delegate(d.list)
   737  	}
   738  }
   739  
   740  // HasFocus returns whether or not this primitive has focus.
   741  func (d *DropDown) HasFocus() bool {
   742  	d.RLock()
   743  	defer d.RUnlock()
   744  
   745  	if d.open {
   746  		return d.list.HasFocus()
   747  	}
   748  	return d.hasFocus
   749  }
   750  
   751  // MouseHandler returns the mouse handler for this primitive.
   752  func (d *DropDown) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
   753  	return d.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
   754  		// Was the mouse event in the drop-down box itself (or on its label)?
   755  		x, y := event.Position()
   756  		_, rectY, _, _ := d.GetInnerRect()
   757  		inRect := y == rectY
   758  		if !d.open && !inRect {
   759  			return d.InRect(x, y), nil // No, and it's not expanded either. Ignore.
   760  		}
   761  
   762  		// Handle dragging. Clicks are implicitly handled by this logic.
   763  		switch action {
   764  		case MouseLeftDown:
   765  			consumed = d.open || inRect
   766  			capture = d
   767  			if !d.open {
   768  				d.openList(setFocus)
   769  				d.dragging = true
   770  			} else if consumed, _ := d.list.MouseHandler()(MouseLeftClick, event, setFocus); !consumed {
   771  				d.closeList(setFocus) // Close drop-down if clicked outside of it.
   772  			}
   773  		case MouseMove:
   774  			if d.dragging {
   775  				// We pretend it's a left click so we can see the selection during
   776  				// dragging. Because we don't act upon it, it's not a problem.
   777  				d.list.MouseHandler()(MouseLeftClick, event, setFocus)
   778  				consumed = true
   779  				capture = d
   780  			}
   781  		case MouseLeftUp:
   782  			if d.dragging {
   783  				d.dragging = false
   784  				d.list.MouseHandler()(MouseLeftClick, event, setFocus)
   785  				consumed = true
   786  			}
   787  		}
   788  
   789  		return
   790  	})
   791  }
   792  

View as plain text