diff --git a/client/src/main/kotlin/io/spine/chords/client/Client.kt b/client/src/main/kotlin/io/spine/chords/client/Client.kt index 3d618727..6c38167f 100644 --- a/client/src/main/kotlin/io/spine/chords/client/Client.kt +++ b/client/src/main/kotlin/io/spine/chords/client/Client.kt @@ -183,6 +183,11 @@ public interface EventSubscription { * by the implementation. */ public suspend fun awaitEvent(): E + + /** + * A callback, which is invoked when the subscribed event is emitted. + */ + public var onEvent: ((E) -> Unit)? } /** diff --git a/client/src/main/kotlin/io/spine/chords/client/CommandLifecycle.kt b/client/src/main/kotlin/io/spine/chords/client/CommandLifecycle.kt new file mode 100644 index 00000000..67fdc5a1 --- /dev/null +++ b/client/src/main/kotlin/io/spine/chords/client/CommandLifecycle.kt @@ -0,0 +1,528 @@ +/* + * Copyright 2024, TeamDev. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chords.client + +import com.google.protobuf.Message +import io.spine.base.CommandMessage +import io.spine.base.EventMessage +import io.spine.base.EventMessageField +import io.spine.base.RejectionMessage +import io.spine.chords.client.appshell.client +import io.spine.chords.core.appshell.app +import io.spine.chords.core.layout.MessageDialog.Companion.showMessage +import io.spine.chords.core.writeOnce +import java.util.concurrent.CompletableFuture +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.future.await +import kotlinx.coroutines.withTimeout + +/** + * An extension function, which first posts the command on which it is invoked, + * then waits until either of the outcomes configured by the passed [lifecycle] + * is received, or until the timeout period configured in [lifecycle] + * object elapses. + * + * This function makes the [lifecycle] object to handle the outcomes or error + * conditions that have occurred after posting the command. + * + * @param lifecycle A [CommandLifecycle] instance whose configuration defines + * how the command's possible outcomes should be handled. + * @return `true`, if either of the non-rejection events configured in + * [lifecycle] was received before the timeout period elapses, and + * `false` otherwise. + * + */ +public suspend fun C.post(lifecycle: CommandLifecycle): Boolean = + lifecycle.post(this) + +/** + * An object, which can be set up to handle the client-side lifecycle of command + * [C], starting with posting of a command, and then handling different types + * of outcomes, and error/timeout conditions. + * + * This object supports configuring the following aspects of the client side + * command handling lifecycle: + * + * - First, the respective command [C] should be posted using the [post] method, + * or using the respective `CommandMessage.post` extension function. + * + * This call will wait until either of the events or rejections specified + * within [outcomeSubscriptions] is emitted, or until the [timeout] + * period elapses. + * + * If either of the specified non-rejection events is emitted during this + * period, then the function will run the respective handler (if specified), + * and return `true` to signify the "positive" command posting outcome. + * + * Otherwise (if either of the specified rejections is emitted, or if an error + * is identified during the command's posting, or when no events or rejections + * are emitted during the [timeout] period), the respective condition is first + * handled accordingly (see below), and then `false` is returned to signify + * the "negative" command posting outcome. + * + * - The [outcomeSubscriptions] lambda can be used to specify an arbitrary set + * of event and rejection subscriptions to specify the expected positive and + * negative outcomes of posting the command [C] respectively. + * + * By default, upon receiving either of the expected rejections, it invokes + * the [onRejection] callback, or, when it's not specified, displays a message + * defined by [rejectionMessage]. + * + * It's also possible to specify the per-event/per-rejection handlers, which + * will override the default behavior above if more granular event/rejection + * handling is required. + * + * - If an error happens when posting or acknowledging the command, the + * [onPostingError] callback is invoked (or the [postingErrorMessage] text is + * displayed if the callback is not specified). + * + * - Analogously, when the timeout condition is identified (if neither of the + * configured events/rejections are received during the [timeout] period), the + * [onTimeout] callback is invoked (or the [timeoutMessage] text is displayed + * if the callback is not specified). + * + * ## Usage examples + * + * Note that the [outcomeSubscriptions] lambda is invoked in context of the + * [OutcomeSubscriptionScope] object, which provides the + * [command][OutcomeSubscriptionScope.command], which is going to be posted, and + * some functions that can be used to declare the list of expected command + * outcomes. See the [event][OutcomeSubscriptionScope.event], + * [rejection][OutcomeSubscriptionScope.rejection], and + * [handledAs][OutcomeSubscriptionScope.handledAs] functions. + * + * Here's a simple example of configuring `CommandLifecycle` to wait for + * `ExpectedEvent` as an expected "positive" outcome of `SomeCommand`: + * ``` + * val command: SomeCommand = someCommand() + * val succeeded: Boolean = command.post( + * CommandLifecycle({ + * event( + * ExpectedEvent::class.java, + * ExpectedEvent.Field.id(), + * command.id + * ) + * }) + * ) + * // `succeeded` will contain `true` if `ExpectedEvent.id == command.id` was + * // emitted as an outcome of handling command `SomeCommand` during the + * // default timeout period, and `false` otherwise. + * ``` + * + * Below is an example, which demonstrates handling both a regular + * (non-rejection) event, and a rejection event. Receiving the specified + * rejection will display the text message defined by [rejectionMessage], and + * will make the `post` function to return `false`. + * ``` + * val command: SomeCommand = someCommand() + * val succeeded: Boolean = command.post( + * CommandLifecycle({ + * event( + * ExpectedEvent::class.java, + * ExpectedEvent.Field.id(), + * command.id + * ) + * rejection( + * ExpectedRejection::class.java, + * ExpectedRejection.Field.id(), + * command.id + * ) + * }) + * ) + * // If `ExpectedRejection` is emitted during the [timeout] period, + * // `succeeded` will be `false. + * ``` + * + * It is also optionally possible to customize event/rejection handlers either + * on a per-event/per-rejection basis using the + * [handledAs][OutcomeSubscriptionScope.handledAs] infix function, or using the + * [onEvent]/[onRejection] callbacks, which will be invoked upon receiving + * either of the configured events/rejections: + * ``` + * val command: SomeCommand = someCommand() + * val succeeded: Boolean = command.post( + * CommandLifecycle( + * { + * event( + * ExpectedEvent::class.java, + * ExpectedEvent.Field.id(), + * command.id + * ) + * rejection( + * ExpectedRejection1::class.java, + * ExpectedRejection1.Field.id(), + * command.id + * ) + * rejection( + * ExpectedRejection2::class.java, + * ExpectedRejection2.Field.id(), + * command.id + * ) handledAs { + * showMessage("Custom rejection message for ExpectedRejection2") + * anyOtherCodeForThisRejection() + * } + * }, + * onRejection = { command, rejection -> + * showMessage("Rejection ${rejection.javaClass.simpleName} was " + + * "received as an outcome for command ${command.javaClass.simpleName}") + * } + * ) + * ) + * ``` + * + * Please see the property descriptions below for the full list of + * customizations available. + * + * @param C A type of command whose lifecycle is being configured. + * + * @param outcomeSubscriptions A lambda, which defines the set of events and + * rejections, which can be emitted as an outcome of command [C]. This lambda + * should use the [event][OutcomeSubscriptionScope.event] and + * [rejection][OutcomeSubscriptionScope.rejection] functions to set up + * expected positive/negative command outcomes respectively. Besides, each + * event/command subscriptions can optionally be accompanied with the + * [handledBy][OutcomeSubscriptionScope.handledAs] infix function to specify + * a custom per-event/per-rejection handler(s) where such fine-grained + * handlers are required. + * @param onEvent An optional callback, which will be invoked upon receiving any + * of the configured non-rejection events. + * @param onRejection An optional callback, which will be invoked upon receiving any + * of the configured rejections. + * @param onPostingError An optional callback, which will be invoked when an + * error was received during posting or acknowledging the command. If no + * callback is provided, the [handlePostingError] method will be invoked, + * which displays a respective message dialog by default. + * @param onTimeout An optional callback, which will be invoked when neither of + * the events/rejections configured with [outcomeSubscriptions] were received + * during the [timeout] period. + * @param eventMessage A callback, which can return a non-null value to specify + * the text message that needs to be displayed when either of the expected + * non-rejection events is received. + * @param rejectionMessage A callback, which can return a non-null value to + * specify the text message that needs to be displayed when either of the + * expected rejections is received. + * @param timeoutMessage A callback, which provides a message that should be + * displayed upon the timeout condition by default (if no [onTimeout] + * parameter is specified). + * @param postingErrorMessage A callback, which provides a message that should + * be displayed upon a posting error by default (if no [onPostingError] + * parameter is specified). + * @param timeout A maximum period of time starting from the moment the command + * was posted that the configured outcomes are being waited for by + * this object. + * @return `true`, to signify the positive command posting outcome (e.g., when + * either of configured non-rejection events was emitted before the [timeout] + * period elapses), and `false` otherwise. + */ +public open class CommandLifecycle( + private val outcomeSubscriptions: OutcomeSubscriptionScope.() -> Unit, + private var onEvent: (suspend (C, EventMessage) -> Unit)? = null, + private var onRejection: (suspend (C, RejectionMessage) -> Unit)? = null, + private var onPostingError: (suspend (C, CommandPostingError) -> Unit)? = null, + private var onTimeout: (suspend (C) -> Unit)? = null, + private var eventMessage: (C, EventMessage) -> String? = { command, event -> null }, + private var rejectionMessage: (C, RejectionMessage) -> String? = { command, rejection -> + "Rejection ${rejection.javaClass.simpleName} was emitted in response to " + + "the command ${command.javaClass.simpleName}" + }, + private var timeoutMessage: (C) -> String? = { command -> + "Timed out waiting for an event in response to " + + "the command ${command.javaClass.simpleName}" + }, + private var postingErrorMessage: (C, CommandPostingError) -> String? = { command, error -> + "An error has occurred when posting or acknowledging " + + "the command ${command.javaClass.simpleName}: ${error.message}" + }, + private var timeout: Duration = 20.seconds +) { + + /** + * Subscriptions, which should be waited for before the [post] + * method returns. + */ + private val subscriptions: MutableSet> = HashSet() + + /** + * The handlers that were attached to subscriptions. + * + * The keys are a subset of [subscriptions]. + */ + private val subscriptionHandlers: MutableMap< + EventSubscription, + suspend (EventMessage) -> Unit + > = HashMap() + + private inner class OutcomeSubscriptionScopeImpl( + override val command: C + ) : OutcomeSubscriptionScope { + override fun event( + eventType: Class, + field: EventMessageField, + fieldValue: Message + ): EventSubscription = subscribe(eventType, field, fieldValue) + + override fun rejection( + rejectionType: Class, + field: EventMessageField, + fieldValue: Message + ): EventSubscription = subscribe(rejectionType, field, fieldValue) + + override fun EventSubscription.handledAs( + handler: suspend (EventMessage) -> Unit + ) { + subscriptions += this + subscriptionHandlers[this] = handler + } + } + + /** + * Subscribes to the specified type of event according to the specified + * field value, and registers this subscription as the one that the [post] + * method should wait for. + */ + protected fun subscribe( + eventType: Class, + field: EventMessageField, + fieldValue: Message + ): EventSubscription { + val eventSubscription = app.client.subscribeToEvent(eventType, field, fieldValue) + subscriptions += eventSubscription + return eventSubscription + } + + /** + * Configures the object with any subscriptions that are needed. + * + * The event subscriptions are expected to be made with the + * [subscribe] method. + */ + protected open fun makeSubscriptions(command: C) { + val scope = OutcomeSubscriptionScopeImpl(command) + scope.outcomeSubscriptions() + } + + /** + * Posts the given [command], and makes sure that the expected outcomes + * (events/rejections) or other conditions (posting errors or outcome + * waiting timeout condition) are handled according to this + * object's configuration. + * + * @param command The command that should be posted. + * @return `true` if any of the expected non-rejection events were received + * before the [timeout] period elapses, and `false` otherwise. + */ + public suspend fun post(command: C): Boolean { + makeSubscriptions(command) + + val futureCommandOutcome = CompletableFuture() + var subscriptionTriggered: EventSubscription<*> by writeOnce() + for (subscription in subscriptions) { + subscription.onEvent = { event -> + subscriptionTriggered = subscription + futureCommandOutcome.complete(event) + } + } + + return try { + app.client.command(command) + + val event = withTimeout(timeout) { futureCommandOutcome.await() } + + val eventHandler = subscriptionHandlers[subscriptionTriggered] + if (eventHandler != null) { + eventHandler(event) + } else { + if (event is RejectionMessage) { + handleRejection(command, event) + } else { + handleEvent(command, event) + } + } + + event !is RejectionMessage + } catch (e: CommandPostingError) { + handlePostingError(command, e) + false + } catch ( + @Suppress( + // A timeout condition is handled by `outcomeHandler`. + "SwallowedException" + ) + e: TimeoutCancellationException + ) { + handleTimeout(command) + false + } + } + + /** + * Invoked when any of the expected non-rejection events was received. + * + * @param command The command that was posted. + * @param event The non-rejection event that was received after posting + * the [command]. + */ + protected open suspend fun handleEvent(command: C, event: EventMessage) { + if (onEvent != null) { + onEvent!!(command, event) + } else { + val message = eventMessage(command, event) + if (message != null) { + showMessage(message) + } + } + } + + /** + * Invoked when any of the expected rejections was received. + * + * @param command The command that was posted. + * @param rejection The rejection that was received after posting + * the [command]. + */ + protected open suspend fun handleRejection(command: C, rejection: RejectionMessage) { + if (onRejection != null) { + onRejection!!(command, rejection) + } else { + val message = rejectionMessage(command, rejection) + if (message != null) { + showMessage(message) + } + } + } + + /** + * Invoked when an error has occurred during posting or acknowledging + * the command. + * + * @param command The command that was posted. + * @param error The error that has occurred during posting or acknowledging + * the command. + */ + protected open suspend fun handlePostingError(command: C, error: CommandPostingError) { + if (onPostingError != null) { + onPostingError!!(command, error) + } else { + val message = postingErrorMessage(command, error) + if (message != null) { + showMessage(message) + } + } + } + + /** + * Invoked when neither of the expected events/rejections were received + * during the [timeout] period after the [command] was posted. + * + * @param command The command that was posted. + */ + protected open suspend fun handleTimeout(command: C) { + if (onTimeout != null) { + onTimeout!!(command) + } else { + val message = timeoutMessage(command) + if (message != null) { + showMessage(message) + } + } + } +} + +/** + * Defines a DSL available in scope of the [CommandLifecycle]'s + * [outcomeSubscriptions][CommandLifecycle.outcomeSubscriptions] callback. + */ +public interface OutcomeSubscriptionScope { + + /** + * The command whose outcomes are being specified in this scope. + */ + public val command: C + + /** + * Subscribes to an event of type [eventType], which has its [field] equal + * to [fieldValue], and registers it as one of the possible outcomes. + * + * This method is typically expected to be used only for non-rejection types + * of events. For specifying rejection subscriptions, use the + * [rejection] function. + * + * @param eventType A type of event that should be subscribed to. + * @param field A field whose value should identify an event being + * subscribed to. + * @param fieldValue A value of event's [field] that identifies an event + * being subscribed to. + * @return A subscription that was created. + * @see rejection + */ + public fun event( + eventType: Class, + field: EventMessageField, + fieldValue: Message + ): EventSubscription + + /** + * Subscribes to a rejection of type [rejectionType], which has its [field] + * equal to [fieldValue], and registers it as one of the possible outcomes. + * + * @param rejectionType A type of rejection that should be subscribed to. + * @param field A field whose value should identify a rejection being + * subscribed to. + * @param fieldValue A value of event's [field] that identifies a rejection + * being subscribed to. + * @return A subscription that was created. + * @see event + */ + public fun rejection( + rejectionType: Class, + field: EventMessageField, + fieldValue: Message + ): EventSubscription + + /** + * An infix function, which can be used alongside with [event] or + * [rejection] function call to specify how the respective outcome should + * be handled. + * + * Specifying such an individual event/rejection handler associates the + * provided [handler] with this particular event/rejection subscription, and + * doing so prevents the [CommandLifecycle]'s default + * [CommandLifecycle.onEvent]/[CommandLifecycle.onRejection] functions to be + * called for this specific event/rejection subscription in favor of + * this [handler]. + * + * @receiver An event/rejection subscription, for which an individual + * handler has to be registered, which will replace the default handler. + * @param handler The callback, which will replace the default handler for + * this specific event/rejection subscription. + */ + public infix fun EventSubscription.handledAs( + handler: suspend (EventMessage) -> Unit + ) +} diff --git a/client/src/main/kotlin/io/spine/chords/client/DesktopClient.kt b/client/src/main/kotlin/io/spine/chords/client/DesktopClient.kt index 38cd34b5..98f34aa8 100644 --- a/client/src/main/kotlin/io/spine/chords/client/DesktopClient.kt +++ b/client/src/main/kotlin/io/spine/chords/client/DesktopClient.kt @@ -233,13 +233,15 @@ public class DesktopClient( fieldValue: Message ): EventSubscription { val eventReceival = CompletableFuture() + val futureEventSubscription = FutureEventSubscription(eventReceival) observeEvent( event = event, field = field, fieldValue = fieldValue) { evt -> eventReceival.complete(evt) + futureEventSubscription.onEvent?.invoke(evt) } - return FutureEventSubscription(eventReceival) + return futureEventSubscription } /** @@ -326,4 +328,6 @@ private class FutureEventSubscription( override suspend fun awaitEvent(): E { return withTimeout(ReactionTimeoutMillis) { future.await() } } + + override var onEvent: ((E) -> Unit)? = null } diff --git a/client/src/main/kotlin/io/spine/chords/client/form/CommandMessageForm.kt b/client/src/main/kotlin/io/spine/chords/client/form/CommandMessageForm.kt index 689bfad8..bbdd4fc1 100644 --- a/client/src/main/kotlin/io/spine/chords/client/form/CommandMessageForm.kt +++ b/client/src/main/kotlin/io/spine/chords/client/form/CommandMessageForm.kt @@ -34,13 +34,9 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import io.spine.base.CommandMessage -import io.spine.base.EventMessage -import io.spine.chords.client.CommandPostingError -import io.spine.chords.client.EventSubscription -import io.spine.chords.client.appshell.client +import io.spine.chords.client.CommandLifecycle +import io.spine.chords.client.post import io.spine.chords.core.ComponentProps -import io.spine.chords.core.appshell.app -import io.spine.chords.core.layout.MessageDialog.Companion.showMessage import io.spine.chords.proto.form.FormPartScope import io.spine.chords.proto.form.MessageForm import io.spine.chords.proto.form.MessageFormSetupBase @@ -301,10 +297,10 @@ public class CommandMessageForm : MessageForm() { /** * A function, which, given a command message that is about to be posted, - * should subscribe to a respective event that is expected to arrive in - * response to handling that command. + * should provide the [CommandLifecycle] object that defines how the + * command's outcomes should be handled. */ - public lateinit var eventSubscription: (C) -> EventSubscription + public lateinit var commandLifecycle: (C) -> CommandLifecycle /** * A state, which reports whether the form is currently in progress of @@ -341,10 +337,7 @@ public class CommandMessageForm : MessageForm() { override fun initialize() { super.initialize() - check(this::eventSubscription.isInitialized) { - "CommandMessageForm's `eventSubscription` property must " + - "be specified." - } + requireProperty(this::commandLifecycle.isInitialized, "commandOutcomeHandler") } @Composable @@ -365,14 +358,12 @@ public class CommandMessageForm : MessageForm() { * errors to be displayed. * - Posts the command that was validated and built. * - Awaits for an event that should arrive upon successful handling of - * the command, as defined by the [eventSubscription] + * the command, as defined by the [commandLifecycle] * constructor's parameters. * - If the event doesn't arrive in a predefined timeout that is considered * an adequate delay from user's perspective, this method * throws [TimeoutCancellationException]. * - * @param outcomeHandler Specifies the way that command outcome is - * handled, e.g. the way how unexpected outcomes are processed. * @return `true` if the command was successfully built without any * validation errors, and `false` if the command message could not be * successfully built from the currently entered data (validation errors @@ -383,9 +374,7 @@ public class CommandMessageForm : MessageForm() { * the [postCommand] invocation is still being handled (when [posting] is * still `true`). */ - public suspend fun postCommand( - outcomeHandler: CommandOutcomeHandler = DefaultOutcomeHandler() - ): Boolean { + public suspend fun postCommand(): Boolean { if (posting) { throw IllegalStateException("Cannot invoke `postCommand`, while" + "waiting for handling the previously posted command.") @@ -399,97 +388,13 @@ public class CommandMessageForm : MessageForm() { "CommandMessageForm's value should be not null since it was just " + "checked to be valid within postCommand." } - val subscription = eventSubscription(command) + + val commandOutcomeHandler = commandLifecycle(command) return try { posting = true - try { - app.client.command(command) - val event = subscription.awaitEvent() - outcomeHandler.onEvent(event) - true - } catch (e: CommandPostingError) { - outcomeHandler.onPostingError(e) - false - } - } catch ( - @Suppress( - // A timeout condition is handled by `outcomeHandler`. - "SwallowedException" - ) - e: TimeoutCancellationException - ) { - outcomeHandler.onTimeout(command) - false + command.post(commandOutcomeHandler) } finally { posting = false } } } - -/** - * An object, which is capable of handling different kinds of outcomes that can - * follow as a result of posting a command. - */ -public interface CommandOutcomeHandler { - - /** - * Invoked upon receiving an event that has - * the command [C]. - */ - public fun onEvent(event: EventMessage) - - /** - * Invoked if no event has been received in response to the command [C] - * in a reasonable period of time defined by the implementation. - */ - public suspend fun onTimeout(command: C) - - /** - * Invoked if an error has occurred during posting and acknowledging - * the command on the server. - * - * @param error The exception that signifies the error that has occurred. - * @see io.spine.chords.client.Client.command - */ - public suspend fun onPostingError(error: CommandPostingError) -} - -/** - * A default implementation for [CommandOutcomeHandler], which is expected to - * be suitable in most typical cases. - * - * @param eventHandler An optional callback, which will be invoked when - * an event that is expected as an outcome of the command [C] is emitted. - * @param timeoutMessage A lambda, which, given a command that was posted with - * no subsequent response received in time, should provide a message that - * should be displayed to the user in this case. If not specified, a default - * message is displayed. - * @param postingErrorMessage A lambda, which, given a [CommandPostingError] - * that has occurred when posting the command, returns the text that should - * be displayed to the user. - */ -public class DefaultOutcomeHandler( - private val eventHandler: ((EventMessage) -> Unit)? = null, - private val timeoutMessage: ((C) -> String)? = null, - private val postingErrorMessage: ((CommandPostingError) -> String)? = null -) : CommandOutcomeHandler { - override fun onEvent(event: EventMessage) { - eventHandler?.invoke(event) - } - - override suspend fun onTimeout(command: C) { - val message = timeoutMessage?.invoke(command) ?: ( - "Timed out waiting for an event in response to " + - "the command ${command.javaClass.simpleName}" - ) - showMessage(message) - } - - override suspend fun onPostingError(error: CommandPostingError) { - val message = postingErrorMessage?.invoke(error) ?: ( - "An error has occurred when posting or acknowledging " + - "the command ${error.message}" - ) - showMessage(message) - } -} diff --git a/client/src/main/kotlin/io/spine/chords/client/layout/CommandDialog.kt b/client/src/main/kotlin/io/spine/chords/client/layout/CommandDialog.kt index d86d995c..97061eda 100644 --- a/client/src/main/kotlin/io/spine/chords/client/layout/CommandDialog.kt +++ b/client/src/main/kotlin/io/spine/chords/client/layout/CommandDialog.kt @@ -33,8 +33,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment.Companion.CenterHorizontally import androidx.compose.ui.Modifier import io.spine.base.CommandMessage -import io.spine.base.EventMessage -import io.spine.chords.client.EventSubscription +import io.spine.chords.client.CommandLifecycle import io.spine.chords.client.form.CommandMessageForm import io.spine.chords.core.layout.Dialog import io.spine.chords.core.layout.SubmitOrCancelDialog @@ -70,7 +69,7 @@ public abstract class CommandDialog onBeforeBuild = ::beforeBuild, props = { validationDisplayMode = MANUAL - eventSubscription = ::subscribeToEvent + commandLifecycle = ::commandLifecycle } ) { Column( @@ -105,15 +104,13 @@ public abstract class CommandDialog /** * A function, which, given a command message that is about to be posted, - * should subscribe to a respective event that is expected to arrive in - * response to handling that command. + * should provide the [CommandLifecycle] object that defines how the + * command's outcomes should be handled. * * @param command A command, which is going to be posted. - * @return A subscription to the event that is expected to arrive in - * response to handling [command]. + * @return A respectively configured [CommandLifecycle] instance. */ - protected abstract fun subscribeToEvent(command: C): - EventSubscription + protected abstract fun commandLifecycle(command: C): CommandLifecycle /** * Allows to programmatically amend the command message builder before diff --git a/client/src/main/kotlin/io/spine/chords/client/layout/CommandWizard.kt b/client/src/main/kotlin/io/spine/chords/client/layout/CommandWizard.kt index 05d6ff43..18ece4fd 100644 --- a/client/src/main/kotlin/io/spine/chords/client/layout/CommandWizard.kt +++ b/client/src/main/kotlin/io/spine/chords/client/layout/CommandWizard.kt @@ -30,8 +30,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import com.google.protobuf.Message import io.spine.base.CommandMessage -import io.spine.base.EventMessage -import io.spine.chords.client.EventSubscription +import io.spine.chords.client.CommandLifecycle import io.spine.chords.client.form.CommandMessageForm import io.spine.chords.core.layout.AbstractWizardPage import io.spine.chords.core.layout.Wizard @@ -74,7 +73,7 @@ public abstract class CommandWizard + protected abstract fun commandLifecycle(command: C): CommandLifecycle /** * Allows to programmatically amend the command message builder before @@ -142,7 +139,7 @@ public abstract class CommandWizard - if (dialog.cancelAvailableInternal && event matches cancelShortcutKey.down) { - dialog.cancel() - } - if (dialog.submitAvailableInternal && event matches submitShortcutKey.up) { - dialog.submit() - } - false - } - ) { - Column( - modifier = Modifier - .fillMaxSize() - .background(colorScheme.background), - ) { - Column( - modifier = Modifier - .fillMaxSize() - .padding(dialog.look.padding), - ) { - dialog.windowContentInternal() - dialog.nestedDialog?.Content() - } - } - } - } -} - -/** - * A [PopupPositionProvider], which makes a lightweight popup to appear at - * the window's center. - */ -private val centerWindowPositionProvider = object : PopupPositionProvider { - override fun calculatePosition( - anchorBounds: IntRect, - windowSize: IntSize, - layoutDirection: LayoutDirection, - popupContentSize: IntSize - ): IntOffset = Zero -} - -/** - * A [DialogDisplayMode] implementation, which ensures displaying a dialog - * as a lightweight modal popup inside the current desktop window. - * - * @param backdropColor The color of the surface that covers the entire content - * of the current desktop window behind the dialog's modal popup displayed in - * this window. - */ -internal class LightweightDisplayMode( - private val backdropColor: Color = Black.copy(alpha = 0.5f) -) : DialogDisplayMode() { - - @Composable - override fun dialogWindow(dialog: Dialog) { - Popup( - popupPositionProvider = centerWindowPositionProvider, - properties = PopupProperties(focusable = true), - onPreviewKeyEvent = { false }, - onKeyEvent = cancelShortcutHandler { - if (dialog.cancelAvailableInternal) { - dialog.cancel() - } - } - ) { - Box( - modifier = Modifier - .fillMaxSize() - .background(backdropColor), - contentAlignment = Center - ) { - val modifier = if (dialog.isBottomDialog) { - Modifier.pointerInput(dialog) { - detectTapGestures(onPress = {}) - } - } else { - Modifier - } - Box(modifier = modifier) { - dialogFrame(dialog) - } - } - } - } - - @Composable - private fun dialogFrame(dialog: Dialog) { - Column( - modifier = Modifier - .clip(shapes.large) - .size(dialog.dialogWidth, dialog.dialogHeight) - .background(colorScheme.background), - ) { - Column( - modifier = Modifier - .fillMaxSize() - .padding(dialog.look.padding), - ) { - DialogTitle(dialog.title, dialog.look.titlePadding) - dialog.windowContentInternal() - dialog.nestedDialog ?.Content() - } - } - } - - /** - * Creates a key event handler function that executes a provided [cancelHandler] - * callback whenever the `Escape` key is pressed. - */ - private fun cancelShortcutHandler(cancelHandler: () -> Unit): (KeyEvent) -> Boolean = { event -> - if (event matches cancelShortcutKey.down) { - cancelHandler() - true - } else { - false - } - } -} /** * A class that should be used for creating companion objects of @@ -829,24 +674,6 @@ public open class DialogSetup( } } - -/** - * The title of the dialog. - * - * @param text The text to be title. - */ -@Composable -private fun DialogTitle( - text: String, - padding: PaddingValues -) { - Text( - modifier = Modifier.padding(padding), - text = text, - style = typography.headlineLarge - ) -} - /** * The action button of the dialog. * diff --git a/core/src/main/kotlin/io/spine/chords/core/layout/MessageDialog.kt b/core/src/main/kotlin/io/spine/chords/core/layout/MessageDialog.kt index 082f03f7..df6407a7 100644 --- a/core/src/main/kotlin/io/spine/chords/core/layout/MessageDialog.kt +++ b/core/src/main/kotlin/io/spine/chords/core/layout/MessageDialog.kt @@ -71,8 +71,8 @@ public class MessageDialog : Dialog() { init { submitAvailable = true - dialogWidth = 430.dp - dialogHeight = 210.dp + width = 430.dp + height = 210.dp } /** diff --git a/core/src/main/kotlin/io/spine/chords/core/layout/WindowType.kt b/core/src/main/kotlin/io/spine/chords/core/layout/WindowType.kt new file mode 100644 index 00000000..abb7ae3d --- /dev/null +++ b/core/src/main/kotlin/io/spine/chords/core/layout/WindowType.kt @@ -0,0 +1,289 @@ +/* + * Copyright 2024, TeamDev. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chords.core.layout + +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.MaterialTheme.colorScheme +import androidx.compose.material3.MaterialTheme.shapes +import androidx.compose.material3.MaterialTheme.typography +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment.Companion.Center +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Color.Companion.Gray +import androidx.compose.ui.input.key.KeyEvent +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntOffset.Companion.Zero +import androidx.compose.ui.unit.IntRect +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.window.DialogState +import androidx.compose.ui.window.DialogWindow +import androidx.compose.ui.window.Popup +import androidx.compose.ui.window.PopupPositionProvider +import androidx.compose.ui.window.PopupProperties +import io.spine.chords.core.keyboard.matches + +/** + * Defines the way that a dialog is displayed on the screen (e.g. as a separate + * desktop window, or as a lightweight modal popup). + */ +public sealed class WindowType { + + /** + * Renders the dialog window according to the display mode defined by this + * object. + * + * @param dialog The [Dialog] that is being displayed. + */ + @Composable + public abstract fun dialogWindow(dialog: Dialog) + + /** + * A [WindowType] implementation, which ensures displaying a dialog + * as a separate desktop window. + * + * @param resizable Specifies whether the window can be resized by the user. + */ + public open class DesktopWindow( + public val resizable: Boolean = false + ) : WindowType() { + + @Composable + override fun dialogWindow(dialog: Dialog) { + DialogWindow( + title = dialog.title, + resizable = resizable, + state = DialogState( + size = DpSize(dialog.width, dialog.height) + ), + onCloseRequest = { dialog.close() }, + onKeyEvent = { event -> + if (dialog.cancelAvailableInternal && event matches cancelShortcutKey.down) { + dialog.cancel() + } + if (dialog.submitAvailableInternal && event matches submitShortcutKey.up) { + dialog.submit() + } + false + } + ) { + Column( + modifier = Modifier + .fillMaxSize() + .background(colorScheme.background), + ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(dialog.look.padding), + ) { + dialog.windowContentInternal() + dialog.nestedDialog?.Content() + } + } + } + } + + /** + * A default reusable instance of [DesktopWindow], which can be used if + * no additional customizations are required. + * + * Here's an example of how it can be used: + * ``` + * Dialog { + * windowType = DesktopWindow + * ... + * } + * ``` + * + * If any customizations are required (e.g. if you need to make the + * window resizable), just create a new [DesktopWindow] instance with + * respective parameters, like this: + * ``` + * Dialog { + * windowType = DesktopWindow(resizable = true) + * ... + * } + * ``` + */ + public companion object : DesktopWindow( + resizable = false + ) + } + + /** + * A [WindowType] implementation, which ensures displaying a dialog + * as a lightweight modal popup inside the current desktop window. + * + * @param backdropColor The color of the surface that covers the entire + * content of the current desktop window behind the dialog's modal popup + * displayed in this window. + */ + public open class LightweightWindow( + public val backdropColor: Color = Gray.copy(alpha = 0.5f) + ) : WindowType() { + + @Composable + override fun dialogWindow(dialog: Dialog) { + Popup( + popupPositionProvider = centerWindowPositionProvider, + properties = PopupProperties(focusable = true), + onPreviewKeyEvent = { false }, + onKeyEvent = cancelShortcutHandler { + if (dialog.cancelAvailableInternal) { + dialog.cancel() + } + } + ) { + Box( + modifier = Modifier + .fillMaxSize() + .background(backdropColor), + contentAlignment = Center + ) { + val modifier = if (dialog.isBottomDialog) { + Modifier.pointerInput(dialog) { + detectTapGestures(onPress = {}) + } + } else { + Modifier + } + Box(modifier = modifier) { + dialogFrame(dialog) + } + } + } + } + + @Composable + private fun dialogFrame(dialog: Dialog) { + Column( + modifier = Modifier + .clip(shapes.large) + .size(dialog.width, dialog.height) + .background(colorScheme.background), + ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(dialog.look.padding), + ) { + DialogTitle(dialog.title, dialog.look.titlePadding) + dialog.windowContentInternal() + dialog.nestedDialog ?.Content() + } + } + } + + /** + * Creates a key event handler function that executes a provided + * [cancelHandler] callback whenever the `Escape` key is pressed. + */ + private fun cancelShortcutHandler( + cancelHandler: () -> Unit + ): (KeyEvent) -> Boolean = { event -> + if (event matches cancelShortcutKey.down) { + cancelHandler() + true + } else { + false + } + } + + /** + * A default reusable instance of [LightweightWindow], which can be used if + * no additional customizations are required. + * + * Here's an example of how it can be used: + * ``` + * Dialog { + * windowType = LightweightWindow + * ... + * } + * ``` + * + * If any customizations are required (e.g. if you need to change the + * backdrop color), just create a new [LightweightWindow] instance with + * respective parameters, like this: + * ``` + * Dialog { + * windowType = LightweightWindow( + * backdropColor = White.copy(alpha = 0.5f) + * ) + * ... + * } + * ``` + */ + public companion object : LightweightWindow() + + } +} + +/** + * A [PopupPositionProvider], which makes a lightweight popup to appear at + * the window's center. + * + * @see WindowType.LightweightWindow + */ +private val centerWindowPositionProvider = object : PopupPositionProvider { + override fun calculatePosition( + anchorBounds: IntRect, + windowSize: IntSize, + layoutDirection: LayoutDirection, + popupContentSize: IntSize + ): IntOffset = Zero +} + +/** + * The title of the dialog for lightweight windows. + * + * @param text The text to be displayed as the window's title. + * @see WindowType.LightweightWindow + */ +@Composable +private fun DialogTitle( + text: String, + padding: PaddingValues +) { + Text( + modifier = Modifier.padding(padding), + text = text, + style = typography.headlineLarge + ) +} diff --git a/core/src/main/kotlin/io/spine/chords/core/layout/Wizard.kt b/core/src/main/kotlin/io/spine/chords/core/layout/Wizard.kt index 06d039a1..64859094 100644 --- a/core/src/main/kotlin/io/spine/chords/core/layout/Wizard.kt +++ b/core/src/main/kotlin/io/spine/chords/core/layout/Wizard.kt @@ -85,7 +85,7 @@ private object WizardContentSize { } /** - * The base class for creating multi-step form component known as wizard. + * The base class for creating a multi-step form component known as a wizard. * * To create a concrete wizard you need to extend the class * and override all abstract methods that configure the data needed for the wizard. @@ -99,9 +99,10 @@ private object WizardContentSize { public abstract class Wizard : Component() { /** - * The text to be the title of the wizard. + * The text to be the title of the wizard, or `null`, if the wizard's title + * shouldn't be displayed at all. */ - protected abstract val title: String + protected abstract val title: String? /** * A callback that should be handled to close the wizard (exclude it from @@ -127,7 +128,11 @@ public abstract class Wizard : Component() { private var submitting: Boolean by mutableStateOf(false) private var currentPageIndex by mutableStateOf(0) - private val pages by lazy { createPages() } + + /** + * A list of pages present in the wizard. + */ + protected val pages: List by lazy { createPages() } /** * Creates the list of pages of which the wizard consists. @@ -165,7 +170,9 @@ public abstract class Wizard : Component() { .padding(32.dp), verticalArrangement = spacedBy(16.dp) ) { - Title(title) + if (title != null) { + Title(title!!) + } Column( Modifier .weight(1F) @@ -289,19 +296,20 @@ private fun Title(text: String) { /** * The panel with control buttons of the wizard. * - * @param onNextClick - * a callback triggered when the user clicks on the "Next" button. - * @param onBackClick - * a callback triggered when the user clicks on the "Back" button. - * @param onFinishClick - * a callback triggered when the user clicks on the "Finish" button. - * this callback triggers in a separate coroutine. - * @param onCancelClick - * a callback triggered when the user clicks on the "Cancel" button. - * @param isOnFirstPage - * is a wizard's currently displayed page the first one. - * @param isOnLastPage - * is a wizard's currently displayed page the last one. + * @param onNextClick A callback triggered when the user clicks on + * the "Next" button. + * @param onBackClick A callback triggered when the user clicks on + * the "Back" button. + * @param onFinishClick A callback triggered when the user clicks on + * the "Finish" button. This callback is triggered in a separate coroutine. + * @param onCancelClick A callback triggered when the user clicks on + * the "Cancel" button. + * @param isOnFirstPage Specifies whether the wizard's currently displayed page + * is the first one. + * @param isOnLastPage Specifies whether the wizard's currently displayed page + * is the last one. + * @param submitting Specifies whether wizard's submission is currently + * in progress. */ @Composable private fun NavigationPanel( diff --git a/dependencies.md b/dependencies.md index aae340d8..b466dffb 100644 --- a/dependencies.md +++ b/dependencies.md @@ -1,6 +1,6 @@ -# Dependencies of `io.spine.chords:spine-chords-client:2.0.0-SNAPSHOT.55` +# Dependencies of `io.spine.chords:spine-chords-client:2.0.0-SNAPSHOT.56` ## Runtime 1. **Group** : com.google.android. **Name** : annotations. **Version** : 4.1.1.4. @@ -111,7 +111,7 @@ * **Project URL:** [https://github.com/JetBrains/compose-jb](https://github.com/JetBrains/compose-jb) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.compose.desktop. **Name** : desktop-jvm-macos-x64. **Version** : 1.5.12.**No license information found** +1. **Group** : org.jetbrains.compose.desktop. **Name** : desktop-jvm-windows-x64. **Version** : 1.5.12.**No license information found** 1. **Group** : org.jetbrains.compose.foundation. **Name** : foundation. **Version** : 1.5.12.**No license information found** 1. **Group** : org.jetbrains.compose.foundation. **Name** : foundation-desktop. **Version** : 1.5.12. * **Project URL:** [https://github.com/JetBrains/compose-jb](https://github.com/JetBrains/compose-jb) @@ -230,7 +230,7 @@ * **Project URL:** [https://www.github.com/JetBrains/skiko](https://www.github.com/JetBrains/skiko) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.skiko. **Name** : skiko-awt-runtime-macos-x64. **Version** : 0.7.85.4. +1. **Group** : org.jetbrains.skiko. **Name** : skiko-awt-runtime-windows-x64. **Version** : 0.7.85.4. * **Project URL:** [https://www.github.com/JetBrains/skiko](https://www.github.com/JetBrains/skiko) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -750,7 +750,7 @@ * **Project URL:** [https://github.com/JetBrains/compose-jb](https://github.com/JetBrains/compose-jb) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.compose.desktop. **Name** : desktop-jvm-macos-x64. **Version** : 1.5.12.**No license information found** +1. **Group** : org.jetbrains.compose.desktop. **Name** : desktop-jvm-windows-x64. **Version** : 1.5.12.**No license information found** 1. **Group** : org.jetbrains.compose.foundation. **Name** : foundation. **Version** : 1.5.12.**No license information found** 1. **Group** : org.jetbrains.compose.foundation. **Name** : foundation-desktop. **Version** : 1.5.12. * **Project URL:** [https://github.com/JetBrains/compose-jb](https://github.com/JetBrains/compose-jb) @@ -992,7 +992,7 @@ * **Project URL:** [https://www.github.com/JetBrains/skiko](https://www.github.com/JetBrains/skiko) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.skiko. **Name** : skiko-awt-runtime-macos-x64. **Version** : 0.7.85.4. +1. **Group** : org.jetbrains.skiko. **Name** : skiko-awt-runtime-windows-x64. **Version** : 0.7.85.4. * **Project URL:** [https://www.github.com/JetBrains/skiko](https://www.github.com/JetBrains/skiko) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -1066,12 +1066,12 @@ The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Dec 17 11:36:27 CET 2024** 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 **Tue Dec 17 13:46:59 EET 2024** 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.55` +# Dependencies of `io.spine.chords:spine-chords-codegen-tests:2.0.0-SNAPSHOT.56` ## Runtime 1. **Group** : com.google.android. **Name** : annotations. **Version** : 4.1.1.4. @@ -1925,12 +1925,12 @@ This report was generated on **Tue Dec 17 11:36:27 CET 2024** using [Gradle-Lice The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Dec 17 11:36:29 CET 2024** 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 **Tue Dec 17 13:47:01 EET 2024** 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.55` +# Dependencies of `io.spine.chords:spine-chords-core:2.0.0-SNAPSHOT.56` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -1992,7 +1992,7 @@ This report was generated on **Tue Dec 17 11:36:29 CET 2024** using [Gradle-Lice * **Project URL:** [https://github.com/JetBrains/compose-jb](https://github.com/JetBrains/compose-jb) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.compose.desktop. **Name** : desktop-jvm-macos-x64. **Version** : 1.5.12.**No license information found** +1. **Group** : org.jetbrains.compose.desktop. **Name** : desktop-jvm-windows-x64. **Version** : 1.5.12.**No license information found** 1. **Group** : org.jetbrains.compose.foundation. **Name** : foundation. **Version** : 1.5.12.**No license information found** 1. **Group** : org.jetbrains.compose.foundation. **Name** : foundation-desktop. **Version** : 1.5.12. * **Project URL:** [https://github.com/JetBrains/compose-jb](https://github.com/JetBrains/compose-jb) @@ -2107,7 +2107,7 @@ This report was generated on **Tue Dec 17 11:36:29 CET 2024** using [Gradle-Lice * **Project URL:** [https://www.github.com/JetBrains/skiko](https://www.github.com/JetBrains/skiko) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.skiko. **Name** : skiko-awt-runtime-macos-x64. **Version** : 0.7.85.4. +1. **Group** : org.jetbrains.skiko. **Name** : skiko-awt-runtime-windows-x64. **Version** : 0.7.85.4. * **Project URL:** [https://www.github.com/JetBrains/skiko](https://www.github.com/JetBrains/skiko) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -2596,7 +2596,7 @@ This report was generated on **Tue Dec 17 11:36:29 CET 2024** using [Gradle-Lice * **Project URL:** [https://github.com/JetBrains/compose-jb](https://github.com/JetBrains/compose-jb) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.compose.desktop. **Name** : desktop-jvm-macos-x64. **Version** : 1.5.12.**No license information found** +1. **Group** : org.jetbrains.compose.desktop. **Name** : desktop-jvm-windows-x64. **Version** : 1.5.12.**No license information found** 1. **Group** : org.jetbrains.compose.foundation. **Name** : foundation. **Version** : 1.5.12.**No license information found** 1. **Group** : org.jetbrains.compose.foundation. **Name** : foundation-desktop. **Version** : 1.5.12. * **Project URL:** [https://github.com/JetBrains/compose-jb](https://github.com/JetBrains/compose-jb) @@ -2838,7 +2838,7 @@ This report was generated on **Tue Dec 17 11:36:29 CET 2024** using [Gradle-Lice * **Project URL:** [https://www.github.com/JetBrains/skiko](https://www.github.com/JetBrains/skiko) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.skiko. **Name** : skiko-awt-runtime-macos-x64. **Version** : 0.7.85.4. +1. **Group** : org.jetbrains.skiko. **Name** : skiko-awt-runtime-windows-x64. **Version** : 0.7.85.4. * **Project URL:** [https://www.github.com/JetBrains/skiko](https://www.github.com/JetBrains/skiko) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -2912,12 +2912,12 @@ This report was generated on **Tue Dec 17 11:36:29 CET 2024** using [Gradle-Lice The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Dec 17 11:36:30 CET 2024** 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 **Tue Dec 17 13:47:03 EET 2024** 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.55` +# Dependencies of `io.spine.chords:spine-chords-proto:2.0.0-SNAPSHOT.56` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -2996,7 +2996,7 @@ This report was generated on **Tue Dec 17 11:36:30 CET 2024** using [Gradle-Lice * **Project URL:** [https://github.com/JetBrains/compose-jb](https://github.com/JetBrains/compose-jb) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.compose.desktop. **Name** : desktop-jvm-macos-x64. **Version** : 1.5.12.**No license information found** +1. **Group** : org.jetbrains.compose.desktop. **Name** : desktop-jvm-windows-x64. **Version** : 1.5.12.**No license information found** 1. **Group** : org.jetbrains.compose.foundation. **Name** : foundation. **Version** : 1.5.12.**No license information found** 1. **Group** : org.jetbrains.compose.foundation. **Name** : foundation-desktop. **Version** : 1.5.12. * **Project URL:** [https://github.com/JetBrains/compose-jb](https://github.com/JetBrains/compose-jb) @@ -3111,7 +3111,7 @@ This report was generated on **Tue Dec 17 11:36:30 CET 2024** using [Gradle-Lice * **Project URL:** [https://www.github.com/JetBrains/skiko](https://www.github.com/JetBrains/skiko) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.skiko. **Name** : skiko-awt-runtime-macos-x64. **Version** : 0.7.85.4. +1. **Group** : org.jetbrains.skiko. **Name** : skiko-awt-runtime-windows-x64. **Version** : 0.7.85.4. * **Project URL:** [https://www.github.com/JetBrains/skiko](https://www.github.com/JetBrains/skiko) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -3600,7 +3600,7 @@ This report was generated on **Tue Dec 17 11:36:30 CET 2024** using [Gradle-Lice * **Project URL:** [https://github.com/JetBrains/compose-jb](https://github.com/JetBrains/compose-jb) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.compose.desktop. **Name** : desktop-jvm-macos-x64. **Version** : 1.5.12.**No license information found** +1. **Group** : org.jetbrains.compose.desktop. **Name** : desktop-jvm-windows-x64. **Version** : 1.5.12.**No license information found** 1. **Group** : org.jetbrains.compose.foundation. **Name** : foundation. **Version** : 1.5.12.**No license information found** 1. **Group** : org.jetbrains.compose.foundation. **Name** : foundation-desktop. **Version** : 1.5.12. * **Project URL:** [https://github.com/JetBrains/compose-jb](https://github.com/JetBrains/compose-jb) @@ -3842,7 +3842,7 @@ This report was generated on **Tue Dec 17 11:36:30 CET 2024** using [Gradle-Lice * **Project URL:** [https://www.github.com/JetBrains/skiko](https://www.github.com/JetBrains/skiko) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.skiko. **Name** : skiko-awt-runtime-macos-x64. **Version** : 0.7.85.4. +1. **Group** : org.jetbrains.skiko. **Name** : skiko-awt-runtime-windows-x64. **Version** : 0.7.85.4. * **Project URL:** [https://www.github.com/JetBrains/skiko](https://www.github.com/JetBrains/skiko) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -3916,12 +3916,12 @@ This report was generated on **Tue Dec 17 11:36:30 CET 2024** using [Gradle-Lice The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Dec 17 11:36:31 CET 2024** 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 **Tue Dec 17 13:47:06 EET 2024** 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.55` +# Dependencies of `io.spine.chords:spine-chords-proto-values:2.0.0-SNAPSHOT.56` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -4715,12 +4715,12 @@ This report was generated on **Tue Dec 17 11:36:31 CET 2024** using [Gradle-Lice The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Dec 17 11:36:32 CET 2024** 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 **Tue Dec 17 13:47:08 EET 2024** 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.55` +# Dependencies of `io.spine.chords:spine-chords-runtime:2.0.0-SNAPSHOT.56` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -5484,4 +5484,4 @@ This report was generated on **Tue Dec 17 11:36:32 CET 2024** using [Gradle-Lice The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Tue Dec 17 11:36:33 CET 2024** 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 **Tue Dec 17 13:47:09 EET 2024** 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 362dcf5a..ed9d0f17 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.55 +2.0.0-SNAPSHOT.56 2015 @@ -73,7 +73,7 @@ all modules and does not describe the project structure per-subproject. org.jetbrains.compose.desktop - desktop-jvm-macos-x64 + desktop-jvm-windows-x64 1.5.12 compile diff --git a/version.gradle.kts b/version.gradle.kts index 7d098eba..65d5d0b5 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.55") +val chordsVersion: String by extra("2.0.0-SNAPSHOT.56")