From 11997dde75cffac8aed8d8f9835da05f158cd95a Mon Sep 17 00:00:00 2001 From: Zayden Date: Tue, 10 Feb 2026 13:00:26 +0700 Subject: [PATCH] Add dynamic icon selection for file groups MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement feature request from issue #31 to allow users to customize icons for file groups beyond automatic pattern-based matching. Changes: - Add customIconPath field to ProjectFileGroup data class - Create FileGroupIconOption enum with 18 predefined icon choices - Add icon ComboBox to ProjectFileGroupDialog with visual icon picker - Update ProjectFileGroupNode to prioritize custom icons over auto-detection - Add localization for icon picker label - Preserve customIconPath in settings persistence Users can now select from icons including Folder, Package, Module, Config Folder, Settings, Resources, and more, or use "Auto" for pattern-based detection. Resolves #31 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../a2pt/nodes/ProjectFileGroupNode.kt | 38 ++++++++++++++++ .../a2pt/settings/AndroidViewSettings.kt | 3 +- .../AndroidViewSettingsConfigurable.kt | 4 +- .../a2pt/settings/FileGroupIconOption.kt | 44 +++++++++++++++++++ .../a2pt/settings/ProjectFileGroupDialog.kt | 43 +++++++++++++++++- .../messages/AndroidViewBundle.properties | 1 + 6 files changed, 129 insertions(+), 4 deletions(-) create mode 100644 src/main/kotlin/com/z8dn/plugins/a2pt/settings/FileGroupIconOption.kt diff --git a/src/main/kotlin/com/z8dn/plugins/a2pt/nodes/ProjectFileGroupNode.kt b/src/main/kotlin/com/z8dn/plugins/a2pt/nodes/ProjectFileGroupNode.kt index 4856af0..a96e500 100644 --- a/src/main/kotlin/com/z8dn/plugins/a2pt/nodes/ProjectFileGroupNode.kt +++ b/src/main/kotlin/com/z8dn/plugins/a2pt/nodes/ProjectFileGroupNode.kt @@ -60,10 +60,16 @@ class ProjectFileGroupNode( /** * Determines the icon for this group based on the patterns. + * - If a custom icon is set, use that * - If there's only one pattern, use a file-type-specific icon * - If there are multiple patterns, use a generic folder icon */ private fun getGroupIcon(): Icon { + // Check if a custom icon is set + if (fileGroup.customIconPath != null) { + return getIconFromPath(fileGroup.customIconPath!!) + } + if (fileGroup.patterns.size == 1) { val pattern = fileGroup.patterns[0] val fileTypeManager = FileTypeManager.getInstance() @@ -86,6 +92,38 @@ class ProjectFileGroupNode( return AllIcons.Nodes.Folder } + /** + * Resolves an icon from the AllIcons path string. + * Example: "AllIcons.Nodes.Folder" -> AllIcons.Nodes.Folder + */ + private fun getIconFromPath(iconPath: String): Icon { + return try { + // Parse the icon path and use reflection to get the icon + when (iconPath) { + "AllIcons.Nodes.Folder" -> AllIcons.Nodes.Folder + "AllIcons.Nodes.Package" -> AllIcons.Nodes.Package + "AllIcons.Nodes.Module" -> AllIcons.Nodes.Module + "AllIcons.Nodes.ConfigFolder" -> AllIcons.Nodes.ConfigFolder + "AllIcons.Nodes.DataTables" -> AllIcons.Nodes.DataTables + "AllIcons.Nodes.ResourceBundle" -> AllIcons.Nodes.ResourceBundle + "AllIcons.Nodes.WebFolder" -> AllIcons.Nodes.WebFolder + "AllIcons.General.Settings" -> AllIcons.General.Settings + "AllIcons.General.GearPlain" -> AllIcons.General.GearPlain + "AllIcons.Actions.ListFiles" -> AllIcons.Actions.ListFiles + "AllIcons.Actions.GroupBy" -> AllIcons.Actions.GroupBy + "AllIcons.Actions.Copy" -> AllIcons.Actions.Copy + "AllIcons.Actions.Edit" -> AllIcons.Actions.Edit + "AllIcons.FileTypes.Config" -> AllIcons.FileTypes.Config + "AllIcons.FileTypes.Text" -> AllIcons.FileTypes.Text + "AllIcons.FileTypes.Properties" -> AllIcons.FileTypes.Properties + "AllIcons.FileTypes.Archive" -> AllIcons.FileTypes.Archive + else -> AllIcons.Nodes.Folder // Default fallback + } + } catch (e: Exception) { + AllIcons.Nodes.Folder // Fallback in case of error + } + } + /** * Checks if a filename matches any of the specified patterns. */ diff --git a/src/main/kotlin/com/z8dn/plugins/a2pt/settings/AndroidViewSettings.kt b/src/main/kotlin/com/z8dn/plugins/a2pt/settings/AndroidViewSettings.kt index f46dd01..15d23c2 100644 --- a/src/main/kotlin/com/z8dn/plugins/a2pt/settings/AndroidViewSettings.kt +++ b/src/main/kotlin/com/z8dn/plugins/a2pt/settings/AndroidViewSettings.kt @@ -12,7 +12,8 @@ import com.intellij.util.xmlb.XmlSerializerUtil */ data class ProjectFileGroup( var groupName: String = "", - var patterns: MutableList = mutableListOf() + var patterns: MutableList = mutableListOf(), + var customIconPath: String? = null ) @State( diff --git a/src/main/kotlin/com/z8dn/plugins/a2pt/settings/AndroidViewSettingsConfigurable.kt b/src/main/kotlin/com/z8dn/plugins/a2pt/settings/AndroidViewSettingsConfigurable.kt index f51bc23..4a10157 100644 --- a/src/main/kotlin/com/z8dn/plugins/a2pt/settings/AndroidViewSettingsConfigurable.kt +++ b/src/main/kotlin/com/z8dn/plugins/a2pt/settings/AndroidViewSettingsConfigurable.kt @@ -127,7 +127,7 @@ class AndroidViewSettingsConfigurable : SearchableConfigurable { // Clear and rebuild the groups list to ensure proper change detection settings.projectFileGroups.clear() groupsTableModel?.getGroups()?.forEach { group -> - settings.projectFileGroups.add(ProjectFileGroup(group.groupName, group.patterns.toMutableList())) + settings.projectFileGroups.add(ProjectFileGroup(group.groupName, group.patterns.toMutableList(), group.customIconPath)) } // Refresh all open projects to reflect the changes @@ -144,7 +144,7 @@ class AndroidViewSettingsConfigurable : SearchableConfigurable { showBuildDirectoryCheckBox?.isSelected = settings.showBuildDirectory groupsTableModel?.setGroups(settings.projectFileGroups.map { - ProjectFileGroup(it.groupName, it.patterns.toMutableList()) + ProjectFileGroup(it.groupName, it.patterns.toMutableList(), it.customIconPath) }) } diff --git a/src/main/kotlin/com/z8dn/plugins/a2pt/settings/FileGroupIconOption.kt b/src/main/kotlin/com/z8dn/plugins/a2pt/settings/FileGroupIconOption.kt new file mode 100644 index 0000000..702b772 --- /dev/null +++ b/src/main/kotlin/com/z8dn/plugins/a2pt/settings/FileGroupIconOption.kt @@ -0,0 +1,44 @@ +package com.z8dn.plugins.a2pt.settings + +import com.intellij.icons.AllIcons +import javax.swing.Icon + +/** + * Enumeration of available icon options for file groups. + * Each option provides a display name and the corresponding icon. + */ +enum class FileGroupIconOption( + val displayName: String, + val iconPath: String?, + val icon: Icon +) { + AUTO("Auto (based on patterns)", null, AllIcons.FileTypes.Any_type), + FOLDER("Folder", "AllIcons.Nodes.Folder", AllIcons.Nodes.Folder), + PACKAGE("Package", "AllIcons.Nodes.Package", AllIcons.Nodes.Package), + MODULE("Module", "AllIcons.Nodes.Module", AllIcons.Nodes.Module), + CONFIG_FOLDER("Config Folder", "AllIcons.Nodes.ConfigFolder", AllIcons.Nodes.ConfigFolder), + DATA_TABLES("Data Tables", "AllIcons.Nodes.DataTables", AllIcons.Nodes.DataTables), + RESOURCES("Resources", "AllIcons.Nodes.ResourceBundle", AllIcons.Nodes.ResourceBundle), + WEB_FOLDER("Web Folder", "AllIcons.Nodes.WebFolder", AllIcons.Nodes.WebFolder), + SETTINGS("Settings", "AllIcons.General.Settings", AllIcons.General.Settings), + GEAR("Gear", "AllIcons.General.GearPlain", AllIcons.General.GearPlain), + LIST_FILES("List Files", "AllIcons.Actions.ListFiles", AllIcons.Actions.ListFiles), + GROUP_BY("Group By", "AllIcons.Actions.GroupBy", AllIcons.Actions.GroupBy), + COPY("Copy", "AllIcons.Actions.Copy", AllIcons.Actions.Copy), + EDIT("Edit", "AllIcons.Actions.Edit", AllIcons.Actions.Edit), + CONFIG("Config File", "AllIcons.FileTypes.Config", AllIcons.FileTypes.Config), + TEXT("Text File", "AllIcons.FileTypes.Text", AllIcons.FileTypes.Text), + PROPERTIES("Properties", "AllIcons.FileTypes.Properties", AllIcons.FileTypes.Properties), + ARCHIVE("Archive", "AllIcons.FileTypes.Archive", AllIcons.FileTypes.Archive); + + companion object { + /** + * Finds the IconOption that matches the given icon path. + * Returns AUTO if no match is found. + */ + fun fromIconPath(iconPath: String?): FileGroupIconOption { + if (iconPath == null) return AUTO + return entries.find { it.iconPath == iconPath } ?: AUTO + } + } +} diff --git a/src/main/kotlin/com/z8dn/plugins/a2pt/settings/ProjectFileGroupDialog.kt b/src/main/kotlin/com/z8dn/plugins/a2pt/settings/ProjectFileGroupDialog.kt index 8f69aac..b84ec52 100644 --- a/src/main/kotlin/com/z8dn/plugins/a2pt/settings/ProjectFileGroupDialog.kt +++ b/src/main/kotlin/com/z8dn/plugins/a2pt/settings/ProjectFileGroupDialog.kt @@ -2,6 +2,7 @@ package com.z8dn.plugins.a2pt.settings import com.z8dn.plugins.a2pt.AndroidViewBundle +import com.intellij.openapi.ui.ComboBox import com.intellij.openapi.ui.DialogWrapper import com.intellij.openapi.ui.ValidationInfo import com.intellij.ui.ToolbarDecorator @@ -9,10 +10,13 @@ import com.intellij.ui.components.JBLabel import com.intellij.ui.components.JBTextField import com.intellij.ui.table.JBTable import com.intellij.util.ui.JBUI +import java.awt.Component import java.awt.Dimension import java.awt.GridBagConstraints import java.awt.GridBagLayout +import javax.swing.DefaultListCellRenderer import javax.swing.JComponent +import javax.swing.JList import javax.swing.JOptionPane import javax.swing.JPanel import javax.swing.ListSelectionModel @@ -26,6 +30,7 @@ class ProjectFileGroupDialog( ) : DialogWrapper(true) { private val groupNameField: JBTextField = JBTextField() + private val iconComboBox: ComboBox private val patternsTable: JBTable private val patternsTableModel: PatternsTableModel @@ -35,6 +40,26 @@ class ProjectFileGroupDialog( else AndroidViewBundle.message("dialog.ProjectFileGroup.Edit.text") + // Initialize icon combo box with custom renderer + iconComboBox = ComboBox(FileGroupIconOption.entries.toTypedArray()).apply { + renderer = object : DefaultListCellRenderer() { + override fun getListCellRendererComponent( + list: JList<*>?, + value: Any?, + index: Int, + isSelected: Boolean, + cellHasFocus: Boolean + ): Component { + val component = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus) + if (value is FileGroupIconOption) { + text = value.displayName + icon = value.icon + } + return component + } + } + } + patternsTableModel = PatternsTableModel() patternsTable = JBTable(patternsTableModel).apply { setShowGrid(true) @@ -46,6 +71,7 @@ class ProjectFileGroupDialog( existingGroup?.let { groupNameField.text = it.groupName patternsTableModel.setPatterns(it.patterns.toMutableList()) + iconComboBox.selectedItem = FileGroupIconOption.fromIconPath(it.customIconPath) } init() @@ -69,6 +95,19 @@ class ProjectFileGroupDialog( groupNameField.preferredSize = Dimension(400, groupNameField.preferredSize.height) panel.add(groupNameField, gbc) + // Icon label + gbc.gridy++ + gbc.weightx = 0.0 + gbc.insets = JBUI.insets(10, 5, 5, 5) + panel.add(JBLabel(AndroidViewBundle.message("dialog.ProjectFileGroup.Icon.text")), gbc) + + // Icon combo box + gbc.gridy++ + gbc.weightx = 1.0 + gbc.insets = JBUI.insets(5) + iconComboBox.preferredSize = Dimension(400, iconComboBox.preferredSize.height) + panel.add(iconComboBox, gbc) + // Patterns section label gbc.gridy++ gbc.insets = JBUI.insets(15, 5, 5, 5) @@ -149,9 +188,11 @@ class ProjectFileGroupDialog( * Returns the configured ProjectFileGroup from the dialog. */ fun getProjectFileGroup(): ProjectFileGroup { + val selectedIcon = iconComboBox.selectedItem as? FileGroupIconOption return ProjectFileGroup( groupName = groupNameField.text.trim(), - patterns = patternsTableModel.getPatterns() + patterns = patternsTableModel.getPatterns(), + customIconPath = selectedIcon?.iconPath ) } diff --git a/src/main/resources/messages/AndroidViewBundle.properties b/src/main/resources/messages/AndroidViewBundle.properties index 29a3355..3a166b9 100644 --- a/src/main/resources/messages/AndroidViewBundle.properties +++ b/src/main/resources/messages/AndroidViewBundle.properties @@ -17,6 +17,7 @@ settings.Table.ColumnName.patterns=Patterns dialog.ProjectFileGroup.Add.text=Add Custom File Group dialog.ProjectFileGroup.Edit.text=Edit Custom File Group dialog.ProjectFileGroup.GroupName.text=Group name: +dialog.ProjectFileGroup.Icon.text=Icon: dialog.ProjectFileGroup.Patterns.text=File patterns: dialog.ProjectFileGroup.Patterns.description=Examples: *.md, LICENSE, README.md, *.txt, .gitignore dialog.ProjectFileGroup.Table.ColumnName.text=Pattern