form.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. package widget
  2. import (
  3. "errors"
  4. "reflect"
  5. "fyne.io/fyne/v2"
  6. "fyne.io/fyne/v2/canvas"
  7. "fyne.io/fyne/v2/internal/cache"
  8. "fyne.io/fyne/v2/layout"
  9. "fyne.io/fyne/v2/theme"
  10. )
  11. // errFormItemInitialState defines the error if the initial validation for a FormItem result
  12. // in an error
  13. var errFormItemInitialState = errors.New("widget.FormItem initial state error")
  14. // FormItem provides the details for a row in a form
  15. type FormItem struct {
  16. Text string
  17. Widget fyne.CanvasObject
  18. // Since: 2.0
  19. HintText string
  20. validationError error
  21. invalid bool
  22. helperOutput *canvas.Text
  23. }
  24. // NewFormItem creates a new form item with the specified label text and input widget
  25. func NewFormItem(text string, widget fyne.CanvasObject) *FormItem {
  26. return &FormItem{Text: text, Widget: widget}
  27. }
  28. var _ fyne.Validatable = (*Form)(nil)
  29. // Form widget is two column grid where each row has a label and a widget (usually an input).
  30. // The last row of the grid will contain the appropriate form control buttons if any should be shown.
  31. // Setting OnSubmit will set the submit button to be visible and call back the function when tapped.
  32. // Setting OnCancel will do the same for a cancel button.
  33. // If you change OnSubmit/OnCancel after the form is created and rendered, you need to call
  34. // Refresh() to update the form with the correct buttons.
  35. // Setting OnSubmit/OnCancel to nil will remove the buttons.
  36. type Form struct {
  37. BaseWidget
  38. Items []*FormItem
  39. OnSubmit func() `json:"-"`
  40. OnCancel func() `json:"-"`
  41. SubmitText string
  42. CancelText string
  43. itemGrid *fyne.Container
  44. buttonBox *fyne.Container
  45. cancelButton *Button
  46. submitButton *Button
  47. disabled bool
  48. onValidationChanged func(error)
  49. validationError error
  50. }
  51. // Append adds a new row to the form, using the text as a label next to the specified Widget
  52. func (f *Form) Append(text string, widget fyne.CanvasObject) {
  53. item := &FormItem{Text: text, Widget: widget}
  54. f.AppendItem(item)
  55. }
  56. // AppendItem adds the specified row to the end of the Form
  57. func (f *Form) AppendItem(item *FormItem) {
  58. f.ExtendBaseWidget(f) // could be called before render
  59. f.Items = append(f.Items, item)
  60. if f.itemGrid != nil {
  61. f.itemGrid.Add(f.createLabel(item.Text))
  62. f.itemGrid.Add(f.createInput(item))
  63. f.setUpValidation(item.Widget, len(f.Items)-1)
  64. }
  65. f.Refresh()
  66. }
  67. // MinSize returns the size that this widget should not shrink below
  68. func (f *Form) MinSize() fyne.Size {
  69. f.ExtendBaseWidget(f)
  70. return f.BaseWidget.MinSize()
  71. }
  72. // Refresh updates the widget state when requested.
  73. func (f *Form) Refresh() {
  74. f.ExtendBaseWidget(f)
  75. cache.Renderer(f.super()) // we are about to make changes to renderer created content... not great!
  76. f.ensureRenderItems()
  77. f.updateButtons()
  78. f.updateLabels()
  79. f.BaseWidget.Refresh()
  80. canvas.Refresh(f.super()) // refresh ourselves for BG color - the above updates the content
  81. }
  82. // Enable enables submitting this form.
  83. //
  84. // Since: 2.1
  85. func (f *Form) Enable() {
  86. f.disabled = false
  87. f.cancelButton.Enable()
  88. f.checkValidation(nil) // as the form may be invalid
  89. }
  90. // Disable disables submitting this form.
  91. //
  92. // Since: 2.1
  93. func (f *Form) Disable() {
  94. f.disabled = true
  95. f.submitButton.Disable()
  96. f.cancelButton.Disable()
  97. }
  98. // Disabled returns whether submitting the form is disabled.
  99. // Note that, if the form fails validation, the submit button may be
  100. // disabled even if this method returns true.
  101. //
  102. // Since: 2.1
  103. func (f *Form) Disabled() bool {
  104. return f.disabled
  105. }
  106. // SetOnValidationChanged is intended for parent widgets or containers to hook into the validation.
  107. // The function might be overwritten by a parent that cares about child validation (e.g. widget.Form)
  108. func (f *Form) SetOnValidationChanged(callback func(error)) {
  109. f.onValidationChanged = callback
  110. }
  111. // Validate validates the entire form and returns the first error that is encountered.
  112. func (f *Form) Validate() error {
  113. for _, item := range f.Items {
  114. if w, ok := item.Widget.(fyne.Validatable); ok {
  115. if err := w.Validate(); err != nil {
  116. return err
  117. }
  118. }
  119. }
  120. return nil
  121. }
  122. func (f *Form) createInput(item *FormItem) fyne.CanvasObject {
  123. _, ok := item.Widget.(fyne.Validatable)
  124. if item.HintText == "" {
  125. if !ok {
  126. return item.Widget
  127. }
  128. if !f.itemWidgetHasValidator(item.Widget) { // we don't have validation
  129. return item.Widget
  130. }
  131. }
  132. text := canvas.NewText(item.HintText, theme.PlaceHolderColor())
  133. text.TextSize = theme.CaptionTextSize()
  134. item.helperOutput = text
  135. f.updateHelperText(item)
  136. textContainer := &fyne.Container{Objects: []fyne.CanvasObject{text}}
  137. return &fyne.Container{Layout: formItemLayout{}, Objects: []fyne.CanvasObject{item.Widget, textContainer}}
  138. }
  139. func (f *Form) itemWidgetHasValidator(w fyne.CanvasObject) bool {
  140. value := reflect.ValueOf(w).Elem()
  141. validatorField := value.FieldByName("Validator")
  142. if validatorField == (reflect.Value{}) {
  143. return false
  144. }
  145. validator, ok := validatorField.Interface().(fyne.StringValidator)
  146. if !ok {
  147. return false
  148. }
  149. return validator != nil
  150. }
  151. func (f *Form) createLabel(text string) *canvas.Text {
  152. return &canvas.Text{Text: text,
  153. Alignment: fyne.TextAlignTrailing,
  154. Color: theme.ForegroundColor(),
  155. TextSize: theme.TextSize(),
  156. TextStyle: fyne.TextStyle{Bold: true}}
  157. }
  158. func (f *Form) updateButtons() {
  159. if f.CancelText == "" {
  160. f.CancelText = "Cancel"
  161. }
  162. if f.SubmitText == "" {
  163. f.SubmitText = "Submit"
  164. }
  165. // set visibility on the buttons
  166. if f.OnCancel == nil {
  167. f.cancelButton.Hide()
  168. } else {
  169. f.cancelButton.SetText(f.CancelText)
  170. f.cancelButton.OnTapped = f.OnCancel
  171. f.cancelButton.Show()
  172. }
  173. if f.OnSubmit == nil {
  174. f.submitButton.Hide()
  175. } else {
  176. f.submitButton.SetText(f.SubmitText)
  177. f.submitButton.OnTapped = f.OnSubmit
  178. f.submitButton.Show()
  179. }
  180. if f.OnCancel == nil && f.OnSubmit == nil {
  181. f.buttonBox.Hide()
  182. } else {
  183. f.buttonBox.Show()
  184. }
  185. }
  186. func (f *Form) checkValidation(err error) {
  187. if err != nil {
  188. f.submitButton.Disable()
  189. return
  190. }
  191. for _, item := range f.Items {
  192. if item.invalid {
  193. f.submitButton.Disable()
  194. return
  195. }
  196. }
  197. if !f.disabled {
  198. f.submitButton.Enable()
  199. }
  200. }
  201. func (f *Form) ensureRenderItems() {
  202. done := len(f.itemGrid.Objects) / 2
  203. if done >= len(f.Items) {
  204. f.itemGrid.Objects = f.itemGrid.Objects[0 : len(f.Items)*2]
  205. return
  206. }
  207. adding := len(f.Items) - done
  208. objects := make([]fyne.CanvasObject, adding*2)
  209. off := 0
  210. for i, item := range f.Items {
  211. if i < done {
  212. continue
  213. }
  214. objects[off] = f.createLabel(item.Text)
  215. off++
  216. f.setUpValidation(item.Widget, i)
  217. objects[off] = f.createInput(item)
  218. off++
  219. }
  220. f.itemGrid.Objects = append(f.itemGrid.Objects, objects...)
  221. }
  222. func (f *Form) setUpValidation(widget fyne.CanvasObject, i int) {
  223. updateValidation := func(err error) {
  224. if err == errFormItemInitialState {
  225. return
  226. }
  227. f.Items[i].validationError = err
  228. f.Items[i].invalid = err != nil
  229. f.setValidationError(err)
  230. f.checkValidation(err)
  231. f.updateHelperText(f.Items[i])
  232. }
  233. if w, ok := widget.(fyne.Validatable); ok {
  234. f.Items[i].invalid = w.Validate() != nil
  235. if e, ok := w.(*Entry); ok {
  236. e.onFocusChanged = func(bool) {
  237. updateValidation(e.validationError)
  238. }
  239. if e.Validator != nil && f.Items[i].invalid {
  240. // set initial state error to guarantee next error (if triggers) is always different
  241. e.SetValidationError(errFormItemInitialState)
  242. }
  243. }
  244. w.SetOnValidationChanged(updateValidation)
  245. }
  246. }
  247. func (f *Form) setValidationError(err error) {
  248. if err == nil && f.validationError == nil {
  249. return
  250. }
  251. if !errors.Is(err, f.validationError) {
  252. if err == nil {
  253. for _, item := range f.Items {
  254. if item.invalid {
  255. err = item.validationError
  256. break
  257. }
  258. }
  259. }
  260. f.validationError = err
  261. if f.onValidationChanged != nil {
  262. f.onValidationChanged(err)
  263. }
  264. }
  265. }
  266. func (f *Form) updateHelperText(item *FormItem) {
  267. if item.helperOutput == nil {
  268. return // testing probably, either way not rendered yet
  269. }
  270. showHintIfError := false
  271. if e, ok := item.Widget.(*Entry); ok && (!e.dirty || e.focused) {
  272. showHintIfError = true
  273. }
  274. if item.validationError == nil || showHintIfError {
  275. item.helperOutput.Text = item.HintText
  276. item.helperOutput.Color = theme.PlaceHolderColor()
  277. } else {
  278. item.helperOutput.Text = item.validationError.Error()
  279. item.helperOutput.Color = theme.ErrorColor()
  280. }
  281. item.helperOutput.Refresh()
  282. }
  283. func (f *Form) updateLabels() {
  284. for i, item := range f.Items {
  285. l := f.itemGrid.Objects[i*2].(*canvas.Text)
  286. l.TextSize = theme.TextSize()
  287. if dis, ok := item.Widget.(fyne.Disableable); ok {
  288. if dis.Disabled() {
  289. l.Color = theme.DisabledColor()
  290. } else {
  291. l.Color = theme.ForegroundColor()
  292. }
  293. } else {
  294. l.Color = theme.ForegroundColor()
  295. }
  296. l.Text = item.Text
  297. l.Refresh()
  298. f.updateHelperText(item)
  299. }
  300. }
  301. // CreateRenderer is a private method to Fyne which links this widget to its renderer
  302. func (f *Form) CreateRenderer() fyne.WidgetRenderer {
  303. f.ExtendBaseWidget(f)
  304. f.cancelButton = &Button{Icon: theme.CancelIcon(), OnTapped: f.OnCancel}
  305. f.submitButton = &Button{Icon: theme.ConfirmIcon(), OnTapped: f.OnSubmit, Importance: HighImportance}
  306. buttons := &fyne.Container{Layout: layout.NewGridLayoutWithRows(1), Objects: []fyne.CanvasObject{f.cancelButton, f.submitButton}}
  307. f.buttonBox = &fyne.Container{Layout: layout.NewBorderLayout(nil, nil, nil, buttons), Objects: []fyne.CanvasObject{buttons}}
  308. f.validationError = errFormItemInitialState // set initial state error to guarantee next error (if triggers) is always different
  309. f.itemGrid = &fyne.Container{Layout: layout.NewFormLayout()}
  310. content := &fyne.Container{Layout: layout.NewVBoxLayout(), Objects: []fyne.CanvasObject{f.itemGrid, f.buttonBox}}
  311. renderer := NewSimpleRenderer(content)
  312. f.ensureRenderItems()
  313. f.updateButtons()
  314. f.updateLabels()
  315. f.checkValidation(nil) // will trigger a validation check for correct intial validation status
  316. return renderer
  317. }
  318. // NewForm creates a new form widget with the specified rows of form items
  319. // and (if any of them should be shown) a form controls row at the bottom
  320. func NewForm(items ...*FormItem) *Form {
  321. form := &Form{Items: items}
  322. form.ExtendBaseWidget(form)
  323. return form
  324. }
  325. type formItemLayout struct{}
  326. func (f formItemLayout) Layout(objs []fyne.CanvasObject, size fyne.Size) {
  327. itemHeight := objs[0].MinSize().Height
  328. objs[0].Resize(fyne.NewSize(size.Width, itemHeight))
  329. objs[1].Move(fyne.NewPos(theme.InnerPadding(), itemHeight+theme.InnerPadding()/2))
  330. objs[1].Resize(fyne.NewSize(size.Width, objs[1].MinSize().Width))
  331. }
  332. func (f formItemLayout) MinSize(objs []fyne.CanvasObject) fyne.Size {
  333. min0 := objs[0].MinSize()
  334. min1 := objs[1].MinSize()
  335. minWidth := fyne.Max(min0.Width, min1.Width)
  336. return fyne.NewSize(minWidth, min0.Height+min1.Height+theme.InnerPadding())
  337. }