From a50693a2a5a52de15a50529f02675dbf83406968 Mon Sep 17 00:00:00 2001 From: MineKing Date: Fri, 20 Feb 2026 22:16:48 +0100 Subject: [PATCH] Add support for new checkbox modal components --- Readme.md | 10 +- .../src/main/kotlin/examples/CheckboxModal.kt | 39 ++++++ gradle/libs.versions.toml | 2 +- .../ui/builder/components/modal/Checkbox.kt | 128 ++++++++++++++++++ 4 files changed, 175 insertions(+), 4 deletions(-) create mode 100644 examples/src/main/kotlin/examples/CheckboxModal.kt create mode 100644 src/main/kotlin/de/mineking/discord/ui/builder/components/modal/Checkbox.kt diff --git a/Readme.md b/Readme.md index 9143366..35544e2 100644 --- a/Readme.md +++ b/Readme.md @@ -541,8 +541,8 @@ registerMenu("menu", useComponentsV2 = true) { +container(color = 0x00FF00) { +section( modalButton( - "text", label = "Modal", title = "Enter Text", component = - textInput("text", placeholder = "Hello World!", value = text).withLabel("Enter Text") + "text", label = "Modal", title = "Enter Text", + component = textInput("text", placeholder = "Hello World!", value = text).withLabel("Enter Text") ) { text = it } @@ -772,11 +772,15 @@ registerModal("modal") { > You always have to wrap components in a `label` to be able to add them to a modal menu. There is also the `withLabel` extension function that makes the component creation more readable. > [!NOTE] -> Discord currently only allows `textInput`s, `stringSelect`s and `entitySelect`s inside modals. For select menus, their main handler (and option handlers) are NOT executed. Instead, the `modalHandler` is used. +> Discord currently only allows `textInput`s, `stringSelect`s, `entitySelect`s and checkbox components inside modals. For select menus, their main handler (and option handlers) are NOT executed. Instead, the `modalHandler` is used. You can create text inputs with the `textInput` function. They can be added to your modal with the `+` operator. Adding a text input will return a function that can be used inside the executor block to read the inputs value. The `execute` block is what is executed when a user submits a modal. +In addition to `textInput` there are also the `checkbox`, `checkboxGroup` and `radioGroup` components, that are exclusive to modals. They also have factory functions as usual. +While discord itself does not support required checkboxes, discord tool kit provides a `requiredCheckbox` function, that creates a required checkbox group with a single option under the hood. +This is useful for additional confirmation steps, since users cannot submit the modal unless they check the box. + If you only want to use a modal to read a text input from the user in a message menu, you can use `modalButton` (or `modalSelectOption`): ```kt registerMenu("menu", deferMode = DeferMode.UNLESS_PREVENTED) { diff --git a/examples/src/main/kotlin/examples/CheckboxModal.kt b/examples/src/main/kotlin/examples/CheckboxModal.kt new file mode 100644 index 0000000..1078925 --- /dev/null +++ b/examples/src/main/kotlin/examples/CheckboxModal.kt @@ -0,0 +1,39 @@ +package examples + +import de.mineking.discord.commands.menuCommand +import de.mineking.discord.discordToolKit +import de.mineking.discord.ui.builder.components.message.actionRow +import de.mineking.discord.ui.builder.components.message.button +import de.mineking.discord.ui.builder.components.modal.checkboxGroupOption +import de.mineking.discord.ui.builder.components.modal.radioGroup +import de.mineking.discord.ui.builder.components.modal.requiredCheckbox +import de.mineking.discord.ui.builder.components.modal.withLabel +import de.mineking.discord.ui.message.modal +import de.mineking.discord.ui.modal.getValue +import setup.createJDA + +fun main() { + val jda = createJDA() + discordToolKit(jda) + .withUIManager() + .withCommandManager { + +menuCommand("checkbox") { + val modal = modal("checkbox") { + val selected by +radioGroup("test", checkboxGroupOption("a"), checkboxGroupOption("b")).withLabel("Radio Group") + +requiredCheckbox("require").withLabel("I confirm that this action cannot be undone") + + execute { + reply("Selected: $selected").setEphemeral(true).queue() + } + } + + +actionRow( + button("open", label = "open") { + switchMenu(modal) + } + ) + } + + updateCommands().queue() + }.build() +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b1a394c..8237907 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,7 +4,7 @@ kotlin-serialization = "1.9.0" kotlin-coroutines = "1.10.2" dtk-localization = "1.1.0" -jda = "6.3.1" +jda = "6.4.1" emoji = "2.3.0" logging = "2.0.11" diff --git a/src/main/kotlin/de/mineking/discord/ui/builder/components/modal/Checkbox.kt b/src/main/kotlin/de/mineking/discord/ui/builder/components/modal/Checkbox.kt new file mode 100644 index 0000000..f5de550 --- /dev/null +++ b/src/main/kotlin/de/mineking/discord/ui/builder/components/modal/Checkbox.kt @@ -0,0 +1,128 @@ +package de.mineking.discord.ui.builder.components.modal + +import de.mineking.discord.localization.DEFAULT_LABEL +import de.mineking.discord.localization.LocalizationFile +import de.mineking.discord.ui.MenuConfig +import de.mineking.discord.ui.modal.ModalResultHandler +import de.mineking.discord.ui.modal.createModalElement +import de.mineking.discord.ui.modal.map +import de.mineking.discord.ui.readLocalizedString +import net.dv8tion.jda.api.EmbedBuilder.ZERO_WIDTH_SPACE +import net.dv8tion.jda.api.components.checkbox.Checkbox +import net.dv8tion.jda.api.components.checkboxgroup.CheckboxGroup +import net.dv8tion.jda.api.components.radiogroup.RadioGroup +import kotlin.math.absoluteValue +import net.dv8tion.jda.api.components.checkboxgroup.CheckboxGroupOption as JDACheckboxGroupOption +import net.dv8tion.jda.api.components.radiogroup.RadioGroupOption as JDARadioGroupOption + +fun checkbox( + name: String, + default: Boolean = false, + handler: ModalResultHandler? = null +) = createModalElement(name, { + val temp = event.getValueByUniqueId(name.hashCode().absoluteValue)!!.asBoolean + temp.also { handler?.invoke(this, it) } +}) { _, id -> + Checkbox.of(id, default) + .withUniqueId(name.hashCode().absoluteValue) +} + +class CheckboxGroupOption( + val value: String, + val label: CharSequence, + val description: CharSequence?, + val default: Boolean, + val localization: LocalizationFile?, + val visible: Boolean, +) { + fun buildCheckboxOption(name: String, localization: LocalizationFile?, config: MenuConfig<*, *>) = + JDACheckboxGroupOption.of(config.readLocalizedString(this.localization ?: localization, name, label, "label", postfix = "options.$value") ?: ZERO_WIDTH_SPACE, value) + .withDescription(config.readLocalizedString(this.localization ?: localization, name, description, "description", postfix = "options.$value")) + .withDefault(default) + .withValue(value) + + fun buildRadioOption(name: String, localization: LocalizationFile?, config: MenuConfig<*, *>) = + JDARadioGroupOption.of(config.readLocalizedString(this.localization ?: localization, name, label, "label", postfix = "options.$value") ?: ZERO_WIDTH_SPACE, value) + .withDescription(config.readLocalizedString(this.localization ?: localization, name, description, "description", postfix = "options.$value")) + .withDefault(default) + .withValue(value) +} + +fun CheckboxGroupOption.visibleIf(visible: Boolean) = CheckboxGroupOption(value, label, description, default, localization, visible) +fun CheckboxGroupOption.hiddenIf(hide: Boolean) = visibleIf(!hide) + +fun checkboxGroupOption( + value: Any, + label: CharSequence = DEFAULT_LABEL, + description: CharSequence? = DEFAULT_LABEL, + default: Boolean = false, + localization: LocalizationFile? = null, +) = CheckboxGroupOption(value.toString(), label, description, default, localization, true) + +fun checkboxGroup( + name: String, + options: List, + required: Boolean = false, + minValues: Int? = null, + maxValues: Int? = null, + localization: LocalizationFile? = null, + handler: ModalResultHandler>? = null, +) = createModalElement(name, { + val temp = event.getValueByUniqueId(name.hashCode().absoluteValue)!!.asStringList + temp.also { handler?.invoke(this, it) } +}) { config, id -> + CheckboxGroup.create(id) + .setUniqueId(name.hashCode().absoluteValue) + .setRequired(required) + .addOptions(options.filter { it.visible }.map { it.buildCheckboxOption(name, localization, config) }) + .apply { + if (minValues != null) setMinValues(minValues) + if (maxValues != null) setMaxValues(maxValues) + } + .build() +} + +fun checkboxGroup( + name: String, + vararg options: CheckboxGroupOption, + required: Boolean = false, + minValues: Int? = null, + maxValues: Int? = null, + localization: LocalizationFile? = null, + handler: ModalResultHandler>? = null, +) = checkboxGroup(name, options.toList(), required, minValues, maxValues, localization, handler) + +fun radioGroup( + name: String, + options: List, + required: Boolean = false, + localization: LocalizationFile? = null, + handler: ModalResultHandler? = null, +) = createModalElement(name, { + val temp = event.getValueByUniqueId(name.hashCode().absoluteValue)!!.asString + temp.also { handler?.invoke(this, it) } +}) { config, id -> + RadioGroup.create(id) + .setUniqueId(name.hashCode().absoluteValue) + .setRequired(required) + .addOptions(options.filter { it.visible }.map { it.buildRadioOption(name, localization, config) }) + .build() +} + +fun radioGroup( + name: String, + vararg options: CheckboxGroupOption, + required: Boolean = false, + localization: LocalizationFile? = null, + handler: ModalResultHandler? = null, +) = radioGroup(name, options.toList(), required, localization, handler) + +fun requiredCheckbox( + name: String, + default: Boolean = false, +) = checkboxGroup( + name, + checkboxGroupOption("value", default = default), + required = true, + minValues = 1, +).map { "value" in it }