@@ -3,9 +3,6 @@ package cc.unitmesh.devti.mcp.ui
33import cc.unitmesh.devti.settings.customize.customizeSetting
44import com.intellij.openapi.project.Project
55import com.intellij.openapi.ui.popup.JBPopupFactory
6- import com.intellij.ui.CheckboxTree
7- import com.intellij.ui.CheckedTreeNode
8- import com.intellij.ui.SimpleTextAttributes
96import com.intellij.ui.components.JBLoadingPanel
107import com.intellij.ui.components.JBScrollPane
118import com.intellij.ui.components.JBTextField
@@ -18,26 +15,10 @@ import java.awt.Dimension
1815import java.awt.event.KeyAdapter
1916import java.awt.event.KeyEvent
2017import javax.swing.*
21- import javax.swing.tree.DefaultTreeModel
22- import javax.swing.tree.TreePath
2318import com.intellij.openapi.application.invokeLater
2419import io.modelcontextprotocol.kotlin.sdk.Tool
25-
26- class ServerTreeNode (val serverName : String ) : CheckedTreeNode(serverName) {
27- init {
28- allowsChildren = true
29- }
30- }
31-
32- class ToolTreeNode (val serverName : String , val tool : Tool ) : CheckedTreeNode(tool.name) {
33- init {
34- allowsChildren = false
35- userObject = tool.name
36- }
37-
38- override fun toString (): String = tool.name
39- }
40-
20+ import java.awt.Component
21+ import java.awt.Font
4122
4223class McpConfigPopup {
4324 companion object {
@@ -48,6 +29,11 @@ class McpConfigPopup {
4829 }
4930 }
5031
32+ private val toolsPanel = JPanel ().apply {
33+ layout = BoxLayout (this , BoxLayout .Y_AXIS )
34+ }
35+ private val toolCheckboxMap = mutableMapOf<Pair <String , String >, JCheckBox > () // Stores serverName/toolName to JCheckBox
36+
5137 private fun createAndShow (component : JComponent ? , project : Project , configService : McpConfigService ) {
5238 val mainPanel = JPanel (BorderLayout ()).apply {
5339 preferredSize = Dimension (400 , 500 )
@@ -60,39 +46,11 @@ class McpConfigPopup {
6046 border = JBUI .Borders .empty(4 )
6147 }
6248
63- // Tree for tool selection
64- val rootNode = CheckedTreeNode (" MCP Tools" )
65- val treeModel = DefaultTreeModel (rootNode)
66- val tree = CheckboxTree (object : CheckboxTree .CheckboxTreeCellRenderer () {
67- override fun customizeRenderer (
68- tree : JTree ? ,
69- value : Any? ,
70- selected : Boolean ,
71- expanded : Boolean ,
72- leaf : Boolean ,
73- row : Int ,
74- hasFocus : Boolean
75- ) {
76- if (value is ToolTreeNode ) {
77- textRenderer.append(value.tool.name, SimpleTextAttributes .REGULAR_ATTRIBUTES )
78- value.tool.description?.let { desc ->
79- if (desc.isNotEmpty()) {
80- textRenderer.append(" - $desc " , SimpleTextAttributes .GRAYED_ATTRIBUTES )
81- }
82- }
83- } else if (value is ServerTreeNode ) {
84- textRenderer.append(value.serverName, SimpleTextAttributes .REGULAR_BOLD_ATTRIBUTES )
85- } else {
86- textRenderer.append(value.toString(), SimpleTextAttributes .REGULAR_ATTRIBUTES )
87- }
88- }
89- }, rootNode).apply {
90- isRootVisible = false
91- showsRootHandles = true
92- }
93-
49+ // Panel for tool selection (replacing CheckboxTree)
50+ // toolsPanel is now a class member, initialized above
51+
9452 val loadingPanel = JBLoadingPanel (BorderLayout (), project)
95- loadingPanel.add(JBScrollPane (tree ), BorderLayout .CENTER )
53+ loadingPanel.add(JBScrollPane (toolsPanel ), BorderLayout .CENTER ) // toolsPanel instead of tree
9654 loadingPanel.preferredSize = Dimension (380 , 350 )
9755
9856 var currentPopup: com.intellij.openapi.ui.popup.JBPopup ? = null
@@ -104,15 +62,15 @@ class McpConfigPopup {
10462 val refreshButton = JButton (" Refresh" ).apply {
10563 addActionListener {
10664 loadingPanel.startLoading()
107- refreshToolsList(project, configService, rootNode, treeModel, tree, loadingPanel)
65+ refreshToolsList(project, configService, loadingPanel)
10866 }
10967 }
11068 add(refreshButton)
11169 add(Box .createHorizontalGlue())
11270
11371 val applyButton = JButton (" Apply" ).apply {
11472 addActionListener {
115- saveSelectedTools(tree, configService)
73+ saveSelectedTools(configService)
11674 currentPopup?.cancel()
11775 }
11876 }
@@ -134,12 +92,12 @@ class McpConfigPopup {
13492
13593 // Load tools asynchronously
13694 loadingPanel.startLoading()
137- loadToolsIntoTree (project, configService, rootNode, treeModel, tree , loadingPanel)
95+ loadToolsIntoPanel (project, configService, loadingPanel)
13896
13997 // Search functionality
14098 searchField.addKeyListener(object : KeyAdapter () {
14199 override fun keyReleased (e : KeyEvent ) {
142- filterTree(tree, rootNode, searchField.text)
100+ filterToolsList( searchField.text)
143101 }
144102 })
145103
@@ -162,30 +120,27 @@ class McpConfigPopup {
162120 private fun refreshToolsList (
163121 project : Project ,
164122 configService : McpConfigService ,
165- rootNode : CheckedTreeNode ,
166- treeModel : DefaultTreeModel ,
167- tree : CheckboxTree ,
168123 loadingPanel : JBLoadingPanel
169124 ) {
170- rootNode.removeAllChildren ()
171- treeModel.reload ()
172-
173- loadToolsIntoTree (project, configService, rootNode, treeModel, tree , loadingPanel)
125+ toolsPanel.removeAll ()
126+ toolCheckboxMap.clear ()
127+
128+ loadToolsIntoPanel (project, configService, loadingPanel)
174129 }
175130
176- private fun loadToolsIntoTree (
131+ private fun loadToolsIntoPanel (
177132 project : Project ,
178133 configService : McpConfigService ,
179- rootNode : CheckedTreeNode ,
180- treeModel : DefaultTreeModel ,
181- tree : CheckboxTree ,
182134 loadingPanel : JBLoadingPanel
183135 ) {
184- rootNode.removeAllChildren()
185- val loadingNode = CheckedTreeNode (" Loading tools..." )
186- rootNode.add(loadingNode)
187- treeModel.reload(rootNode) // Reload to show the loading node
188- expandAllNodes(tree)
136+ toolsPanel.removeAll() // Clear previous content
137+ toolCheckboxMap.clear()
138+
139+ val loadingLabel = JLabel (" Loading tools..." )
140+ loadingLabel.alignmentX = Component .LEFT_ALIGNMENT
141+ toolsPanel.add(loadingLabel)
142+ toolsPanel.revalidate()
143+ toolsPanel.repaint()
189144
190145 CoroutineScope (Dispatchers .IO ).launch {
191146 try {
@@ -194,55 +149,64 @@ class McpConfigPopup {
194149 val selectedTools = configService.getSelectedTools()
195150
196151 invokeLater {
197- rootNode.removeAllChildren () // Remove "Loading tools..." node
152+ toolsPanel.removeAll () // Remove "Loading tools..." label
198153
199154 if (allTools.isEmpty()) {
200- val noToolsNode = CheckedTreeNode (" No tools available." )
201- rootNode.add(noToolsNode)
155+ val noToolsLabel = JLabel (" No tools available." )
156+ noToolsLabel.alignmentX = Component .LEFT_ALIGNMENT
157+ toolsPanel.add(noToolsLabel)
202158 } else {
203159 allTools.forEach { (serverName, tools) ->
204- val serverNode = ServerTreeNode (serverName)
205- rootNode.add(serverNode)
160+ val serverLabel = JLabel (serverName).apply {
161+ font = font.deriveFont(Font .BOLD )
162+ border = JBUI .Borders .emptyTop(8 )
163+ alignmentX = Component .LEFT_ALIGNMENT
164+ }
165+ toolsPanel.add(serverLabel)
206166
207167 if (tools.isEmpty()) {
208- val noToolsForServerNode = CheckedTreeNode (" No tools from this server." )
209- serverNode.add(noToolsForServerNode)
168+ val noToolsForServerLabel = JLabel (" No tools from this server." ).apply {
169+ alignmentX = Component .LEFT_ALIGNMENT
170+ }
171+ toolsPanel.add(noToolsForServerLabel)
210172 } else {
211173 tools.forEach { tool ->
212- val toolNode = ToolTreeNode (serverName, tool)
213- val isSelected = selectedTools[serverName]?.contains(tool.name) == true
214- toolNode.isChecked = isSelected
215- serverNode.add(toolNode)
174+ val checkBoxText = tool.description?.let { desc ->
175+ if (desc.isNotEmpty()) " ${tool.name} - $desc " else tool.name
176+ } ? : tool.name
177+ val checkBox = JCheckBox (checkBoxText).apply {
178+ isSelected = selectedTools[serverName]?.contains(tool.name) == true
179+ alignmentX = Component .LEFT_ALIGNMENT
180+ border = JBUI .Borders .emptyLeft(10 )
181+ }
182+ toolCheckboxMap[Pair (serverName, tool.name)] = checkBox
183+ toolsPanel.add(checkBox)
216184 }
217185 }
218186 }
219187 }
220-
221- treeModel.nodeStructureChanged(rootNode) // Notify that rootNode's children changed
222- expandAllNodes(tree)
223188
224189 loadingPanel.stopLoading()
225190
226- tree .revalidate() // Ensure tree layout is updated
227- tree .repaint()
191+ toolsPanel .revalidate()
192+ toolsPanel .repaint()
228193 loadingPanel.revalidate()
229194 loadingPanel.repaint()
230- // Also revalidate and repaint parent in case its layout depends on loadingPanel
231195 (loadingPanel.parent as ? JComponent )?.revalidate()
232196 (loadingPanel.parent as ? JComponent )?.repaint()
233197 }
234198 } catch (e: Exception ) {
235199 invokeLater {
236- rootNode.removeAllChildren ()
237- val errorNode = CheckedTreeNode (" Error loading tools: ${e.message} " )
238- rootNode.add(errorNode)
239- treeModel.nodeStructureChanged(rootNode) // Notify change
240- expandAllNodes(tree )
241-
200+ toolsPanel.removeAll ()
201+ val errorLabel = JLabel (" Error loading tools: ${e.message} " ). apply {
202+ alignmentX = Component . LEFT_ALIGNMENT
203+ }
204+ toolsPanel.add(errorLabel )
205+
242206 loadingPanel.stopLoading()
243207
244- tree .revalidate()
245- tree .repaint()
208+ toolsPanel .revalidate()
209+ toolsPanel .repaint()
246210 loadingPanel.revalidate()
247211 loadingPanel.repaint()
248212 (loadingPanel.parent as ? JComponent )?.revalidate()
@@ -252,33 +216,55 @@ class McpConfigPopup {
252216 }
253217 }
254218
255- private fun saveSelectedTools (tree : CheckboxTree , configService : McpConfigService ) {
219+ private fun saveSelectedTools (configService : McpConfigService ) {
256220 val selectedTools = mutableMapOf<String , MutableSet <String >>()
257221
258- val root = tree.model.root as CheckedTreeNode
259- for (i in 0 until root.childCount) {
260- val serverNode = root.getChildAt(i) as ServerTreeNode
261- val serverName = serverNode.serverName
262-
263- for (j in 0 until serverNode.childCount) {
264- val toolNode = serverNode.getChildAt(j) as ToolTreeNode
265- if (toolNode.isChecked) {
266- selectedTools.computeIfAbsent(serverName) { mutableSetOf () }
267- .add(toolNode.tool.name)
268- }
222+ toolCheckboxMap.forEach { (key, checkBox) ->
223+ val (serverName, toolName) = key
224+ if (checkBox.isSelected) {
225+ selectedTools.computeIfAbsent(serverName) { mutableSetOf () }
226+ .add(toolName)
269227 }
270228 }
271229
272230 configService.setSelectedTools(selectedTools)
273231 }
274232
275- private fun filterTree (tree : CheckboxTree , rootNode : CheckedTreeNode , searchText : String ) {
276- tree.expandPath(TreePath (rootNode.path))
277- }
278-
279- private fun expandAllNodes (tree : CheckboxTree ) {
280- for (i in 0 until tree.rowCount) {
281- tree.expandRow(i)
233+ private fun filterToolsList (searchText : String ) {
234+ val lowerSearchText = searchText.lowercase().trim()
235+ var firstVisible: Component ? = null
236+
237+ toolsPanel.components.forEach { component ->
238+ when (component) {
239+ is JCheckBox -> {
240+ val toolName = toolCheckboxMap.entries.find { it.value == component }?.key?.second ? : " "
241+ val toolDescription = component.text.substringAfter(" $toolName - " , " " ).substringBeforeLast(" - $toolName " , " " )
242+
243+ val isVisible = toolName.lowercase().contains(lowerSearchText) ||
244+ toolDescription.lowercase().contains(lowerSearchText) ||
245+ lowerSearchText.isEmpty()
246+ component.isVisible = isVisible
247+ if (isVisible && firstVisible == null ) {
248+ firstVisible = component
249+ }
250+ }
251+ is JLabel -> {
252+ // Server labels or status labels. For now, keep them visible or hide if all children are hidden.
253+ // This part can be enhanced to hide server labels if all its tools are hidden.
254+ // For simplicity, we'll keep them visible. If search is empty, all are visible.
255+ component.isVisible = true
256+ }
257+ }
258+ }
259+ // If there's a search term and some items are visible, try to scroll to the first visible item.
260+ if (lowerSearchText.isNotEmpty() && firstVisible != null ) {
261+ val finalFirstVisible = firstVisible
262+ SwingUtilities .invokeLater {
263+ toolsPanel.scrollRectToVisible(finalFirstVisible.bounds)
264+ }
282265 }
266+
267+ toolsPanel.revalidate()
268+ toolsPanel.repaint()
283269 }
284270}
0 commit comments