check_group.go 5.9 KB

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