diff --git a/dependencies.md b/dependencies.md index 4e4527bf..87fbaa22 100644 --- a/dependencies.md +++ b/dependencies.md @@ -1,6 +1,6 @@ -# Dependencies of `io.spine.chords:spine-chords-client:2.0.0-SNAPSHOT.88` +# Dependencies of `io.spine.chords:spine-chords-client:2.0.0-SNAPSHOT.89` ## Runtime 1. **Group** : cafe.adriel.voyager. **Name** : voyager-core. **Version** : 1.0.1.**No license information found** @@ -1104,12 +1104,12 @@ The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Jul 14 20:02:53 EEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). +This report was generated on **Thu Jul 16 14:46:06 EEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.chords:spine-chords-codegen-tests:2.0.0-SNAPSHOT.88` +# Dependencies of `io.spine.chords:spine-chords-codegen-tests:2.0.0-SNAPSHOT.89` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -1899,12 +1899,12 @@ This report was generated on **Tue Jul 14 20:02:53 EEST 2026** using [Gradle-Lic The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Jul 14 20:02:54 EEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). +This report was generated on **Thu Jul 16 14:46:07 EEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.chords:spine-chords-core:2.0.0-SNAPSHOT.88` +# Dependencies of `io.spine.chords:spine-chords-core:2.0.0-SNAPSHOT.89` ## Runtime 1. **Group** : cafe.adriel.voyager. **Name** : voyager-core. **Version** : 1.0.1. @@ -2938,12 +2938,12 @@ This report was generated on **Tue Jul 14 20:02:54 EEST 2026** using [Gradle-Lic The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Jul 14 20:02:55 EEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). +This report was generated on **Thu Jul 16 14:46:08 EEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.chords:spine-chords-proto:2.0.0-SNAPSHOT.88` +# Dependencies of `io.spine.chords:spine-chords-proto:2.0.0-SNAPSHOT.89` ## Runtime 1. **Group** : cafe.adriel.voyager. **Name** : voyager-core. **Version** : 1.0.1.**No license information found** @@ -3976,12 +3976,12 @@ This report was generated on **Tue Jul 14 20:02:55 EEST 2026** using [Gradle-Lic The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Jul 14 20:02:56 EEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). +This report was generated on **Thu Jul 16 14:46:09 EEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.chords:spine-chords-proto-values:2.0.0-SNAPSHOT.88` +# Dependencies of `io.spine.chords:spine-chords-proto-values:2.0.0-SNAPSHOT.89` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -4775,12 +4775,12 @@ This report was generated on **Tue Jul 14 20:02:56 EEST 2026** using [Gradle-Lic The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Jul 14 20:02:57 EEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). +This report was generated on **Thu Jul 16 14:46:10 EEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.chords:spine-chords-runtime:2.0.0-SNAPSHOT.88` +# Dependencies of `io.spine.chords:spine-chords-runtime:2.0.0-SNAPSHOT.89` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -5544,4 +5544,4 @@ This report was generated on **Tue Jul 14 20:02:57 EEST 2026** using [Gradle-Lic The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Jul 14 20:02:57 EEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). \ No newline at end of file +This report was generated on **Thu Jul 16 14:46:11 EEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). \ No newline at end of file diff --git a/pom.xml b/pom.xml index 9e6a9c17..9f8dc7bd 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ all modules and does not describe the project structure per-subproject. --> io.spine.chords Chords -2.0.0-SNAPSHOT.88 +2.0.0-SNAPSHOT.89 2015 diff --git a/proto/src/main/kotlin/io/spine/chords/proto/form/MessageForm.kt b/proto/src/main/kotlin/io/spine/chords/proto/form/MessageForm.kt index 7c076fb9..0c452026 100644 --- a/proto/src/main/kotlin/io/spine/chords/proto/form/MessageForm.kt +++ b/proto/src/main/kotlin/io/spine/chords/proto/form/MessageForm.kt @@ -1544,17 +1544,17 @@ public open class MessageForm : InputComponent(), InputContext { * @param updateValidationErrors Specifies whether a failure to create * a message should be accompanied by displaying the respective * validation errors. - * @param focusInvalidField Specifies whether the first field that has + * @param focusInvalidPart Specifies whether the first field that has * caused a validation failure (if any) should receive a focus. */ private fun updateMessage( updateValidationErrors: Boolean = true, - focusInvalidField: Boolean = true + focusInvalidPart: Boolean = true ) { if (updateValidationErrors) { clearValidationDisplay() fields.values.forEach { - it.editor?.updateValidationDisplay(focusInvalidField) + it.editor?.updateValidationDisplay(focusInvalidPart) } } @@ -1575,8 +1575,8 @@ public open class MessageForm : InputComponent(), InputContext { } catch (e: ValidationException) { if (updateValidationErrors) { showConstraintViolations(e.constraintViolations) - if (focusInvalidField) { - focus() + if (focusInvalidPart) { + focusInvalidField() } recoveringFromManualValidationErrors = true } @@ -1595,8 +1595,8 @@ public open class MessageForm : InputComponent(), InputContext { } valid.value = !nestedErrorsPresent value.value = if (nestedErrorsPresent) { - if (focusInvalidField) { - focus() + if (focusInvalidPart) { + focusInvalidField() } null } else { @@ -1711,13 +1711,71 @@ public open class MessageForm : InputComponent(), InputContext { } } + /** + * Specifies that the focus request that this form is about to receive was + * made in order to focus the field that has caused a validation failure. + * + * Focus requests are dispatched asynchronously (see [focusRequest]), and + * a nested form receives them via its [focus] method. This flag is how + * a form that focuses an invalid field whose editor is a nested form tells + * that form to focus its invalid field as well, instead of its first one. + * + * The flag is cleared as soon as it is read in [focus]. + * + * @see focusInvalidField + */ + private var focusInvalidFieldRequested = false + /** * Focuses the form. * - * More precisely, it focuses the first field, or the first invalid field - * (if there's at least one invalid field). + * More precisely, the form's first field is focused, unless this form has + * been asked to focus the field that has caused a validation failure, in + * which case [focusInvalidField] is invoked. + * + * Focusing the first field is what is expected when the form is displayed + * to the user for the first time (e.g., when a dialog containing the form + * is opened), so the user can start filling the form in from its + * first field. + * + * Note that a field being empty doesn't make it invalid in this context: + * all fields of a form that has just been displayed are typically empty, + * and some of them can be reported as invalid merely because of this. Use + * [focusInvalidField] to focus the field whose entry has actually caused + * a validation failure. + * + * @see focusInvalidField */ override fun focus() { + if (focusInvalidFieldRequested) { + focusInvalidFieldRequested = false + focusInvalidField() + return + } + val firstField = fields.values.firstOrNull() + + // If the first field is a oneof field, make sure that the + // selected oneof option actually gets the focus by default. + val fieldToFocus = firstField?.formOneof?.selectedFormField ?: firstField + + fieldToFocus?.focusEditor() + } + + /** + * Focuses the first field that has caused a validation failure, and falls + * back to focusing the form's first field if there's no such field. + * + * This is the focusing that is expected once the form's validation has + * been triggered, so the user can fix the entry that has caused it + * to fail. + * + * If the field to be focused is edited by a nested form, that form is + * asked to focus its invalid field as well, so that the invalid entry is + * focused however deeply it is nested. + * + * @see focus + */ + protected fun focusInvalidField() { val fieldToFocus = fields.values.firstOrNull { field -> !field.valueValid.value || @@ -1732,15 +1790,14 @@ public open class MessageForm : InputComponent(), InputContext { } formOneof?.selectedFormField ?: formOneof?.fields?.values?.firstOrNull() - } ?: run { - val firstField = fields.values.firstOrNull() - - // If the first field is a oneof field, make sure that the - // selected oneof option actually gets the focus by default. - firstField?.formOneof?.selectedFormField ?: firstField } - fieldToFocus?.focusEditor() + if (fieldToFocus == null) { + focus() + return + } + (fieldToFocus.editor as? MessageForm<*>)?.focusInvalidFieldRequested = true + fieldToFocus.focusEditor() } } diff --git a/version.gradle.kts b/version.gradle.kts index 18bbc932..1d6450f8 100644 --- a/version.gradle.kts +++ b/version.gradle.kts @@ -27,4 +27,4 @@ /** * The version of all Chords libraries. */ -val chordsVersion: String by extra("2.0.0-SNAPSHOT.88") +val chordsVersion: String by extra("2.0.0-SNAPSHOT.89")