check_group.go 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. package widget
  2. import (
  3. "strings"
  4. "fyne.io/fyne/v2"
  5. "fyne.io/fyne/v2/canvas"
  6. "fyne.io/fyne/v2/internal/widget"
  7. )
  8. // CheckGroup widget has a list of text labels and checkbox icons next to each.
  9. // Changing the selection (any number can be selected) will trigger the changed func.
  10. //
  11. // Since: 2.1
  12. type CheckGroup struct {
  13. DisableableWidget
  14. Horizontal bool
  15. Required bool
  16. OnChanged func([]string) `json:"-"`
  17. Options []string
  18. Selected []string
  19. items []*Check
  20. }
  21. var _ fyne.Widget = (*CheckGroup)(nil)
  22. // NewCheckGroup creates a new check group widget with the set options and change handler
  23. //
  24. // Since: 2.1
  25. func NewCheckGroup(options []string, changed func([]string)) *CheckGroup {
  26. r := &CheckGroup{
  27. DisableableWidget: DisableableWidget{},
  28. Options: options,
  29. OnChanged: changed,
  30. }
  31. r.ExtendBaseWidget(r)
  32. r.update()
  33. return r
  34. }
  35. // Append adds a new option to the end of a CheckGroup widget.
  36. func (r *CheckGroup) Append(option string) {
  37. r.Options = append(r.Options, option)
  38. r.Refresh()
  39. }
  40. // CreateRenderer is a private method to Fyne which links this widget to its renderer
  41. func (r *CheckGroup) CreateRenderer() fyne.WidgetRenderer {
  42. r.ExtendBaseWidget(r)
  43. r.propertyLock.Lock()
  44. defer r.propertyLock.Unlock()
  45. r.update()
  46. objects := make([]fyne.CanvasObject, len(r.items))
  47. for i, item := range r.items {
  48. objects[i] = item
  49. }
  50. return &checkGroupRenderer{widget.NewBaseRenderer(objects), r.items, r}
  51. }
  52. // MinSize returns the size that this widget should not shrink below
  53. func (r *CheckGroup) MinSize() fyne.Size {
  54. r.ExtendBaseWidget(r)
  55. return r.BaseWidget.MinSize()
  56. }
  57. // Refresh causes this widget to be redrawn in it's current state.
  58. //
  59. // Implements: fyne.CanvasObject
  60. func (r *CheckGroup) Refresh() {
  61. r.propertyLock.Lock()
  62. r.update()
  63. r.propertyLock.Unlock()
  64. r.BaseWidget.Refresh()
  65. }
  66. // Remove removes the first occurrence of the specified option found from a CheckGroup widget.
  67. // Return true if an option was removed.
  68. //
  69. // Since: 2.3
  70. func (r *CheckGroup) Remove(option string) bool {
  71. for i, o := range r.Options {
  72. if strings.EqualFold(option, o) {
  73. r.Options = append(r.Options[:i], r.Options[i+1:]...)
  74. for j, s := range r.Selected {
  75. if strings.EqualFold(option, s) {
  76. r.Selected = append(r.Selected[:j], r.Selected[j+1:]...)
  77. break
  78. }
  79. }
  80. r.Refresh()
  81. return true
  82. }
  83. }
  84. return false
  85. }
  86. // SetSelected sets the checked options, it can be used to set a default option.
  87. func (r *CheckGroup) SetSelected(options []string) {
  88. //if r.Selected == options {
  89. // return
  90. //}
  91. r.Selected = options
  92. if r.OnChanged != nil {
  93. r.OnChanged(options)
  94. }
  95. r.Refresh()
  96. }
  97. func (r *CheckGroup) itemTapped(item *Check) {
  98. if r.Disabled() {
  99. return
  100. }
  101. contains := false
  102. for i, s := range r.Selected {
  103. if s == item.Text {
  104. contains = true
  105. if len(r.Selected) <= 1 {
  106. if r.Required {
  107. item.SetChecked(true)
  108. return
  109. }
  110. r.Selected = nil
  111. } else {
  112. r.Selected = append(r.Selected[:i], r.Selected[i+1:]...)
  113. }
  114. break
  115. }
  116. }
  117. if !contains {
  118. r.Selected = append(r.Selected, item.Text)
  119. }
  120. if r.OnChanged != nil {
  121. r.OnChanged(r.Selected)
  122. }
  123. r.Refresh()
  124. }
  125. func (r *CheckGroup) update() {
  126. r.Options = removeDuplicates(r.Options)
  127. if len(r.items) < len(r.Options) {
  128. for i := len(r.items); i < len(r.Options); i++ {
  129. var item *Check
  130. item = NewCheck(r.Options[i], func(bool) {
  131. r.itemTapped(item)
  132. })
  133. r.items = append(r.items, item)
  134. }
  135. } else if len(r.items) > len(r.Options) {
  136. r.items = r.items[:len(r.Options)]
  137. }
  138. for i, item := range r.items {
  139. contains := false
  140. for _, s := range r.Selected {
  141. if s == item.Text {
  142. contains = true
  143. break
  144. }
  145. }
  146. item.Text = r.Options[i]
  147. item.Checked = contains
  148. item.DisableableWidget.disabled = r.disabled
  149. item.Refresh()
  150. }
  151. }
  152. type checkGroupRenderer struct {
  153. widget.BaseRenderer
  154. items []*Check
  155. checks *CheckGroup
  156. }
  157. // Layout the components of the checks widget
  158. func (r *checkGroupRenderer) Layout(_ fyne.Size) {
  159. count := 1
  160. if r.items != nil && len(r.items) > 0 {
  161. count = len(r.items)
  162. }
  163. var itemHeight, itemWidth float32
  164. minSize := r.checks.MinSize()
  165. if r.checks.Horizontal {
  166. itemHeight = minSize.Height
  167. itemWidth = minSize.Width / float32(count)
  168. } else {
  169. itemHeight = minSize.Height / float32(count)
  170. itemWidth = minSize.Width
  171. }
  172. itemSize := fyne.NewSize(itemWidth, itemHeight)
  173. x, y := float32(0), float32(0)
  174. for _, item := range r.items {
  175. item.Resize(itemSize)
  176. item.Move(fyne.NewPos(x, y))
  177. if r.checks.Horizontal {
  178. x += itemWidth
  179. } else {
  180. y += itemHeight
  181. }
  182. }
  183. }
  184. // MinSize calculates the minimum size of a checks item.
  185. // This is based on the contained text, the checks icon and a standard amount of padding
  186. // between each item.
  187. func (r *checkGroupRenderer) MinSize() fyne.Size {
  188. width := float32(0)
  189. height := float32(0)
  190. for _, item := range r.items {
  191. itemMin := item.MinSize()
  192. width = fyne.Max(width, itemMin.Width)
  193. height = fyne.Max(height, itemMin.Height)
  194. }
  195. if r.checks.Horizontal {
  196. width = width * float32(len(r.items))
  197. } else {
  198. height = height * float32(len(r.items))
  199. }
  200. return fyne.NewSize(width, height)
  201. }
  202. func (r *checkGroupRenderer) Refresh() {
  203. r.updateItems()
  204. canvas.Refresh(r.checks.super())
  205. }
  206. func (r *checkGroupRenderer) updateItems() {
  207. if len(r.items) < len(r.checks.Options) {
  208. for i := len(r.items); i < len(r.checks.Options); i++ {
  209. var item *Check
  210. item = NewCheck(r.checks.Options[i], func(bool) {
  211. r.checks.itemTapped(item)
  212. })
  213. r.SetObjects(append(r.Objects(), item))
  214. r.items = append(r.items, item)
  215. }
  216. r.Layout(r.checks.Size())
  217. } else if len(r.items) > len(r.checks.Options) {
  218. total := len(r.checks.Options)
  219. r.items = r.items[:total]
  220. r.SetObjects(r.Objects()[:total])
  221. }
  222. for i, item := range r.items {
  223. contains := false
  224. for _, s := range r.checks.Selected {
  225. if s == item.Text {
  226. contains = true
  227. break
  228. }
  229. }
  230. item.Text = r.checks.Options[i]
  231. item.Checked = contains
  232. item.disabled = r.checks.disabled
  233. item.Refresh()
  234. }
  235. }