diff --git a/widget/entry.go b/widget/entry.go
index bc72eddc94..0ae0de4a1a 100644
--- a/widget/entry.go
+++ b/widget/entry.go
@@ -59,12 +59,17 @@ type Entry struct {
cursorAnim *entryCursorAnimation
+ dirty bool
focused bool
text *textProvider
placeholder *textProvider
content *entryContent
scroll *widget.Scroll
+ // useful for Form validation (as the error text should only be shown when
+ // the entry is unfocused)
+ onFocusChanged func(bool)
+
// selectRow and selectColumn represent the selection start location
// The selection will span from selectRow/Column to CursorRow/Column -- note that the cursor
// position may occur before or after the select start position in the text.
@@ -142,7 +147,7 @@ func (e *Entry) Bind(data binding.String) {
val, err := data.Get()
if err != nil {
convertErr = err
- e.SetValidationError(e.Validate())
+ e.Validate()
return
}
e.Text = val
@@ -156,7 +161,7 @@ func (e *Entry) Bind(data binding.String) {
e.OnChanged = func(s string) {
convertErr = data.Set(s)
- e.SetValidationError(e.Validate())
+ e.Validate()
}
}
@@ -303,8 +308,12 @@ func (e *Entry) ExtendBaseWidget(wid fyne.Widget) {
// Implements: fyne.Focusable
func (e *Entry) FocusGained() {
e.setFieldsAndRefresh(func() {
+ e.dirty = true
e.focused = true
})
+ if e.onFocusChanged != nil {
+ e.onFocusChanged(true)
+ }
}
// FocusLost is called when the Entry has had focus removed.
@@ -315,6 +324,9 @@ func (e *Entry) FocusLost() {
e.focused = false
e.selectKeyDown = false
})
+ if e.onFocusChanged != nil {
+ e.onFocusChanged(false)
+ }
}
// Hide hides the entry.
@@ -1033,6 +1045,10 @@ func (e *Entry) textProvider() *textProvider {
return e.text
}
+ if e.Text != "" {
+ e.dirty = true
+ }
+
text := newTextProvider(e.Text, e)
text.ExtendBaseWidget(text)
text.inset = fyne.NewSize(0, theme.InputBorderSize())
@@ -1080,14 +1096,16 @@ func (e *Entry) updateText(text string) {
changed := e.Text != text
e.Text = text
+ if e.Text != "" {
+ e.dirty = true
+ }
+
if changed {
callback = e.OnChanged
}
})
- if validate := e.Validator; validate != nil {
- e.SetValidationError(validate(text))
- }
+ e.Validate()
if callback != nil {
callback(text)
@@ -1287,7 +1305,7 @@ func (r *entryRenderer) Refresh() {
}
if r.entry.Validator != nil {
- if !r.entry.focused && !r.entry.Disabled() && r.entry.Text != "" && r.entry.validationError != nil {
+ if !r.entry.focused && !r.entry.Disabled() && r.entry.dirty && r.entry.validationError != nil {
r.line.FillColor = theme.ErrorColor()
}
r.ensureValidationSetup()
@@ -1306,9 +1324,8 @@ func (r *entryRenderer) ensureValidationSetup() {
r.objects = append(r.objects, r.entry.validationStatus)
r.Layout(r.entry.size)
- if r.entry.Text != "" {
- r.entry.Validate()
- }
+ r.entry.Validate()
+
r.Refresh()
}
}
diff --git a/widget/entry_validation.go b/widget/entry_validation.go
index 5a162ea841..a548e46342 100644
--- a/widget/entry_validation.go
+++ b/widget/entry_validation.go
@@ -95,15 +95,15 @@ func (r *validationStatusRenderer) MinSize() fyne.Size {
func (r *validationStatusRenderer) Refresh() {
r.entry.propertyLock.RLock()
defer r.entry.propertyLock.RUnlock()
- if r.entry.Text == "" || r.entry.disabled {
+ if r.entry.disabled {
r.icon.Hide()
return
}
- if r.entry.validationError == nil {
+ if r.entry.validationError == nil && r.entry.Text != "" {
r.icon.Resource = theme.ConfirmIcon()
r.icon.Show()
- } else if !r.entry.focused {
+ } else if r.entry.validationError != nil && !r.entry.focused && r.entry.dirty {
r.icon.Resource = theme.NewErrorThemedResource(theme.ErrorIcon())
r.icon.Show()
} else {
diff --git a/widget/entry_validation_test.go b/widget/entry_validation_test.go
index 4e72353a9e..83f9320b09 100644
--- a/widget/entry_validation_test.go
+++ b/widget/entry_validation_test.go
@@ -63,6 +63,30 @@ func TestEntry_Validate(t *testing.T) {
assert.Equal(t, entry.Validate(), entry.Validator(entry.Text))
}
+func TestEntry_NotEmptyValidator(t *testing.T) {
+ test.NewApp()
+ defer test.NewApp()
+ entry := widget.NewEntry()
+ entry.Validator = func(s string) error {
+ if s == "" {
+ return errors.New("should not be empty")
+ }
+ return nil
+ }
+ w := test.NewWindow(entry)
+ defer w.Close()
+
+ test.AssertRendersToMarkup(t, "entry/validator_not_empty_initial.xml", w.Canvas())
+
+ w.Canvas().Focus(entry)
+
+ test.AssertRendersToMarkup(t, "entry/validator_not_empty_focused.xml", w.Canvas())
+
+ w.Canvas().Focus(nil)
+
+ test.AssertRendersToMarkup(t, "entry/validator_not_empty_unfocused.xml", w.Canvas())
+}
+
func TestEntry_SetValidationError(t *testing.T) {
entry, window := setupImageTest(t, false)
fyne.CurrentApp().Settings().SetTheme(theme.LightTheme())
diff --git a/widget/form.go b/widget/form.go
index 170cc1db54..f6c99c4879 100644
--- a/widget/form.go
+++ b/widget/form.go
@@ -190,21 +190,27 @@ func (f *Form) checkValidation(err error) {
}
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.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.Validator != nil && f.Items[i].invalid {
- // set initial state error to guarantee next error (if triggers) is always different
- e.SetValidationError(errFormItemInitialState)
- }
- w.SetOnValidationChanged(func(err error) {
- if err == errFormItemInitialState {
- return
+ if e, ok := w.(*Entry); ok {
+ e.onFocusChanged = func(bool) {
+ updateValidation(e.validationError)
}
- f.Items[i].validationError = err
- f.Items[i].invalid = err != nil
- f.checkValidation(err)
- f.updateHelperText(f.Items[i])
- })
+ 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)
}
}
@@ -212,7 +218,11 @@ func (f *Form) updateHelperText(item *FormItem) {
if item.helperOutput == nil {
return // testing probably, either way not rendered yet
}
- if item.validationError == nil {
+ 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 {
diff --git a/widget/form_test.go b/widget/form_test.go
index 086b00baf6..b60e1dfff7 100644
--- a/widget/form_test.go
+++ b/widget/form_test.go
@@ -213,9 +213,6 @@ func TestForm_EntryValidation_FirstTypeValid(t *testing.T) {
w := test.NewWindow(form)
defer w.Close()
- assert.Equal(t, errFormItemInitialState, entry1.validationError)
- assert.Equal(t, errFormItemInitialState, entry2.validationError)
-
test.AssertImageMatches(t, "form/validation_entry_first_type_initial.png", w.Canvas().Capture())
test.Type(entry1, "H")
diff --git a/widget/testdata/entry/validator_not_empty_focused.xml b/widget/testdata/entry/validator_not_empty_focused.xml
new file mode 100644
index 0000000000..44486a01c8
--- /dev/null
+++ b/widget/testdata/entry/validator_not_empty_focused.xml
@@ -0,0 +1,21 @@
+
diff --git a/widget/testdata/entry/validator_not_empty_initial.xml b/widget/testdata/entry/validator_not_empty_initial.xml
new file mode 100644
index 0000000000..d0f109da87
--- /dev/null
+++ b/widget/testdata/entry/validator_not_empty_initial.xml
@@ -0,0 +1,20 @@
+
diff --git a/widget/testdata/entry/validator_not_empty_unfocused.xml b/widget/testdata/entry/validator_not_empty_unfocused.xml
new file mode 100644
index 0000000000..e64db69c13
--- /dev/null
+++ b/widget/testdata/entry/validator_not_empty_unfocused.xml
@@ -0,0 +1,21 @@
+
diff --git a/widget/testdata/form/hint_invalid.png b/widget/testdata/form/hint_invalid.png
index de16457c11..6fe6671471 100644
Binary files a/widget/testdata/form/hint_invalid.png and b/widget/testdata/form/hint_invalid.png differ
diff --git a/widget/testdata/form/validation_entry_first_type_invalid.png b/widget/testdata/form/validation_entry_first_type_invalid.png
index 2b613f3bf1..b887eb68ab 100644
Binary files a/widget/testdata/form/validation_entry_first_type_invalid.png and b/widget/testdata/form/validation_entry_first_type_invalid.png differ