Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
251 changes: 221 additions & 30 deletions widget/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ type List struct {
// Since: 2.8
OnHighlighted func(id ListItemID) `json:"-"`

// MuliSelect enables selection of multiple items
//
// Since: 2.9
MultiSelect bool

currentHighlight ListItemID
focused bool
scroller *widget.Scroll
Expand Down Expand Up @@ -302,23 +307,29 @@ func (l *List) Highlight(id ListItemID) {
l.Refresh()
}

// Select add the item identified by the given ID to the selection.
// Select puts the item identified by the given ID into the selection,
// or adds it to the selection when the list is in MultiSelect mode.
func (l *List) Select(id ListItemID) {
if len(l.selected) > 0 && id == l.selected[0] {
return
}
length := 0
if f := l.Length; f != nil {
length = f()
oldIDs := l.selected
for _, oldID := range oldIDs {
if oldID == id {
return
}
}
length := l.length()
if id < 0 || id >= length {
return
}
old := l.selected
l.selected = []ListItemID{id}
if l.MultiSelect {
l.selected = append(l.selected, id)
} else {
l.selected = []ListItemID{id}
}
defer func() {
if f := l.OnUnselected; f != nil && len(old) > 0 {
f(old[0])
if f := l.OnUnselected; f != nil && len(oldIDs) > 0 && !l.MultiSelect {
for _, oldID := range oldIDs {
f(oldID)
}
}
if f := l.OnSelected; f != nil {
f(id)
Expand All @@ -328,14 +339,92 @@ func (l *List) Select(id ListItemID) {
l.Refresh()
}

// SelectAll selects all items in the list.
//
// Since: 2.9
func (l *List) SelectAll() {
length := l.length()
if length == 0 || length == len(l.selected) {
return
}

oldIDs := l.selected
l.selected = make([]ListItemID, length)
for i := range l.selected {
l.selected[i] = i
}
l.Refresh()

// Call OnSelected callback for each newly selected item
onSelected := l.OnSelected
if onSelected == nil {
return
}

wasSel := make(map[ListItemID]struct{}, len(oldIDs))
for _, oldID := range oldIDs {
wasSel[oldID] = struct{}{}
}
for id := ListItemID(0); id < ListItemID(length); id++ {
if _, exists := wasSel[id]; !exists {
onSelected(id)
}
}
}

// SetSelection sets the currently selected items in the list when in MultiSelect mode,
// otherwise selects the first item given, or clear the selection when empty.
//
// Since: 2.9
func (l *List) SetSelection(ids []ListItemID) {
if !l.MultiSelect && len(ids) > 0 {
ids = []ListItemID{ids[0]}
}

length := l.length()
if length == 0 {
return
}

wasSel := make(map[ListItemID]struct{}, len(l.selected))
for _, id := range l.selected {
wasSel[id] = struct{}{}
}

newIDs := make([]ListItemID, 0, len(ids))
newSel := make(map[ListItemID]struct{}, len(ids))
for _, id := range ids {
if id >= 0 && id < length {
newIDs = append(newIDs, id)
newSel[id] = struct{}{}
}
}
l.selected = newIDs
l.Refresh()

// Call OnSelected, OnUnselected callbacks for each newly (un)selected item
onSelected := l.OnSelected
onUnselected := l.OnUnselected
if onSelected == nil && onUnselected == nil {
return
}

for id := ListItemID(0); id < ListItemID(length); id++ {
_, wasSelected := wasSel[id]
_, newSelected := newSel[id]
if wasSelected && !newSelected && onUnselected != nil {
onUnselected(id)
} else if newSelected && !wasSelected && onSelected != nil {
onSelected(id)
}
}
}

// ScrollTo scrolls to the item represented by id
//
// Since: 2.1
func (l *List) ScrollTo(id ListItemID) {
length := 0
if f := l.Length; f != nil {
length = f()
}
length := l.length()
if id < 0 || id >= length {
return
}
Expand Down Expand Up @@ -387,29 +476,47 @@ func (l *List) GetScrollOffset() float32 {
return l.offsetY
}

func isModifierPressed(mod fyne.KeyModifier) bool {
d, ok := fyne.CurrentApp().Driver().(desktop.Driver)
if ok && d.CurrentKeyModifiers()&mod > 0 {
return true
}
return false
}

// TypedKey is called if a key event happens while this List is focused.
func (l *List) TypedKey(event *fyne.KeyEvent) {
oldFocus := l.currentHighlight

scrollOrSelect := func() {
if isModifierPressed(fyne.KeyModifierShift) {
l.SetSelection(rangeSelectionIds(l.currentHighlight, l.selected))
}
l.scrollTo(l.currentHighlight)
l.RefreshItem(l.currentHighlight)
}

switch event.Name {
case fyne.KeySpace:
l.Select(l.currentHighlight)
if isModifierPressed(fyne.KeyModifierShift) {
l.SetSelection(rangeSelectionIds(l.currentHighlight, l.selected))
} else {
l.Select(l.currentHighlight)
}
case fyne.KeyDown:
if f := l.Length; f != nil && l.currentHighlight >= f()-1 {
return
}
l.RefreshItem(l.currentHighlight)
l.currentHighlight++
l.scrollTo(l.currentHighlight)
l.RefreshItem(l.currentHighlight)
scrollOrSelect()
case fyne.KeyUp:
if l.currentHighlight <= 0 {
return
}
l.RefreshItem(l.currentHighlight)
l.currentHighlight--
l.scrollTo(l.currentHighlight)
l.RefreshItem(l.currentHighlight)
scrollOrSelect()
}

if oldFocus != l.currentHighlight {
Expand All @@ -426,12 +533,26 @@ func (l *List) TypedRune(_ rune) {

// Unselect removes the item identified by the given ID from the selection.
func (l *List) Unselect(id ListItemID) {
if len(l.selected) == 0 || l.selected[0] != id {
isSel := false
for _, selID := range l.selected {
if selID == id {
isSel = true
break
}
}
if !isSel {
return
}

l.selected = nil
newSel := make([]ListItemID, 0, len(l.selected)-1)
for _, selID := range l.selected {
if selID != id {
newSel = append(newSel, selID)
}
}
l.selected = newSel
l.Refresh()

if f := l.OnUnselected; f != nil {
f(id)
}
Expand All @@ -445,16 +566,23 @@ func (l *List) UnselectAll() {
return
}

selected := l.selected
oldIDs := l.selected
l.selected = nil
l.Refresh()
if f := l.OnUnselected; f != nil {
for _, id := range selected {
for _, id := range oldIDs {
f(id)
}
}
}

func (l *List) length() int {
if f := l.Length; f != nil {
return f()
}
return 0
}

// Refresh causes this List to be redrawn in its current state
func (l *List) Refresh() {
l.minSizeCache = fyne.Size{}
Expand All @@ -470,7 +598,7 @@ func (l *List) contentMinSize() fyne.Size {
if l.Length == nil {
return fyne.NewSize(0, 0)
}
items := l.Length()
items := l.length()

if len(l.itemHeights) == 0 {
return fyne.NewSize(l.itemMin.Width,
Expand Down Expand Up @@ -608,6 +736,7 @@ type listItem struct {
BaseWidget

onTapped func()
onTappedSecondary func() // used instead of tapped+modifier for multi-select
onHovered func()
background *canvas.Rectangle
child fyne.CanvasObject
Expand Down Expand Up @@ -673,6 +802,17 @@ func (li *listItem) Tapped(*fyne.PointEvent) {
}
}

func (li *listItem) TappedSecondary(*fyne.PointEvent) {
if !fyne.CurrentDevice().IsMobile() {
return
}
if f := li.onTappedSecondary; f != nil {
li.selected = true
li.Refresh()
f()
}
}

// Declare conformity with the WidgetRenderer interface.
var _ fyne.WidgetRenderer = (*listItemRenderer)(nil)

Expand Down Expand Up @@ -798,19 +938,70 @@ func (l *listLayout) setupListItem(li *listItem, id ListItemID, focus bool) {

l.list.currentHighlight = id
}
if isModifierPressed(fyne.KeyModifierSuper | fyne.KeyModifierControl) {
if li.selected {
l.list.Unselect(id)
} else {
l.list.Select(id)
}
} else if isModifierPressed(fyne.KeyModifierShift) {
l.list.SetSelection(rangeSelectionIds(id, l.list.selected))
} else {
l.list.Select(id)
}
}
if !fyne.CurrentDevice().IsMobile() {
return
}
li.onTappedSecondary = func() {
if li.selected {
l.list.Unselect(id)
} else {
l.list.SetSelection(append(l.list.selected, id))
}
}
}

func rangeSelectionIds(cur ListItemID, selected []ListItemID) []ListItemID {
var high, low ListItemID = math.MinInt, math.MaxInt
for _, id := range selected {
if id > high {
high = id
}
if id < low {
low = id
}
}

if len(selected) == 0 {
low = cur
high = cur
}

if cur < low {
r := make([]ListItemID, 0, (low-cur)+len(selected))
for id := cur; id <= low; id++ {
r = append(r, id)
}
r = append(r, selected[1:]...)
return r
}

l.list.Select(id)
r := make([]ListItemID, 0, (cur-high)+len(selected))
if len(selected) > 0 {
r = append(r, selected[:len(selected)-1]...)
}
for id := high; id <= cur; id++ {
r = append(r, id)
}
return r
}

func (l *listLayout) updateList(newOnly bool) {
th := l.list.Theme()
separatorThickness := th.Size(theme.SizeNamePadding)
width := l.list.Size().Width
length := 0
if f := l.list.Length; f != nil {
length = f()
}
length := l.list.length()
if l.list.UpdateItem == nil {
fyne.LogError("Missing UpdateCell callback required for List", nil)
}
Expand Down
Loading
Loading