diff --git a/src/main/kotlin/com/lambda/gui/MenuBar.kt b/src/main/kotlin/com/lambda/gui/MenuBar.kt index 4aea2644e..8ab49202c 100644 --- a/src/main/kotlin/com/lambda/gui/MenuBar.kt +++ b/src/main/kotlin/com/lambda/gui/MenuBar.kt @@ -26,6 +26,7 @@ import com.lambda.core.Loader import com.lambda.event.EventFlow import com.lambda.graphics.texture.TextureOwner.upload import com.lambda.gui.DearImGui.EXTERNAL_LINK +import com.lambda.gui.components.QuickSearch import com.lambda.gui.dsl.ImGuiBuilder import com.lambda.module.ModuleRegistry import com.lambda.module.tag.ModuleTag @@ -51,10 +52,6 @@ object MenuBar { val lambdaLogo = upload("textures/lambda.png") val githubLogo = upload("textures/github_logo.png") - // ToDo: On pressing shift (or something else) open a quick search bar popup. - // - Search for modules, hud elements, and commands using levenshtein distance. - private val quickSearch = ImString(64) - fun ImGuiBuilder.buildMenuBar() { mainMenuBar { lambdaMenu() @@ -436,18 +433,8 @@ object MenuBar { } private fun ImGuiBuilder.buildHelpMenu() { - menuItem("Quick Search...") { - // ToDo: - // - Search for modules, commands, and HUD widgets. - // - Show matches in a search panel below the GUI. - // - Support regex. - // - Support levenshtein distance. - // - Support multiple search terms. - // - Support search history. - // - Support search filters (by type, enabled/disabled, etc). - // - Support search scopes (all/enabled/disabled). - // - Support search shortcuts (Ctrl+F, Cmd+F, etc). - // - Show match count in the search panel. + menuItem("Quick Search...", "Shift+Shift") { + QuickSearch.open() } menuItem("Documentation $EXTERNAL_LINK") { Util.getOperatingSystem().open("$REPO_URL/wiki") diff --git a/src/main/kotlin/com/lambda/gui/components/ClickGuiLayout.kt b/src/main/kotlin/com/lambda/gui/components/ClickGuiLayout.kt index dba01cfa0..7cb893ce3 100644 --- a/src/main/kotlin/com/lambda/gui/components/ClickGuiLayout.kt +++ b/src/main/kotlin/com/lambda/gui/components/ClickGuiLayout.kt @@ -21,6 +21,8 @@ import com.lambda.core.Loadable import com.lambda.event.events.GuiEvent import com.lambda.event.listener.SafeListener.Companion.listen import com.lambda.gui.MenuBar.buildMenuBar +import com.lambda.gui.components.QuickSearch.renderQuickSearch +import com.lambda.gui.components.QuickSearchInputHandler import com.lambda.gui.dsl.ImGuiBuilder.buildLayout import com.lambda.module.ModuleRegistry import com.lambda.module.modules.client.ClickGui @@ -30,6 +32,9 @@ import imgui.flag.ImGuiWindowFlags.AlwaysAutoResize object ClickGuiLayout : Loadable { init { + // Ensure QuickSearchInputHandler is loaded + QuickSearchInputHandler + listen { if (!ClickGui.isEnabled) return@listen @@ -43,6 +48,7 @@ object ClickGuiLayout : Loadable { } buildMenuBar() + renderQuickSearch() ImGui.showDemoWindow() } diff --git a/src/main/kotlin/com/lambda/gui/components/QuickSearch.kt b/src/main/kotlin/com/lambda/gui/components/QuickSearch.kt new file mode 100644 index 000000000..0c5a89851 --- /dev/null +++ b/src/main/kotlin/com/lambda/gui/components/QuickSearch.kt @@ -0,0 +1,287 @@ +/* + * Copyright 2025 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.gui.components + +import com.lambda.command.CommandRegistry +import com.lambda.config.AbstractSetting +import com.lambda.config.Configuration +import com.lambda.gui.dsl.ImGuiBuilder +import com.lambda.module.Module +import com.lambda.module.ModuleRegistry +import com.lambda.util.StringUtils.findSimilarStrings +import imgui.ImGui +import imgui.flag.ImGuiInputTextFlags +import imgui.flag.ImGuiWindowFlags +import imgui.type.ImString + +object QuickSearch { + private val searchInput = ImString(256) + private var isOpen = false + private var shouldFocus = false + private val maxResults = 50 + private val searchThreshold = 3 + + data class SearchResult( + val name: String, + val type: SearchResultType, + val description: String = "", + val action: () -> Unit + ) + + enum class SearchResultType(val displayName: String) { + MODULE("Module"), + SETTING("Setting"), + COMMAND("Command") + } + + fun open() { + isOpen = true + shouldFocus = true + searchInput.clear() + } + + fun close() { + isOpen = false + shouldFocus = false + } + + fun toggle() { + if (isOpen) close() else open() + } + + fun ImGuiBuilder.renderQuickSearch() { + if (!isOpen) return + + ImGui.openPopup("QuickSearch") + + val flags = ImGuiWindowFlags.AlwaysAutoResize or + ImGuiWindowFlags.NoTitleBar or + ImGuiWindowFlags.NoMove or + ImGuiWindowFlags.NoResize + + popupModal("QuickSearch", flags) { + // Set popup position to center of screen + val displaySize = ImGui.getIO().displaySize + val popupSize = ImGui.getWindowSize() + ImGui.setWindowPos( + (displaySize.x - popupSize.x) * 0.5f, + (displaySize.y - popupSize.y) * 0.3f + ) + + text("Quick Search") + separator() + + // Search input + if (shouldFocus) { + ImGui.setKeyboardFocusHere() + shouldFocus = false + } + + val searchChanged = inputText("##search", searchInput, ImGuiInputTextFlags.AutoSelectAll) + + // Handle escape key to close (simplified) + if (ImGui.isKeyPressed(256)) { // ImGuiKey.Escape + close() + ImGui.closeCurrentPopup() + return@popupModal + } + + // Handle enter key (simplified) + if (ImGui.isKeyPressed(257)) { // ImGuiKey.Enter + val results = performSearch(searchInput.get()) + if (results.isNotEmpty()) { + results.first().action() + close() + ImGui.closeCurrentPopup() + return@popupModal + } + } + + separator() + + // Search results + val query = searchInput.get().trim() + if (query.isNotEmpty()) { + val results = performSearch(query) + + if (results.isEmpty()) { + textDisabled("No results found") + } else { + text("Results (${results.size}):") + child("SearchResults", 400f, 300f, true) { + results.forEachIndexed { index, result -> + val isSelected = index == 0 // Highlight first result + selectable("${result.name}##${result.type}", isSelected) { + result.action() + close() + ImGui.closeCurrentPopup() + } + + if (ImGui.isItemHovered()) { + lambdaTooltip("${result.type.displayName}: ${result.name}\n${result.description}") + } + + sameLine() + textDisabled("[${result.type.displayName}]") + } + } + } + } else { + textDisabled("Type to search modules, settings, and commands...") + text("") + text("Examples:") + bulletText("'auto' - find AutoWalk, AutoTool, etc.") + bulletText("'gui' - find ClickGUI, GuiSettings, etc.") + bulletText("'speed' - find Speed module, speedSettings, etc.") + } + + separator() + + button("Close") { + close() + ImGui.closeCurrentPopup() + } + + sameLine() + textDisabled("Tip: Press Shift+Shift to open quickly") + } + } + + private fun performSearch(query: String): List { + if (query.isBlank()) return emptyList() + + val results = mutableListOf() + val lowerQuery = query.lowercase() + + // Search modules + searchModules(lowerQuery, results) + + // Search commands + searchCommands(lowerQuery, results) + + // Search settings + searchSettings(lowerQuery, results) + + return results.sortedBy { it.name.lowercase() }.take(maxResults) + } + + private fun searchModules(query: String, results: MutableList) { + // Direct name matches first + ModuleRegistry.modules.forEach { module -> + val moduleNameLower = module.name.lowercase() + if (moduleNameLower.contains(query) || moduleNameLower.startsWith(query)) { + results.add(SearchResult( + name = module.name, + type = SearchResultType.MODULE, + description = "${module.description} (${if (module.isEnabled) "Enabled" else "Disabled"})", + action = { module.toggle() } + )) + } + } + + // Fuzzy matches for modules if not too many direct matches + if (results.count { it.type == SearchResultType.MODULE } < 10) { + val moduleNames = ModuleRegistry.modules.map { it.name }.toSet() + val similarNames = findSimilarStrings(query, moduleNames, searchThreshold) + + similarNames.forEach { name -> + val module = ModuleRegistry.modules.find { it.name == name } + if (module != null && results.none { it.name == name && it.type == SearchResultType.MODULE }) { + results.add(SearchResult( + name = module.name, + type = SearchResultType.MODULE, + description = "${module.description} (${if (module.isEnabled) "Enabled" else "Disabled"})", + action = { module.toggle() } + )) + } + } + } + } + + private fun searchCommands(query: String, results: MutableList) { + CommandRegistry.commands.forEach { command -> + val commandNameLower = command.name.lowercase() + if (commandNameLower.contains(query) || + command.aliases.any { it.lowercase().contains(query) }) { + results.add(SearchResult( + name = command.name, + type = SearchResultType.COMMAND, + description = "${command.description} (prefix: ${CommandRegistry.prefix})", + action = { + // For commands, show info about usage + println("Command: ${CommandRegistry.prefix}${command.name} - ${command.description}") + } + )) + } + } + + // Add fuzzy matching for commands too + if (results.count { it.type == SearchResultType.COMMAND } < 5) { + val commandNames = CommandRegistry.commands.map { it.name }.toSet() + val similarNames = findSimilarStrings(query, commandNames, searchThreshold) + + similarNames.forEach { name -> + val command = CommandRegistry.commands.find { it.name == name } + if (command != null && results.none { it.name == name && it.type == SearchResultType.COMMAND }) { + results.add(SearchResult( + name = command.name, + type = SearchResultType.COMMAND, + description = "${command.description} (prefix: ${CommandRegistry.prefix})", + action = { + println("Command: ${CommandRegistry.prefix}${command.name} - ${command.description}") + } + )) + } + } + } + } + + private fun searchSettings(query: String, results: MutableList) { + // Limit setting search to avoid too many results + var settingCount = 0 + val maxSettings = 15 + + Configuration.configurations.forEach { config -> + if (settingCount >= maxSettings) return@forEach + + config.configurables.forEach { configurable -> + if (settingCount >= maxSettings) return@forEach + + configurable.settings.forEach { setting -> + if (settingCount >= maxSettings) return@forEach + + val settingNameLower = setting.name.lowercase() + val configurableName = configurable.name.lowercase() + + if (settingNameLower.contains(query) || configurableName.contains(query)) { + results.add(SearchResult( + name = "${configurable.name}.${setting.name}", + type = SearchResultType.SETTING, + description = setting.description.ifEmpty { "Setting in ${configurable.name}" }, + action = { + // For settings, show current value + println("Setting: ${configurable.name}.${setting.name} = ${setting.value}") + } + )) + settingCount++ + } + } + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/lambda/gui/components/QuickSearchInputHandler.kt b/src/main/kotlin/com/lambda/gui/components/QuickSearchInputHandler.kt new file mode 100644 index 000000000..0624bc1ac --- /dev/null +++ b/src/main/kotlin/com/lambda/gui/components/QuickSearchInputHandler.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2025 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.gui.components + +import com.lambda.core.Loadable +import com.lambda.event.events.KeyboardEvent +import com.lambda.event.listener.UnsafeListener.Companion.listenUnsafe +import com.lambda.util.KeyCode +import org.lwjgl.glfw.GLFW + +object QuickSearchInputHandler : Loadable { + private var lastShiftPressTime = 0L + private var lastShiftKeyCode = -1 + private val doubleShiftTimeWindow = 500L // 500ms window for double shift + + init { + listenUnsafe { event -> + handleKeyPress(event) + } + } + + private fun handleKeyPress(event: KeyboardEvent.Press) { + // Check if it's a shift key press + if (event.isPressed && (event.keyCode == GLFW.GLFW_KEY_LEFT_SHIFT || event.keyCode == GLFW.GLFW_KEY_RIGHT_SHIFT)) { + val currentTime = System.currentTimeMillis() + + // Check if this is a double shift press + if (lastShiftKeyCode == event.keyCode && + currentTime - lastShiftPressTime <= doubleShiftTimeWindow) { + // Double shift detected! + QuickSearch.open() + // Reset to prevent triple-shift issues + lastShiftPressTime = 0L + lastShiftKeyCode = -1 + } else { + // First shift press, record it + lastShiftPressTime = currentTime + lastShiftKeyCode = event.keyCode + } + } + } +} \ No newline at end of file