From 820c3e075566b30dc2c6cd4a74e322a185e5e6ac Mon Sep 17 00:00:00 2001 From: IceTank <61137113+IceTank@users.noreply.github.com> Date: Wed, 4 Feb 2026 18:00:34 +0100 Subject: [PATCH 1/2] draft: List selection popup Model --- .../settings/collections/CollectionSetting.kt | 159 ++++++++++++++++-- .../module/modules/client/SelectionTest.kt | 30 ++++ 2 files changed, 175 insertions(+), 14 deletions(-) create mode 100644 src/main/kotlin/com/lambda/module/modules/client/SelectionTest.kt diff --git a/src/main/kotlin/com/lambda/config/settings/collections/CollectionSetting.kt b/src/main/kotlin/com/lambda/config/settings/collections/CollectionSetting.kt index b092b38ee..7f368ee88 100644 --- a/src/main/kotlin/com/lambda/config/settings/collections/CollectionSetting.kt +++ b/src/main/kotlin/com/lambda/config/settings/collections/CollectionSetting.kt @@ -27,10 +27,13 @@ import com.lambda.config.SettingGroupEditor import com.lambda.context.SafeContext import com.lambda.gui.dsl.ImGuiBuilder import com.lambda.threading.runSafe +import imgui.ImGui import imgui.ImGuiListClipper import imgui.callback.ImListClipperCallback import imgui.flag.ImGuiChildFlags import imgui.flag.ImGuiSelectableFlags.DontClosePopups +import imgui.flag.ImGuiWindowFlags +import imgui.type.ImBoolean import java.lang.reflect.Type /** @@ -52,15 +55,143 @@ open class CollectionSetting( defaultValue, type ) { - private var searchFilter = "" - private val strListType = - TypeToken.getParameterized(Collection::class.java, String::class.java).type + private var searchFilter = "" + private val strListType = + TypeToken.getParameterized(Collection::class.java, String::class.java).type - val selectListeners = mutableListOf Unit>() - val deselectListeners = mutableListOf Unit>() + val selectListeners = mutableListOf Unit>() + val deselectListeners = mutableListOf Unit>() + + val modelSelected = mutableSetOf() context(setting: Setting<*, MutableCollection>) - override fun ImGuiBuilder.buildLayout() = buildComboBox("item") { it.toString() } + override fun ImGuiBuilder.buildLayout() { + val popupName = "##${setting.name}-CollectionSettingPopup" + + val childWidth = 350f + val childHeight = 500f + val buttonColumnWidth = 60f + val totalWidth = (childWidth + 10f) * 2 + buttonColumnWidth + 20f + val totalHeight = childHeight + 190f + + if (ImGui.button("Edit List##${setting.name}-Edit")) { + ImGui.openPopup(popupName) + } + + ImGui.setNextWindowSize(totalWidth, totalHeight) + val pOpen = ImBoolean(true) + if (ImGui.beginPopupModal(popupName, pOpen, ImGuiWindowFlags.NoResize)) { + val availableItems = immutableCollection.filter { it !in value } + val selectedItems = value.toList() + + inputText("##${setting.name}-SearchBox", ::searchFilter) + sameLine() + button("X##${setting.name}-ClearSearch") { + searchFilter = "" + } + sameLine() + button("Select All##${setting.name}-SelectAll") { + immutableCollection.filter { item -> + val q = searchFilter.trim() + if (q.isEmpty()) true + else item.toString().contains(q, ignoreCase = true) + }.forEach { item -> + modelSelected.add(item) + } + } + + ImGui.columns(3, "##${setting.name}-Columns", false) + ImGui.setColumnWidth(0, childWidth + 10f) + ImGui.setColumnWidth(1, buttonColumnWidth) + + // Linke Seite: Verfügbare Elemente + ImGui.text("Values") + child("##${setting.name}-AvailableChild", childWidth, childHeight, ImGuiChildFlags.Border) { + availableItems.filter { item -> + val q = searchFilter.trim() + if (q.isEmpty()) true + else item.toString().contains(q, ignoreCase = true) + }.forEach { item -> + val selected = item in modelSelected + selectable("$item##available", selected) { + modelSelected.add(item) + } + } + } + + ImGui.nextColumn() + + // Mittlere Spalte: Buttons + ImGui.dummy(0f, 300f) + val columnWidth = ImGui.getColumnWidth() + val buttonWidth = 40f + ImGui.setCursorPosX(ImGui.getCursorPosX() + (columnWidth - buttonWidth) / 2f) + if (ImGui.button(">>", buttonWidth, 0f)) { + modelSelected + .filter { item -> + val q = searchFilter.trim() + if (q.isEmpty()) true + else item.toString().contains(q, ignoreCase = true) + } + .forEach { item -> + if (item in value) return@forEach + value.add(item) + runSafe { selectListeners.forEach { listener -> listener(item) } } + } + modelSelected.clear() + } + ImGui.setCursorPosX(ImGui.getCursorPosX() + (columnWidth - buttonWidth) / 2f) + if (ImGui.button("<<", buttonWidth, 0f)) { + modelSelected + .filter { item -> + val q = searchFilter.trim() + if (q.isEmpty()) true + else item.toString().contains(q, ignoreCase = true) + } + .forEach { item -> + if (item !in value) return@forEach + value.remove(item) + runSafe { deselectListeners.forEach { listener -> listener(item) } } + } + modelSelected.clear() + } + + ImGui.nextColumn() + + // Rechte Seite: Ausgewählte Elemente + ImGui.text("Selected") + child("##${setting.name}-SelectedChild", childWidth, childHeight, ImGuiChildFlags.Border) { + selectedItems.filter { item -> + val q = searchFilter.trim() + if (q.isEmpty()) true + else item.toString().contains(q, ignoreCase = true) + }.forEach { item -> + val selected = item in modelSelected + if (ImGui.selectable("$item##selected", selected)) { + modelSelected.add(item) + } + } + } + + ImGui.columns(1) + + ImGui.separator() + val closeButtonWidth = 100f + val windowWidth = ImGui.getWindowWidth() + ImGui.setCursorPosX((windowWidth - closeButtonWidth) / 2f) + if (ImGui.button("Save", closeButtonWidth, 0f)) { + modelSelected.clear() + ImGui.closeCurrentPopup() + } + + ImGui.endPopup() + } + if (!pOpen.get()) { + modelSelected.clear() + } + + buildComboBox("item") { it.toString() } + } context(setting: Setting<*, MutableCollection>) fun ImGuiBuilder.buildComboBox(itemName: String, toString: (R) -> String) { @@ -106,11 +237,11 @@ open class CollectionSetting( } context(setting: Setting<*, MutableCollection>) - override fun toJson(): JsonElement = + override fun toJson(): JsonElement = gson.toJsonTree(value, type) context(setting: Setting<*, MutableCollection>) - override fun loadFromJson(serialized: JsonElement) { + override fun loadFromJson(serialized: JsonElement) { val strList = if (serialize) gson.fromJson(serialized, type) else gson.fromJson>(serialized, strListType) @@ -129,10 +260,10 @@ open class CollectionSetting( core.deselectListeners.add(block) } - @SettingEditorDsl - @Suppress("unchecked_cast") - fun SettingGroupEditor.TypedEditBuilder>.immutableCollection(collection: Collection) { - (settings as Collection>).forEach { it.immutableCollection = collection } - } - } + @SettingEditorDsl + @Suppress("unchecked_cast") + fun SettingGroupEditor.TypedEditBuilder>.immutableCollection(collection: Collection) { + (settings as Collection>).forEach { it.immutableCollection = collection } + } + } } diff --git a/src/main/kotlin/com/lambda/module/modules/client/SelectionTest.kt b/src/main/kotlin/com/lambda/module/modules/client/SelectionTest.kt new file mode 100644 index 000000000..1e4c774ee --- /dev/null +++ b/src/main/kotlin/com/lambda/module/modules/client/SelectionTest.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2026 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.module.modules.client + +import com.lambda.module.Module +import com.lambda.module.tag.ModuleTag + +class SelectionTest : Module( + name="SelectionTest", + description="", + tag = ModuleTag.CLIENT +) { + + var selection1 by setting("Selection 1", mutableListOf(), mutableListOf("String 1", "String 2", "String 3")) +} \ No newline at end of file From 9424866320440633155f95271c4d07279e93d165 Mon Sep 17 00:00:00 2001 From: IceTank <61137113+IceTank@users.noreply.github.com> Date: Wed, 4 Feb 2026 18:07:53 +0100 Subject: [PATCH 2/2] give popup model window setting name --- .../com/lambda/config/settings/collections/CollectionSetting.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/com/lambda/config/settings/collections/CollectionSetting.kt b/src/main/kotlin/com/lambda/config/settings/collections/CollectionSetting.kt index 7f368ee88..fe5d18a0b 100644 --- a/src/main/kotlin/com/lambda/config/settings/collections/CollectionSetting.kt +++ b/src/main/kotlin/com/lambda/config/settings/collections/CollectionSetting.kt @@ -66,7 +66,7 @@ open class CollectionSetting( context(setting: Setting<*, MutableCollection>) override fun ImGuiBuilder.buildLayout() { - val popupName = "##${setting.name}-CollectionSettingPopup" + val popupName = "${setting.name}##${setting.name}-CollectionSettingPopup" val childWidth = 350f val childHeight = 500f