| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390 |
- package widget
- import (
- "errors"
- "reflect"
- "fyne.io/fyne/v2"
- "fyne.io/fyne/v2/canvas"
- "fyne.io/fyne/v2/internal/cache"
- "fyne.io/fyne/v2/layout"
- "fyne.io/fyne/v2/theme"
- )
- // errFormItemInitialState defines the error if the initial validation for a FormItem result
- // in an error
- var errFormItemInitialState = errors.New("widget.FormItem initial state error")
- // FormItem provides the details for a row in a form
- type FormItem struct {
- Text string
- Widget fyne.CanvasObject
- // Since: 2.0
- HintText string
- validationError error
- invalid bool
- helperOutput *canvas.Text
- }
- // NewFormItem creates a new form item with the specified label text and input widget
- func NewFormItem(text string, widget fyne.CanvasObject) *FormItem {
- return &FormItem{Text: text, Widget: widget}
- }
- var _ fyne.Validatable = (*Form)(nil)
- // Form widget is two column grid where each row has a label and a widget (usually an input).
- // The last row of the grid will contain the appropriate form control buttons if any should be shown.
- // Setting OnSubmit will set the submit button to be visible and call back the function when tapped.
- // Setting OnCancel will do the same for a cancel button.
- // If you change OnSubmit/OnCancel after the form is created and rendered, you need to call
- // Refresh() to update the form with the correct buttons.
- // Setting OnSubmit/OnCancel to nil will remove the buttons.
- type Form struct {
- BaseWidget
- Items []*FormItem
- OnSubmit func() `json:"-"`
- OnCancel func() `json:"-"`
- SubmitText string
- CancelText string
- itemGrid *fyne.Container
- buttonBox *fyne.Container
- cancelButton *Button
- submitButton *Button
- disabled bool
- onValidationChanged func(error)
- validationError error
- }
- // Append adds a new row to the form, using the text as a label next to the specified Widget
- func (f *Form) Append(text string, widget fyne.CanvasObject) {
- item := &FormItem{Text: text, Widget: widget}
- f.AppendItem(item)
- }
- // AppendItem adds the specified row to the end of the Form
- func (f *Form) AppendItem(item *FormItem) {
- f.ExtendBaseWidget(f) // could be called before render
- f.Items = append(f.Items, item)
- if f.itemGrid != nil {
- f.itemGrid.Add(f.createLabel(item.Text))
- f.itemGrid.Add(f.createInput(item))
- f.setUpValidation(item.Widget, len(f.Items)-1)
- }
- f.Refresh()
- }
- // MinSize returns the size that this widget should not shrink below
- func (f *Form) MinSize() fyne.Size {
- f.ExtendBaseWidget(f)
- return f.BaseWidget.MinSize()
- }
- // Refresh updates the widget state when requested.
- func (f *Form) Refresh() {
- f.ExtendBaseWidget(f)
- cache.Renderer(f.super()) // we are about to make changes to renderer created content... not great!
- f.ensureRenderItems()
- f.updateButtons()
- f.updateLabels()
- f.BaseWidget.Refresh()
- canvas.Refresh(f.super()) // refresh ourselves for BG color - the above updates the content
- }
- // Enable enables submitting this form.
- //
- // Since: 2.1
- func (f *Form) Enable() {
- f.disabled = false
- f.cancelButton.Enable()
- f.checkValidation(nil) // as the form may be invalid
- }
- // Disable disables submitting this form.
- //
- // Since: 2.1
- func (f *Form) Disable() {
- f.disabled = true
- f.submitButton.Disable()
- f.cancelButton.Disable()
- }
- // Disabled returns whether submitting the form is disabled.
- // Note that, if the form fails validation, the submit button may be
- // disabled even if this method returns true.
- //
- // Since: 2.1
- func (f *Form) Disabled() bool {
- return f.disabled
- }
- // SetOnValidationChanged is intended for parent widgets or containers to hook into the validation.
- // The function might be overwritten by a parent that cares about child validation (e.g. widget.Form)
- func (f *Form) SetOnValidationChanged(callback func(error)) {
- f.onValidationChanged = callback
- }
- // Validate validates the entire form and returns the first error that is encountered.
- func (f *Form) Validate() error {
- for _, item := range f.Items {
- if w, ok := item.Widget.(fyne.Validatable); ok {
- if err := w.Validate(); err != nil {
- return err
- }
- }
- }
- return nil
- }
- func (f *Form) createInput(item *FormItem) fyne.CanvasObject {
- _, ok := item.Widget.(fyne.Validatable)
- if item.HintText == "" {
- if !ok {
- return item.Widget
- }
- if !f.itemWidgetHasValidator(item.Widget) { // we don't have validation
- return item.Widget
- }
- }
- text := canvas.NewText(item.HintText, theme.PlaceHolderColor())
- text.TextSize = theme.CaptionTextSize()
- item.helperOutput = text
- f.updateHelperText(item)
- textContainer := &fyne.Container{Objects: []fyne.CanvasObject{text}}
- return &fyne.Container{Layout: formItemLayout{}, Objects: []fyne.CanvasObject{item.Widget, textContainer}}
- }
- func (f *Form) itemWidgetHasValidator(w fyne.CanvasObject) bool {
- value := reflect.ValueOf(w).Elem()
- validatorField := value.FieldByName("Validator")
- if validatorField == (reflect.Value{}) {
- return false
- }
- validator, ok := validatorField.Interface().(fyne.StringValidator)
- if !ok {
- return false
- }
- return validator != nil
- }
- func (f *Form) createLabel(text string) *canvas.Text {
- return &canvas.Text{Text: text,
- Alignment: fyne.TextAlignTrailing,
- Color: theme.ForegroundColor(),
- TextSize: theme.TextSize(),
- TextStyle: fyne.TextStyle{Bold: true}}
- }
- func (f *Form) updateButtons() {
- if f.CancelText == "" {
- f.CancelText = "Cancel"
- }
- if f.SubmitText == "" {
- f.SubmitText = "Submit"
- }
- // set visibility on the buttons
- if f.OnCancel == nil {
- f.cancelButton.Hide()
- } else {
- f.cancelButton.SetText(f.CancelText)
- f.cancelButton.OnTapped = f.OnCancel
- f.cancelButton.Show()
- }
- if f.OnSubmit == nil {
- f.submitButton.Hide()
- } else {
- f.submitButton.SetText(f.SubmitText)
- f.submitButton.OnTapped = f.OnSubmit
- f.submitButton.Show()
- }
- if f.OnCancel == nil && f.OnSubmit == nil {
- f.buttonBox.Hide()
- } else {
- f.buttonBox.Show()
- }
- }
- func (f *Form) checkValidation(err error) {
- if err != nil {
- f.submitButton.Disable()
- return
- }
- for _, item := range f.Items {
- if item.invalid {
- f.submitButton.Disable()
- return
- }
- }
- if !f.disabled {
- f.submitButton.Enable()
- }
- }
- func (f *Form) ensureRenderItems() {
- done := len(f.itemGrid.Objects) / 2
- if done >= len(f.Items) {
- f.itemGrid.Objects = f.itemGrid.Objects[0 : len(f.Items)*2]
- return
- }
- adding := len(f.Items) - done
- objects := make([]fyne.CanvasObject, adding*2)
- off := 0
- for i, item := range f.Items {
- if i < done {
- continue
- }
- objects[off] = f.createLabel(item.Text)
- off++
- f.setUpValidation(item.Widget, i)
- objects[off] = f.createInput(item)
- off++
- }
- f.itemGrid.Objects = append(f.itemGrid.Objects, objects...)
- }
- func (f *Form) setUpValidation(widget fyne.CanvasObject, i int) {
- updateValidation := func(err error) {
- if err == errFormItemInitialState {
- return
- }
- f.Items[i].validationError = err
- f.Items[i].invalid = err != nil
- f.setValidationError(err)
- f.checkValidation(err)
- f.updateHelperText(f.Items[i])
- }
- if w, ok := widget.(fyne.Validatable); ok {
- f.Items[i].invalid = w.Validate() != nil
- if e, ok := w.(*Entry); ok {
- e.onFocusChanged = func(bool) {
- updateValidation(e.validationError)
- }
- if e.Validator != nil && f.Items[i].invalid {
- // set initial state error to guarantee next error (if triggers) is always different
- e.SetValidationError(errFormItemInitialState)
- }
- }
- w.SetOnValidationChanged(updateValidation)
- }
- }
- func (f *Form) setValidationError(err error) {
- if err == nil && f.validationError == nil {
- return
- }
- if !errors.Is(err, f.validationError) {
- if err == nil {
- for _, item := range f.Items {
- if item.invalid {
- err = item.validationError
- break
- }
- }
- }
- f.validationError = err
- if f.onValidationChanged != nil {
- f.onValidationChanged(err)
- }
- }
- }
- func (f *Form) updateHelperText(item *FormItem) {
- if item.helperOutput == nil {
- return // testing probably, either way not rendered yet
- }
- showHintIfError := false
- if e, ok := item.Widget.(*Entry); ok && (!e.dirty || e.focused) {
- showHintIfError = true
- }
- if item.validationError == nil || showHintIfError {
- item.helperOutput.Text = item.HintText
- item.helperOutput.Color = theme.PlaceHolderColor()
- } else {
- item.helperOutput.Text = item.validationError.Error()
- item.helperOutput.Color = theme.ErrorColor()
- }
- item.helperOutput.Refresh()
- }
- func (f *Form) updateLabels() {
- for i, item := range f.Items {
- l := f.itemGrid.Objects[i*2].(*canvas.Text)
- l.TextSize = theme.TextSize()
- if dis, ok := item.Widget.(fyne.Disableable); ok {
- if dis.Disabled() {
- l.Color = theme.DisabledColor()
- } else {
- l.Color = theme.ForegroundColor()
- }
- } else {
- l.Color = theme.ForegroundColor()
- }
- l.Text = item.Text
- l.Refresh()
- f.updateHelperText(item)
- }
- }
- // CreateRenderer is a private method to Fyne which links this widget to its renderer
- func (f *Form) CreateRenderer() fyne.WidgetRenderer {
- f.ExtendBaseWidget(f)
- f.cancelButton = &Button{Icon: theme.CancelIcon(), OnTapped: f.OnCancel}
- f.submitButton = &Button{Icon: theme.ConfirmIcon(), OnTapped: f.OnSubmit, Importance: HighImportance}
- buttons := &fyne.Container{Layout: layout.NewGridLayoutWithRows(1), Objects: []fyne.CanvasObject{f.cancelButton, f.submitButton}}
- f.buttonBox = &fyne.Container{Layout: layout.NewBorderLayout(nil, nil, nil, buttons), Objects: []fyne.CanvasObject{buttons}}
- f.validationError = errFormItemInitialState // set initial state error to guarantee next error (if triggers) is always different
- f.itemGrid = &fyne.Container{Layout: layout.NewFormLayout()}
- content := &fyne.Container{Layout: layout.NewVBoxLayout(), Objects: []fyne.CanvasObject{f.itemGrid, f.buttonBox}}
- renderer := NewSimpleRenderer(content)
- f.ensureRenderItems()
- f.updateButtons()
- f.updateLabels()
- f.checkValidation(nil) // will trigger a validation check for correct intial validation status
- return renderer
- }
- // NewForm creates a new form widget with the specified rows of form items
- // and (if any of them should be shown) a form controls row at the bottom
- func NewForm(items ...*FormItem) *Form {
- form := &Form{Items: items}
- form.ExtendBaseWidget(form)
- return form
- }
- type formItemLayout struct{}
- func (f formItemLayout) Layout(objs []fyne.CanvasObject, size fyne.Size) {
- itemHeight := objs[0].MinSize().Height
- objs[0].Resize(fyne.NewSize(size.Width, itemHeight))
- objs[1].Move(fyne.NewPos(theme.InnerPadding(), itemHeight+theme.InnerPadding()/2))
- objs[1].Resize(fyne.NewSize(size.Width, objs[1].MinSize().Width))
- }
- func (f formItemLayout) MinSize(objs []fyne.CanvasObject) fyne.Size {
- min0 := objs[0].MinSize()
- min1 := objs[1].MinSize()
- minWidth := fyne.Max(min0.Width, min1.Width)
- return fyne.NewSize(minWidth, min0.Height+min1.Height+theme.InnerPadding())
- }
|