From f1d6351668099272e0f8e1467bef0da7106f8a57 Mon Sep 17 00:00:00 2001 From: Traben Date: Thu, 25 Sep 2025 01:10:41 +1000 Subject: [PATCH 1/6] double click support to open preprocessed source --- .../utils/SourceSetFileDialog.kt | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/main/kotlin/org/polyfrost/intelliprocessor/utils/SourceSetFileDialog.kt b/src/main/kotlin/org/polyfrost/intelliprocessor/utils/SourceSetFileDialog.kt index 8b16d47..3b06d03 100644 --- a/src/main/kotlin/org/polyfrost/intelliprocessor/utils/SourceSetFileDialog.kt +++ b/src/main/kotlin/org/polyfrost/intelliprocessor/utils/SourceSetFileDialog.kt @@ -43,7 +43,25 @@ class SourceSetFileDialog( ) } } + + // Double click also triggers doOKAction() + addMouseListener(object : MouseAdapter() { + private var lastClickTime = 0L + private var lastSelectedIndex = -1 + + override fun mouseClicked(e: MouseEvent?) { + if (e?.button != MouseEvent.BUTTON1) return + + val clickTime = System.currentTimeMillis() + if (selectedIndex == lastSelectedIndex && clickTime - lastClickTime < 1000) { // 1 second threshold + doOKAction() + } + lastClickTime = clickTime + lastSelectedIndex = selectedIndex + } + }) } + private val search = SearchTextField() init { title = "Select Preprocessed Source File" From 07a07442532f8dbc390417cba6222040eca477ae Mon Sep 17 00:00:00 2001 From: Traben Date: Thu, 25 Sep 2025 01:11:45 +1000 Subject: [PATCH 2/6] Focus the search field when the dialog is first opened --- .../intelliprocessor/utils/SourceSetFileDialog.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/org/polyfrost/intelliprocessor/utils/SourceSetFileDialog.kt b/src/main/kotlin/org/polyfrost/intelliprocessor/utils/SourceSetFileDialog.kt index 3b06d03..9378ea5 100644 --- a/src/main/kotlin/org/polyfrost/intelliprocessor/utils/SourceSetFileDialog.kt +++ b/src/main/kotlin/org/polyfrost/intelliprocessor/utils/SourceSetFileDialog.kt @@ -9,6 +9,8 @@ import com.intellij.ui.components.JBList import com.intellij.ui.components.JBScrollPane import java.awt.BorderLayout import java.awt.Component +import java.awt.event.MouseAdapter +import java.awt.event.MouseEvent import java.io.File import javax.swing.DefaultListCellRenderer import javax.swing.JComponent @@ -68,9 +70,12 @@ class SourceSetFileDialog( init() } + // Focus the search field when the dialog is first opened, streamlines keyboard navigation + override fun getPreferredFocusedComponent(): JComponent? = search + override fun createCenterPanel(): JComponent { val panel = JPanel(BorderLayout()) - val search = SearchTextField() + search.addDocumentListener(object : DocumentAdapter() { override fun textChanged(e: DocumentEvent) { val filter = search.text.lowercase() From f42f10efc69a5dbe9d31e1c73a238b82635dbbd0 Mon Sep 17 00:00:00 2001 From: Traben Date: Fri, 26 Sep 2025 02:29:55 +1000 Subject: [PATCH 3/6] select the only search result for keyboard navigation streamlining --- .../polyfrost/intelliprocessor/utils/SourceSetFileDialog.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/kotlin/org/polyfrost/intelliprocessor/utils/SourceSetFileDialog.kt b/src/main/kotlin/org/polyfrost/intelliprocessor/utils/SourceSetFileDialog.kt index 9378ea5..f88839c 100644 --- a/src/main/kotlin/org/polyfrost/intelliprocessor/utils/SourceSetFileDialog.kt +++ b/src/main/kotlin/org/polyfrost/intelliprocessor/utils/SourceSetFileDialog.kt @@ -82,6 +82,11 @@ class SourceSetFileDialog( listModel.replaceAll(sourceFiles.filter { it.toRelativePath().toString().lowercase().contains(filter) }) + + // Improve keyboard navigation by auto-selecting the only remaining result + if (listModel.size == 1) { + list.setSelectedValue(listModel.getElementAt(0), false) + } } }) From aac0032fd3f51b92e4fbb6712870848ad54fd496 Mon Sep 17 00:00:00 2001 From: Traben Date: Fri, 26 Sep 2025 02:43:39 +1000 Subject: [PATCH 4/6] refactor `SourceSet`s and the list for the `PreprocessorJumpAction`: - correct list ordering - support override files from `versions//src/` - remove redundant full path from list string - layout list cells into nicer columns - add simple "key" to streamline keyboard searching further - add indicators for main & override files --- .../action/PreprocessorFileJumpAction.kt | 36 ++++++++--- .../intelliprocessor/utils/SourceSetFile.kt | 50 +++++++++++++++- .../utils/SourceSetFileDialog.kt | 59 ++++++++++++------- 3 files changed, 111 insertions(+), 34 deletions(-) diff --git a/src/main/kotlin/org/polyfrost/intelliprocessor/action/PreprocessorFileJumpAction.kt b/src/main/kotlin/org/polyfrost/intelliprocessor/action/PreprocessorFileJumpAction.kt index 6b72997..5e69feb 100644 --- a/src/main/kotlin/org/polyfrost/intelliprocessor/action/PreprocessorFileJumpAction.kt +++ b/src/main/kotlin/org/polyfrost/intelliprocessor/action/PreprocessorFileJumpAction.kt @@ -42,7 +42,7 @@ class PreprocessorFileJumpAction : DumbAwareAction() { val projectPath = currentlyEditingFile.relativeToOrNull(rootDirectory)?.toList() ?: return warning(project, "Current file not in project root") - val currentSourceSetFile = getSourceSetFrom(projectPath) + val currentSourceSetFile = getSourceSetFrom(projectPath, mainVersion, rootDirectory) ?: return warning(project, "File does not seem to be a preprocessor source or generated file") val allVersions = identifyVersionDirectories(rootDirectory) @@ -50,18 +50,18 @@ class PreprocessorFileJumpAction : DumbAwareAction() { return warning(project, "Could not find any preprocessed source sets. Make sure to build your project") } - val targets = allVersions.map { currentSourceSetFile.copy(version = it) } - .filter { it.version != mainVersion } // The preprocessed sources are not generated for the main project + val targets = allVersions.map { currentSourceSetFile.copy(subVersion = it) } + .filter { it.subVersion != mainVersion } // The preprocessed sources are not generated for the main project val ideView = LangDataKeys.IDE_VIEW.getData(e.dataContext) ?: return warning(project, "Could not find IDE view") val caret = editor.caretModel.currentCaret.visualPosition - SourceSetFileDialog(project, mainVersion, targets) { selected -> - val virtualFile = VfsUtil.findFile(rootDirectory.resolve(selected.toRelativePath()), true) + SourceSetFileDialog(project, targets) { selected -> + val virtualFile = VfsUtil.findFile(rootDirectory.resolve(selected.navigatePath()), true) if (virtualFile == null) { warning( project, - "Could not find file for version ${selected.version ?: mainVersion} on disk. Try building your project" + "Could not find file for version ${selected.displayVersion} on disk. Try building your project" ) return@SourceSetFileDialog @@ -83,18 +83,20 @@ class PreprocessorFileJumpAction : DumbAwareAction() { }.show() } - private fun getSourceSetFrom(path: List): SourceSetFile? { + private fun getSourceSetFrom(path: List, mainVersion: String, rootDirectory: Path): SourceSetFile? { if (path.size < 4) { return null } - // A path in the format of src//// + // The main file path in the format of src//// if (path[0].toString() == "src") { return SourceSetFile( path[1].toString(), path[2].toString(), path.subList(3, path.size).joinToPath(), null, + mainVersion, + rootDirectory, ) } @@ -102,7 +104,7 @@ class PreprocessorFileJumpAction : DumbAwareAction() { return null } - // A path in the format of `versions//build/preprocessed////` + // A generated preprocessed path in the format of `versions//build/preprocessed////` if (path[0].toString() == "versions" && path[2].toString() == "build" && path[3].toString() == "preprocessed" @@ -112,6 +114,22 @@ class PreprocessorFileJumpAction : DumbAwareAction() { path[5].toString(), path.subList(6, path.size).joinToPath(), path[1].toString(), + mainVersion, + rootDirectory, + ) + } + + // An override file path in the format of `versions//src////` + if (path[0].toString() == "versions" && + path[2].toString() == "src" + ) { + return SourceSetFile( + path[3].toString(), + path[4].toString(), + path.subList(5, path.size).joinToPath(), + path[1].toString(), + mainVersion, + rootDirectory, ) } diff --git a/src/main/kotlin/org/polyfrost/intelliprocessor/utils/SourceSetFile.kt b/src/main/kotlin/org/polyfrost/intelliprocessor/utils/SourceSetFile.kt index 988fa02..73a549b 100644 --- a/src/main/kotlin/org/polyfrost/intelliprocessor/utils/SourceSetFile.kt +++ b/src/main/kotlin/org/polyfrost/intelliprocessor/utils/SourceSetFile.kt @@ -6,15 +6,59 @@ data class SourceSetFile( val sourceSetName: String, val language: String, val classPath: Path, - val version: String?, + val subVersion: String?, // If null, this is the mainVersion in project/src/ + private val mainVersion: String, + private val rootDirectory: Path, ) { + val isNonGenerated = this.subVersion == null || + toOverridePath()?.let { rootDirectory.resolve(it).toFile().exists() } == true + + val displayVersion = subVersion ?: mainVersion + + // Used to sort entries as 1.8.9 will order before 1.12.2 otherwise + val versionInt = displayVersion.split('-').let { platform -> + // Convert semantic version to the preprocessor int: 1.21.2 -> 12102 + fun List.getOrZero(index: Int) = getOrNull(index)?.toIntOrNull() ?: 0 + val semVer = platform[0].split('.') + semVer.getOrZero(0) * 10000 + semVer.getOrZero(1) * 100 + semVer.getOrZero(2) + } + + // Simpler search key used to streamline keyboard navigation via search, 1.21.2-fabric -> 12102fabric + private val simpleVersion = "$versionInt${displayVersion.split('-')[1]}" fun toRelativePath(): Path { - return if (version == null) { + return if (subVersion == null) { Path.of("src", sourceSetName, language).resolve(classPath) } else { - Path.of("versions", version, "build", "preprocessed", sourceSetName, language).resolve(classPath) + Path.of("versions", subVersion, "build", "preprocessed", sourceSetName, language).resolve(classPath) + } + } + + // Refers to the path of a possible, non-generated, overriding source file in versions//src/ + fun toOverridePath(): Path? { + return if (subVersion == null) { + null + } else { + Path.of("versions", subVersion, "src", sourceSetName, language).resolve(classPath) } } + fun navigatePath(): Path { + return if (isNonGenerated) { + toOverridePath() ?: toRelativePath() + } else { + toRelativePath() + } + } + + override fun toString(): String { + val displayFile = classPath.last() + val srcMark = if (subVersion == null) "(main)" + else if (isNonGenerated) "(override)" + else "" + + // e.g. [12102fabric] | 1.21.2-fabric | MyClass.java (main) + return "[$simpleVersion] | $displayVersion | $displayFile $srcMark" + } + } diff --git a/src/main/kotlin/org/polyfrost/intelliprocessor/utils/SourceSetFileDialog.kt b/src/main/kotlin/org/polyfrost/intelliprocessor/utils/SourceSetFileDialog.kt index f88839c..d4e149a 100644 --- a/src/main/kotlin/org/polyfrost/intelliprocessor/utils/SourceSetFileDialog.kt +++ b/src/main/kotlin/org/polyfrost/intelliprocessor/utils/SourceSetFileDialog.kt @@ -4,45 +4,56 @@ import com.intellij.openapi.project.Project import com.intellij.openapi.ui.DialogWrapper import com.intellij.ui.CollectionListModel import com.intellij.ui.DocumentAdapter +import com.intellij.ui.JBColor import com.intellij.ui.SearchTextField import com.intellij.ui.components.JBList import com.intellij.ui.components.JBScrollPane import java.awt.BorderLayout -import java.awt.Component +import java.awt.Font +import java.awt.GridLayout import java.awt.event.MouseAdapter import java.awt.event.MouseEvent -import java.io.File import javax.swing.DefaultListCellRenderer import javax.swing.JComponent -import javax.swing.JList +import javax.swing.JLabel import javax.swing.JPanel +import javax.swing.ListCellRenderer import javax.swing.ListSelectionModel import javax.swing.event.DocumentEvent class SourceSetFileDialog( - private val project: Project, - private val mainVersion: String, - private val sourceFiles: List, + project: Project, + sourceFilesUnsorted: List, private val onFileChosen: (SourceSetFile) -> Unit ) : DialogWrapper(project) { + private val sourceFiles = sourceFilesUnsorted.sortedBy { it.versionInt } + private val listModel = CollectionListModel(sourceFiles) private val list = JBList(listModel).apply { selectionMode = ListSelectionModel.SINGLE_SELECTION - cellRenderer = object : DefaultListCellRenderer() { - override fun getListCellRendererComponent( - list: JList<*>?, value: Any?, index: Int, - isSelected: Boolean, cellHasFocus: Boolean - ): Component { - val item = value as SourceSetFile - val display = item.version ?: mainVersion - return super.getListCellRendererComponent( - list, - "${display}${File.separator}${item.classPath}", - index, - isSelected, - cellHasFocus - ) + cellRenderer = ListCellRenderer { list, value, index, isSelected, cellHasFocus -> + // Use DefaultListCellRenderer to get selection colors etc + fun String.label() = DefaultListCellRenderer().getListCellRendererComponent( + list, + this, + index, + isSelected, + cellHasFocus + ).apply { + // Further differentiate preprocessed generated files with font style + if ((value as SourceSetFile).isNonGenerated) { + font = font.deriveFont(Font.BOLD) + } + } + + JPanel(GridLayout(1, 2)).apply { + val stringParts = value.toString().split("|") + add(JPanel(GridLayout(1, 2)).apply { + add(" ${stringParts[0]}".label()) + add("| ${stringParts[1]}".label()) + }) + add("| ${stringParts[2]}".label()) } } @@ -80,7 +91,7 @@ class SourceSetFileDialog( override fun textChanged(e: DocumentEvent) { val filter = search.text.lowercase() listModel.replaceAll(sourceFiles.filter { - it.toRelativePath().toString().lowercase().contains(filter) + it.toString().lowercase().contains(filter) }) // Improve keyboard navigation by auto-selecting the only remaining result @@ -91,7 +102,11 @@ class SourceSetFileDialog( }) panel.add(search, BorderLayout.NORTH) - panel.add(JBScrollPane(list), BorderLayout.CENTER) + panel.add(JLabel(" (override) files are, non-generated, override files present in the versions//src/ directory.").apply { + font = font.deriveFont(Font.ITALIC, 12f) + foreground = JBColor.GRAY + }, BorderLayout.CENTER) + panel.add(JBScrollPane(list), BorderLayout.SOUTH) return panel } From 6a5f7583b3811d14865d7cad2d925284fa755640 Mon Sep 17 00:00:00 2001 From: Traben Date: Fri, 26 Sep 2025 03:06:56 +1000 Subject: [PATCH 5/6] better rearranged SourceSetFile content into: - first half `toRelativePath()` related logic - second half `toString()` related logic also removes unneeded diff by restoring `toRelativePath()` as the final path getter --- .../action/PreprocessorFileJumpAction.kt | 2 +- .../intelliprocessor/utils/SourceSetFile.kt | 43 ++++++++----------- 2 files changed, 18 insertions(+), 27 deletions(-) diff --git a/src/main/kotlin/org/polyfrost/intelliprocessor/action/PreprocessorFileJumpAction.kt b/src/main/kotlin/org/polyfrost/intelliprocessor/action/PreprocessorFileJumpAction.kt index 5e69feb..81ae2b4 100644 --- a/src/main/kotlin/org/polyfrost/intelliprocessor/action/PreprocessorFileJumpAction.kt +++ b/src/main/kotlin/org/polyfrost/intelliprocessor/action/PreprocessorFileJumpAction.kt @@ -57,7 +57,7 @@ class PreprocessorFileJumpAction : DumbAwareAction() { val caret = editor.caretModel.currentCaret.visualPosition SourceSetFileDialog(project, targets) { selected -> - val virtualFile = VfsUtil.findFile(rootDirectory.resolve(selected.navigatePath()), true) + val virtualFile = VfsUtil.findFile(rootDirectory.resolve(selected.toRelativePath()), true) if (virtualFile == null) { warning( project, diff --git a/src/main/kotlin/org/polyfrost/intelliprocessor/utils/SourceSetFile.kt b/src/main/kotlin/org/polyfrost/intelliprocessor/utils/SourceSetFile.kt index 73a549b..9f14ae1 100644 --- a/src/main/kotlin/org/polyfrost/intelliprocessor/utils/SourceSetFile.kt +++ b/src/main/kotlin/org/polyfrost/intelliprocessor/utils/SourceSetFile.kt @@ -10,8 +10,24 @@ data class SourceSetFile( private val mainVersion: String, private val rootDirectory: Path, ) { + // Refers to the path of a possible, non-generated, overriding source file in versions//src/ + private val overridePath: Path? = subVersion?.let { + Path.of("versions", it, "src", sourceSetName, language).resolve(classPath) + } + val isNonGenerated = this.subVersion == null || - toOverridePath()?.let { rootDirectory.resolve(it).toFile().exists() } == true + overridePath?.let { rootDirectory.resolve(it).toFile().exists() } == true + + fun toRelativePath(): Path { + return if (subVersion == null) { + Path.of("src", sourceSetName, language).resolve(classPath) + } else if (isNonGenerated) { + overridePath!! + } else { + Path.of("versions", subVersion, "build", "preprocessed", sourceSetName, language).resolve(classPath) + } + } + val displayVersion = subVersion ?: mainVersion @@ -26,31 +42,6 @@ data class SourceSetFile( // Simpler search key used to streamline keyboard navigation via search, 1.21.2-fabric -> 12102fabric private val simpleVersion = "$versionInt${displayVersion.split('-')[1]}" - fun toRelativePath(): Path { - return if (subVersion == null) { - Path.of("src", sourceSetName, language).resolve(classPath) - } else { - Path.of("versions", subVersion, "build", "preprocessed", sourceSetName, language).resolve(classPath) - } - } - - // Refers to the path of a possible, non-generated, overriding source file in versions//src/ - fun toOverridePath(): Path? { - return if (subVersion == null) { - null - } else { - Path.of("versions", subVersion, "src", sourceSetName, language).resolve(classPath) - } - } - - fun navigatePath(): Path { - return if (isNonGenerated) { - toOverridePath() ?: toRelativePath() - } else { - toRelativePath() - } - } - override fun toString(): String { val displayFile = classPath.last() val srcMark = if (subVersion == null) "(main)" From 5a7b2925b293537ae21449ecd07e23a01deda835 Mon Sep 17 00:00:00 2001 From: Traben Date: Fri, 26 Sep 2025 03:21:57 +1000 Subject: [PATCH 6/6] change selection to always first --- .../polyfrost/intelliprocessor/utils/SourceSetFileDialog.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/org/polyfrost/intelliprocessor/utils/SourceSetFileDialog.kt b/src/main/kotlin/org/polyfrost/intelliprocessor/utils/SourceSetFileDialog.kt index d4e149a..a0850f6 100644 --- a/src/main/kotlin/org/polyfrost/intelliprocessor/utils/SourceSetFileDialog.kt +++ b/src/main/kotlin/org/polyfrost/intelliprocessor/utils/SourceSetFileDialog.kt @@ -94,8 +94,10 @@ class SourceSetFileDialog( it.toString().lowercase().contains(filter) }) - // Improve keyboard navigation by auto-selecting the only remaining result - if (listModel.size == 1) { + if (filter.isEmpty() || listModel.isEmpty) { + list.setSelectedValue(null, false) + } else { + // Improve keyboard navigation by auto-selecting the first result list.setSelectedValue(listModel.getElementAt(0), false) } }