Skip to content

Commit a9a46cd

Browse files
CopilotAvanatiker
andcommitted
Implement basic QuickSearch functionality with double shift detection
Co-authored-by: Avanatiker <8580605+Avanatiker@users.noreply.github.com>
1 parent a2b88c2 commit a9a46cd

File tree

4 files changed

+306
-16
lines changed

4 files changed

+306
-16
lines changed

src/main/kotlin/com/lambda/gui/MenuBar.kt

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import com.lambda.core.Loader
2626
import com.lambda.event.EventFlow
2727
import com.lambda.graphics.texture.TextureOwner.upload
2828
import com.lambda.gui.DearImGui.EXTERNAL_LINK
29+
import com.lambda.gui.components.QuickSearch
2930
import com.lambda.gui.dsl.ImGuiBuilder
3031
import com.lambda.module.ModuleRegistry
3132
import com.lambda.module.tag.ModuleTag
@@ -51,10 +52,6 @@ object MenuBar {
5152
val lambdaLogo = upload("textures/lambda.png")
5253
val githubLogo = upload("textures/github_logo.png")
5354

54-
// ToDo: On pressing shift (or something else) open a quick search bar popup.
55-
// - Search for modules, hud elements, and commands using levenshtein distance.
56-
private val quickSearch = ImString(64)
57-
5855
fun ImGuiBuilder.buildMenuBar() {
5956
mainMenuBar {
6057
lambdaMenu()
@@ -436,18 +433,8 @@ object MenuBar {
436433
}
437434

438435
private fun ImGuiBuilder.buildHelpMenu() {
439-
menuItem("Quick Search...") {
440-
// ToDo:
441-
// - Search for modules, commands, and HUD widgets.
442-
// - Show matches in a search panel below the GUI.
443-
// - Support regex.
444-
// - Support levenshtein distance.
445-
// - Support multiple search terms.
446-
// - Support search history.
447-
// - Support search filters (by type, enabled/disabled, etc).
448-
// - Support search scopes (all/enabled/disabled).
449-
// - Support search shortcuts (Ctrl+F, Cmd+F, etc).
450-
// - Show match count in the search panel.
436+
menuItem("Quick Search...", "Shift+Shift") {
437+
QuickSearch.open()
451438
}
452439
menuItem("Documentation $EXTERNAL_LINK") {
453440
Util.getOperatingSystem().open("$REPO_URL/wiki")

src/main/kotlin/com/lambda/gui/components/ClickGuiLayout.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import com.lambda.core.Loadable
2121
import com.lambda.event.events.GuiEvent
2222
import com.lambda.event.listener.SafeListener.Companion.listen
2323
import com.lambda.gui.MenuBar.buildMenuBar
24+
import com.lambda.gui.components.QuickSearch.renderQuickSearch
2425
import com.lambda.gui.dsl.ImGuiBuilder.buildLayout
2526
import com.lambda.module.ModuleRegistry
2627
import com.lambda.module.modules.client.ClickGui
@@ -43,6 +44,7 @@ object ClickGuiLayout : Loadable {
4344
}
4445

4546
buildMenuBar()
47+
renderQuickSearch()
4648

4749
ImGui.showDemoWindow()
4850
}
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
/*
2+
* Copyright 2025 Lambda
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation, either version 3 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
18+
package com.lambda.gui.components
19+
20+
import com.lambda.command.CommandRegistry
21+
import com.lambda.config.AbstractSetting
22+
import com.lambda.config.Configuration
23+
import com.lambda.gui.dsl.ImGuiBuilder
24+
import com.lambda.module.Module
25+
import com.lambda.module.ModuleRegistry
26+
import com.lambda.util.StringUtils.findSimilarStrings
27+
import imgui.ImGui
28+
import imgui.flag.ImGuiInputTextFlags
29+
import imgui.flag.ImGuiWindowFlags
30+
import imgui.type.ImString
31+
32+
object QuickSearch {
33+
private val searchInput = ImString(256)
34+
private var isOpen = false
35+
private var shouldFocus = false
36+
private val maxResults = 50
37+
private val searchThreshold = 3
38+
39+
data class SearchResult(
40+
val name: String,
41+
val type: SearchResultType,
42+
val description: String = "",
43+
val action: () -> Unit
44+
)
45+
46+
enum class SearchResultType(val displayName: String) {
47+
MODULE("Module"),
48+
SETTING("Setting"),
49+
COMMAND("Command")
50+
}
51+
52+
fun open() {
53+
isOpen = true
54+
shouldFocus = true
55+
searchInput.clear()
56+
}
57+
58+
fun close() {
59+
isOpen = false
60+
shouldFocus = false
61+
}
62+
63+
fun toggle() {
64+
if (isOpen) close() else open()
65+
}
66+
67+
fun ImGuiBuilder.renderQuickSearch() {
68+
if (!isOpen) return
69+
70+
ImGui.openPopup("QuickSearch")
71+
72+
val flags = ImGuiWindowFlags.AlwaysAutoResize or
73+
ImGuiWindowFlags.NoTitleBar or
74+
ImGuiWindowFlags.NoMove or
75+
ImGuiWindowFlags.NoResize
76+
77+
popupModal("QuickSearch", flags) {
78+
// Set popup position to center of screen
79+
val displaySize = ImGui.getIO().displaySize
80+
val popupSize = ImGui.getWindowSize()
81+
ImGui.setWindowPos(
82+
(displaySize.x - popupSize.x) * 0.5f,
83+
(displaySize.y - popupSize.y) * 0.3f
84+
)
85+
86+
text("Quick Search")
87+
separator()
88+
89+
// Search input
90+
if (shouldFocus) {
91+
ImGui.setKeyboardFocusHere()
92+
shouldFocus = false
93+
}
94+
95+
if (inputText("##search", searchInput, ImGuiInputTextFlags.AutoSelectAll)) {
96+
// Input changed, search will be updated below
97+
}
98+
99+
// Handle escape key to close (simplified)
100+
if (ImGui.isKeyPressed(256)) { // ImGuiKey.Escape
101+
close()
102+
ImGui.closeCurrentPopup()
103+
return@popupModal
104+
}
105+
106+
// Handle enter key (simplified)
107+
if (ImGui.isKeyPressed(257)) { // ImGuiKey.Enter
108+
val results = performSearch(searchInput.get())
109+
if (results.isNotEmpty()) {
110+
results.first().action()
111+
close()
112+
ImGui.closeCurrentPopup()
113+
return@popupModal
114+
}
115+
}
116+
117+
separator()
118+
119+
// Search results
120+
val query = searchInput.get().trim()
121+
if (query.isNotEmpty()) {
122+
val results = performSearch(query)
123+
124+
if (results.isEmpty()) {
125+
textColored(0.7f, 0.7f, 0.7f, 1.0f, "No results found")
126+
} else {
127+
text("Results (${results.size}):")
128+
child("SearchResults", 400f, 300f, true) {
129+
results.forEach { result ->
130+
if (selectable("${result.name}##${result.type}")) {
131+
result.action()
132+
close()
133+
ImGui.closeCurrentPopup()
134+
}
135+
136+
if (ImGui.isItemHovered()) {
137+
lambdaTooltip("${result.type.displayName}: ${result.name}\n${result.description}")
138+
}
139+
140+
sameLine()
141+
textColored(0.6f, 0.6f, 0.6f, 1.0f, "[${result.type.displayName}]")
142+
}
143+
}
144+
}
145+
} else {
146+
textColored(0.7f, 0.7f, 0.7f, 1.0f, "Type to search modules, settings, and commands...")
147+
}
148+
149+
separator()
150+
151+
if (button("Close")) {
152+
close()
153+
ImGui.closeCurrentPopup()
154+
}
155+
}
156+
}
157+
158+
private fun performSearch(query: String): List<SearchResult> {
159+
if (query.isBlank()) return emptyList()
160+
161+
val results = mutableListOf<SearchResult>()
162+
val lowerQuery = query.lowercase()
163+
164+
// Search modules
165+
searchModules(lowerQuery, results)
166+
167+
// Search commands
168+
searchCommands(lowerQuery, results)
169+
170+
// Search settings
171+
searchSettings(lowerQuery, results)
172+
173+
return results.sortedBy { it.name.lowercase() }.take(maxResults)
174+
}
175+
176+
private fun searchModules(query: String, results: MutableList<SearchResult>) {
177+
// Direct name matches first
178+
ModuleRegistry.modules.forEach { module ->
179+
if (module.name.lowercase().contains(query)) {
180+
results.add(SearchResult(
181+
name = module.name,
182+
type = SearchResultType.MODULE,
183+
description = module.description,
184+
action = { module.toggle() }
185+
))
186+
}
187+
}
188+
189+
// Fuzzy matches for modules if not too many direct matches
190+
if (results.count { it.type == SearchResultType.MODULE } < 10) {
191+
val moduleNames = ModuleRegistry.modules.map { it.name }.toSet()
192+
val similarNames = findSimilarStrings(query, moduleNames, searchThreshold)
193+
194+
similarNames.forEach { name ->
195+
val module = ModuleRegistry.modules.find { it.name == name }
196+
if (module != null && results.none { it.name == name && it.type == SearchResultType.MODULE }) {
197+
results.add(SearchResult(
198+
name = module.name,
199+
type = SearchResultType.MODULE,
200+
description = module.description,
201+
action = { module.toggle() }
202+
))
203+
}
204+
}
205+
}
206+
}
207+
208+
private fun searchCommands(query: String, results: MutableList<SearchResult>) {
209+
CommandRegistry.commands.forEach { command ->
210+
if (command.name.lowercase().contains(query) ||
211+
command.aliases.any { it.lowercase().contains(query) }) {
212+
results.add(SearchResult(
213+
name = command.name,
214+
type = SearchResultType.COMMAND,
215+
description = command.description,
216+
action = {
217+
// For commands, we could open chat with the command prefix
218+
// but that's complex, so for now just show info
219+
}
220+
))
221+
}
222+
}
223+
}
224+
225+
private fun searchSettings(query: String, results: MutableList<SearchResult>) {
226+
Configuration.configurations.forEach { config ->
227+
config.configurables.forEach { configurable ->
228+
configurable.settings.forEach { setting ->
229+
if (setting.name.lowercase().contains(query)) {
230+
results.add(SearchResult(
231+
name = "${configurable.name}.${setting.name}",
232+
type = SearchResultType.SETTING,
233+
description = setting.description,
234+
action = {
235+
// For settings, we could navigate to the setting
236+
// but that's complex for now
237+
}
238+
))
239+
}
240+
}
241+
}
242+
}
243+
}
244+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright 2025 Lambda
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation, either version 3 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
18+
package com.lambda.gui.components
19+
20+
import com.lambda.core.Loadable
21+
import com.lambda.event.events.KeyboardEvent
22+
import com.lambda.event.listener.UnsafeListener.Companion.listenUnsafe
23+
import com.lambda.util.KeyCode
24+
import org.lwjgl.glfw.GLFW
25+
26+
object QuickSearchInputHandler : Loadable {
27+
private var lastShiftPressTime = 0L
28+
private var lastShiftKeyCode = -1
29+
private val doubleShiftTimeWindow = 500L // 500ms window for double shift
30+
31+
init {
32+
listenUnsafe<KeyboardEvent.Press> { event ->
33+
handleKeyPress(event)
34+
}
35+
}
36+
37+
private fun handleKeyPress(event: KeyboardEvent.Press) {
38+
// Check if it's a shift key press
39+
if (event.isPressed && (event.keyCode == GLFW.GLFW_KEY_LEFT_SHIFT || event.keyCode == GLFW.GLFW_KEY_RIGHT_SHIFT)) {
40+
val currentTime = System.currentTimeMillis()
41+
42+
// Check if this is a double shift press
43+
if (lastShiftKeyCode == event.keyCode &&
44+
currentTime - lastShiftPressTime <= doubleShiftTimeWindow) {
45+
// Double shift detected!
46+
QuickSearch.open()
47+
// Reset to prevent triple-shift issues
48+
lastShiftPressTime = 0L
49+
lastShiftKeyCode = -1
50+
} else {
51+
// First shift press, record it
52+
lastShiftPressTime = currentTime
53+
lastShiftKeyCode = event.keyCode
54+
}
55+
}
56+
}
57+
}

0 commit comments

Comments
 (0)