From 7f28674e83b6d6aaa4bf5dcfa557edc057d1a734 Mon Sep 17 00:00:00 2001 From: Yauheni Slizh Date: Sat, 31 Jan 2026 18:16:03 +0100 Subject: [PATCH 1/4] Add ktlint to the project --- build.gradle.kts | 25 ++-- .../typingplugin/actions/StartTypingAction.kt | 25 ++-- .../kiolk/typingplugin/ui/TypingDialog.kt | 111 ++++++++++-------- 3 files changed, 91 insertions(+), 70 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 929c850..d3e016e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,6 +2,7 @@ plugins { id("java") id("org.jetbrains.kotlin.jvm") version "1.9.22" id("org.jetbrains.intellij.platform") version "2.1.0" + id("org.jlleitschuh.gradle.ktlint") version "12.1.2" } version = "1.0.3" @@ -30,20 +31,22 @@ intellijPlatform { ideaVersion { sinceBuild = "241" untilBuild = "253.*" - // untilBuild is omitted to allow compatibility with all future versions + // untilBuild is omitted to allow compatibility with all future versions // and avoid "made-up" build number rejections. } } signing { // Robust handling for both literal newlines and the "\n" string - val cert = providers.environmentVariable("CERTIFICATE_CHAIN") - .orElse(providers.gradleProperty("certificateChain")) - .map { it.replace("\\n", "\n") } - - val key = providers.environmentVariable("PRIVATE_KEY") - .orElse(providers.gradleProperty("privateKey")) - .map { it.replace("\\n", "\n") } + val cert = + providers.environmentVariable("CERTIFICATE_CHAIN") + .orElse(providers.gradleProperty("certificateChain")) + .map { it.replace("\\n", "\n") } + + val key = + providers.environmentVariable("PRIVATE_KEY") + .orElse(providers.gradleProperty("privateKey")) + .map { it.replace("\\n", "\n") } certificateChain.set(cert) privateKey.set(key) @@ -59,3 +62,9 @@ intellijPlatform { kotlin { jvmToolchain(21) } + +ktlint { + verbose.set(true) + outputToConsole.set(true) + coloredOutput.set(true) +} diff --git a/src/main/kotlin/com/github/kiolk/typingplugin/actions/StartTypingAction.kt b/src/main/kotlin/com/github/kiolk/typingplugin/actions/StartTypingAction.kt index 33dbfb1..4e90b90 100644 --- a/src/main/kotlin/com/github/kiolk/typingplugin/actions/StartTypingAction.kt +++ b/src/main/kotlin/com/github/kiolk/typingplugin/actions/StartTypingAction.kt @@ -1,27 +1,28 @@ package com.github.kiolk.typingplugin.actions +import com.github.kiolk.typingplugin.ui.TypingDialog import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.CommonDataKeys -import com.github.kiolk.typingplugin.ui.TypingDialog class StartTypingAction : AnAction() { override fun actionPerformed(e: AnActionEvent) { val project = e.project ?: return val editor = e.getData(CommonDataKeys.EDITOR) ?: return - + // Check for selection val selectionModel = editor.selectionModel - val textToType = if (selectionModel.hasSelection()) { - selectionModel.selectedText ?: editor.document.text - } else { - editor.document.text - } - - // Clean up the selected text: - // If we select a middle block, we might want to trim initial common indentation + val textToType = + if (selectionModel.hasSelection()) { + selectionModel.selectedText ?: editor.document.text + } else { + editor.document.text + } + + // Clean up the selected text: + // If we select a middle block, we might want to trim initial common indentation val cleanedText = textToType.trimIndent() - + val typingDialog = TypingDialog(project, cleanedText) typingDialog.show() } @@ -29,7 +30,7 @@ class StartTypingAction : AnAction() { override fun update(e: AnActionEvent) { val editor = e.getData(CommonDataKeys.EDITOR) e.presentation.isEnabledAndVisible = editor != null - + // Optional: Update text based on selection if (editor?.selectionModel?.hasSelection() == true) { e.presentation.text = "Type Selected Area" diff --git a/src/main/kotlin/com/github/kiolk/typingplugin/ui/TypingDialog.kt b/src/main/kotlin/com/github/kiolk/typingplugin/ui/TypingDialog.kt index c59b0d4..d3d6f97 100644 --- a/src/main/kotlin/com/github/kiolk/typingplugin/ui/TypingDialog.kt +++ b/src/main/kotlin/com/github/kiolk/typingplugin/ui/TypingDialog.kt @@ -17,7 +17,6 @@ import javax.swing.text.SimpleAttributeSet import javax.swing.text.StyleConstants class TypingDialog(private val project: Project, private val sourceCode: String) : DialogWrapper(project) { - private var currentIndex = 0 private var errorCount = 0 private var startTime: Long = 0 @@ -25,20 +24,24 @@ class TypingDialog(private val project: Project, private val sourceCode: String) private val skippedIndices = mutableSetOf() // Styles - private val ghostAttributes = SimpleAttributeSet().apply { - StyleConstants.setForeground(this, Color.GRAY) - } - private val correctAttributes = SimpleAttributeSet().apply { - StyleConstants.setForeground(this, Color.GREEN) - } - private val wrongAttributes = SimpleAttributeSet().apply { - StyleConstants.setForeground(this, Color.RED) - StyleConstants.setUnderline(this, true) - } - private val cursorAttributes = SimpleAttributeSet().apply { - StyleConstants.setBackground(this, Color.LIGHT_GRAY) - StyleConstants.setForeground(this, Color.BLACK) - } + private val ghostAttributes = + SimpleAttributeSet().apply { + StyleConstants.setForeground(this, Color.GRAY) + } + private val correctAttributes = + SimpleAttributeSet().apply { + StyleConstants.setForeground(this, Color.GREEN) + } + private val wrongAttributes = + SimpleAttributeSet().apply { + StyleConstants.setForeground(this, Color.RED) + StyleConstants.setUnderline(this, true) + } + private val cursorAttributes = + SimpleAttributeSet().apply { + StyleConstants.setBackground(this, Color.LIGHT_GRAY) + StyleConstants.setForeground(this, Color.BLACK) + } init { title = "Typing Training" @@ -47,13 +50,13 @@ class TypingDialog(private val project: Project, private val sourceCode: String) override fun createCenterPanel(): JComponent { val panel = JPanel(BorderLayout()) - + textPane.apply { text = sourceCode isEditable = false background = EditorColorsManager.getInstance().globalScheme.defaultBackground font = EditorColorsManager.getInstance().globalScheme.getFont(EditorFontType.PLAIN) - + StyleConstants.setBackground(ghostAttributes, background) StyleConstants.setBackground(correctAttributes, background) StyleConstants.setBackground(wrongAttributes, background) @@ -62,30 +65,32 @@ class TypingDialog(private val project: Project, private val sourceCode: String) textPane.styledDocument.setCharacterAttributes(0, sourceCode.length, ghostAttributes, true) updateCursor() - addKeyListener(object : KeyAdapter() { - override fun keyTyped(e: KeyEvent) { - if (e.keyChar.code < 32 || e.keyChar.code == 127) return - if (startTime == 0L) startTime = System.currentTimeMillis() - handleTyping(e.keyChar) - } + addKeyListener( + object : KeyAdapter() { + override fun keyTyped(e: KeyEvent) { + if (e.keyChar.code < 32 || e.keyChar.code == 127) return + if (startTime == 0L) startTime = System.currentTimeMillis() + handleTyping(e.keyChar) + } - override fun keyPressed(e: KeyEvent) { - when (e.keyCode) { - KeyEvent.VK_BACK_SPACE, KeyEvent.VK_DELETE, KeyEvent.VK_CLEAR -> { - handleBackspace() - } - KeyEvent.VK_ENTER -> { - if (startTime == 0L) startTime = System.currentTimeMillis() - handleTyping('\n') + override fun keyPressed(e: KeyEvent) { + when (e.keyCode) { + KeyEvent.VK_BACK_SPACE, KeyEvent.VK_DELETE, KeyEvent.VK_CLEAR -> { + handleBackspace() + } + KeyEvent.VK_ENTER -> { + if (startTime == 0L) startTime = System.currentTimeMillis() + handleTyping('\n') + } } } - } - }) + }, + ) } panel.add(JBScrollPane(textPane), BorderLayout.CENTER) panel.preferredSize = java.awt.Dimension(800, 600) - + return panel } @@ -98,11 +103,11 @@ class TypingDialog(private val project: Project, private val sourceCode: String) val targetChar = sourceCode[currentIndex] val isNewlineMatch = (targetChar == '\n' || targetChar == '\r') && (charTyped == '\n' || charTyped == '\r') - + if (charTyped == targetChar || isNewlineMatch) { textPane.styledDocument.setCharacterAttributes(currentIndex, 1, correctAttributes, true) - - if (targetChar == '\r' && currentIndex + 1 < sourceCode.length && sourceCode[currentIndex+1] == '\n') { + + if (targetChar == '\r' && currentIndex + 1 < sourceCode.length && sourceCode[currentIndex + 1] == '\n') { currentIndex += 2 } else { currentIndex++ @@ -166,15 +171,16 @@ class TypingDialog(private val project: Project, private val sourceCode: String) if (currentIndex < sourceCode.length) { val attrs = textPane.styledDocument.getCharacterElement(currentIndex).attributes val isWrong = StyleConstants.getForeground(attrs) == Color.RED - - val style = if (isWrong) { - val s = SimpleAttributeSet(wrongAttributes) - StyleConstants.setBackground(s, Color.LIGHT_GRAY) - s - } else { - cursorAttributes - } - + + val style = + if (isWrong) { + val s = SimpleAttributeSet(wrongAttributes) + StyleConstants.setBackground(s, Color.LIGHT_GRAY) + s + } else { + cursorAttributes + } + textPane.styledDocument.setCharacterAttributes(currentIndex, 1, style, true) textPane.caretPosition = currentIndex } @@ -186,11 +192,16 @@ class TypingDialog(private val project: Project, private val sourceCode: String) val totalTimeMinutes = totalTimeSeconds / 60.0 val totalChars = sourceCode.length val wpm = if (totalTimeMinutes > 0) (totalChars / 5.0) / totalTimeMinutes else 0.0 - val accuracy = if (totalChars + errorCount > 0) { - (totalChars.toDouble() / (totalChars + errorCount)) * 100 - } else 0.0 + val accuracy = + if (totalChars + errorCount > 0) { + (totalChars.toDouble() / (totalChars + errorCount)) * 100 + } else { + 0.0 + } - val statsMessage = "Typing Finished!\n\nTime: ${"%.1f".format(totalTimeSeconds)}s\nWPM: ${"%.1f".format(wpm)}\nAccuracy: ${"%.1f".format(accuracy)}%\nErrors: $errorCount" + val statsMessage = "Typing Finished!\n\nTime: ${"%.1f".format( + totalTimeSeconds, + )}s\nWPM: ${"%.1f".format(wpm)}\nAccuracy: ${"%.1f".format(accuracy)}%\nErrors: $errorCount" Messages.showInfoMessage(project, statsMessage, "Session Summary") } } From 9d516946ae40616ec6c60602624ad460ba394bb1 Mon Sep 17 00:00:00 2001 From: Yauheni Slizh Date: Sat, 31 Jan 2026 22:09:03 +0100 Subject: [PATCH 2/4] Add testing basic functionality --- build.gradle.kts | 14 ++- .../typingplugin/actions/StartTypingAction.kt | 17 +++- .../actions/StartTypingActionTest.kt | 93 +++++++++++++++++++ .../kiolk/typingplugin/ui/TypingDialogTest.kt | 92 ++++++++++++++++++ 4 files changed, 209 insertions(+), 7 deletions(-) create mode 100644 src/test/kotlin/com/github/kiolk/typingplugin/actions/StartTypingActionTest.kt create mode 100644 src/test/kotlin/com/github/kiolk/typingplugin/ui/TypingDialogTest.kt diff --git a/build.gradle.kts b/build.gradle.kts index d3e016e..c027319 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.intellij.platform.gradle.TestFrameworkType + plugins { id("java") id("org.jetbrains.kotlin.jvm") version "1.9.22" @@ -21,8 +23,13 @@ dependencies { intellijIdeaCommunity("2024.3") bundledPlugins("com.intellij.java") instrumentationTools() + testFramework(TestFrameworkType.Platform) zipSigner() } + testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.2") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.2") + testRuntimeOnly("org.junit.vintage:junit-vintage-engine:5.10.2") + testImplementation("junit:junit:4.13.2") } intellijPlatform { @@ -31,13 +38,10 @@ intellijPlatform { ideaVersion { sinceBuild = "241" untilBuild = "253.*" - // untilBuild is omitted to allow compatibility with all future versions - // and avoid "made-up" build number rejections. } } signing { - // Robust handling for both literal newlines and the "\n" string val cert = providers.environmentVariable("CERTIFICATE_CHAIN") .orElse(providers.gradleProperty("certificateChain")) @@ -68,3 +72,7 @@ ktlint { outputToConsole.set(true) coloredOutput.set(true) } + +tasks.test { + useJUnitPlatform() +} diff --git a/src/main/kotlin/com/github/kiolk/typingplugin/actions/StartTypingAction.kt b/src/main/kotlin/com/github/kiolk/typingplugin/actions/StartTypingAction.kt index 4e90b90..15df628 100644 --- a/src/main/kotlin/com/github/kiolk/typingplugin/actions/StartTypingAction.kt +++ b/src/main/kotlin/com/github/kiolk/typingplugin/actions/StartTypingAction.kt @@ -4,13 +4,21 @@ import com.github.kiolk.typingplugin.ui.TypingDialog import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project class StartTypingAction : AnAction() { override fun actionPerformed(e: AnActionEvent) { val project = e.project ?: return val editor = e.getData(CommonDataKeys.EDITOR) ?: return - // Check for selection + val cleanedText = getCleanedText(editor) + + val typingDialog = createTypingDialog(project, cleanedText) + typingDialog.show() + } + + fun getCleanedText(editor: Editor): String { val selectionModel = editor.selectionModel val textToType = if (selectionModel.hasSelection()) { @@ -21,10 +29,11 @@ class StartTypingAction : AnAction() { // Clean up the selected text: // If we select a middle block, we might want to trim initial common indentation - val cleanedText = textToType.trimIndent() + return textToType.trimIndent() + } - val typingDialog = TypingDialog(project, cleanedText) - typingDialog.show() + fun createTypingDialog(project: Project, text: String): TypingDialog { + return TypingDialog(project, text) } override fun update(e: AnActionEvent) { diff --git a/src/test/kotlin/com/github/kiolk/typingplugin/actions/StartTypingActionTest.kt b/src/test/kotlin/com/github/kiolk/typingplugin/actions/StartTypingActionTest.kt new file mode 100644 index 0000000..39b18bc --- /dev/null +++ b/src/test/kotlin/com/github/kiolk/typingplugin/actions/StartTypingActionTest.kt @@ -0,0 +1,93 @@ +package com.github.kiolk.typingplugin.actions + +import com.intellij.testFramework.fixtures.BasePlatformTestCase +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.testFramework.TestActionEvent +import org.junit.Test +import org.junit.Assert.* + +class StartTypingActionTest : BasePlatformTestCase() { + + @Test + fun testActionIsDisabledWhenNoEditor() { + val action = StartTypingAction() + val event = TestActionEvent.createTestEvent(action) { dataId -> + if (CommonDataKeys.EDITOR.name == dataId) null else null + } + + action.update(event) + + assertFalse(event.presentation.isEnabledAndVisible) + } + + @Test + fun testActionIsEnabledWhenEditorIsPresent() { + myFixture.configureByText("Test.java", "public class Test {}") + val action = StartTypingAction() + val event = TestActionEvent.createTestEvent(action) { dataId -> + when (dataId) { + CommonDataKeys.EDITOR.name -> myFixture.editor + CommonDataKeys.PROJECT.name -> project + else -> null + } + } + + action.update(event) + + assertTrue(event.presentation.isEnabledAndVisible) + assertEquals("Type This Class", event.presentation.text) + } + + @Test + fun testActionTextChangesWhenTextIsSelected() { + myFixture.configureByText("Test.java", "public class Test {}") + val action = StartTypingAction() + val event = TestActionEvent.createTestEvent(action) { dataId -> + when (dataId) { + CommonDataKeys.EDITOR.name -> myFixture.editor + CommonDataKeys.PROJECT.name -> project + else -> null + } + } + + action.update(event) + + assertTrue(event.presentation.isEnabledAndVisible) + assertEquals("Type Selected Area", event.presentation.text) + } + + @Test + fun testGetCleanedTextReturnsFullTextWhenNoSelection() { + val content = "public class Test {}" + myFixture.configureByText("Test.java", content) + val action = StartTypingAction() + + val cleanedText = action.getCleanedText(myFixture.editor) + + assertEquals(content, cleanedText) + } + + @Test + fun testGetCleanedTextReturnsTrimmedSelection() { + // We align the selection start with the indentation we want to preserve/trim consistently. + val content = """ + public class Test { + public void main() { + System.out.println("Hello"); + System.out.println("World"); + } + } + """.trimIndent() + + myFixture.configureByText("Test.java", content) + val action = StartTypingAction() + + val cleanedText = action.getCleanedText(myFixture.editor) + + val expected = """ + System.out.println("Hello"); + System.out.println("World"); + """.trimIndent() + assertEquals(expected, cleanedText) + } +} diff --git a/src/test/kotlin/com/github/kiolk/typingplugin/ui/TypingDialogTest.kt b/src/test/kotlin/com/github/kiolk/typingplugin/ui/TypingDialogTest.kt new file mode 100644 index 0000000..1dcdafc --- /dev/null +++ b/src/test/kotlin/com/github/kiolk/typingplugin/ui/TypingDialogTest.kt @@ -0,0 +1,92 @@ +package com.github.kiolk.typingplugin.ui + +import com.intellij.testFramework.fixtures.BasePlatformTestCase +import org.junit.Test +import org.junit.Assert.* +import java.awt.Color +import java.awt.event.KeyEvent +import javax.swing.JTextPane +import javax.swing.text.StyleConstants + +class TypingDialogTest : BasePlatformTestCase() { + + private lateinit var dialog: TypingDialog + private lateinit var textPane: JTextPane + private val sourceCode = "public class Test {}" + + override fun setUp() { + super.setUp() + // TypingDialog needs to be initialized on EDT potentially, + // but for unit testing logic we might get away with it or use invokeAndWait. + dialog = TypingDialog(project, sourceCode) + textPane = dialog.getPreferredFocusedComponent() as JTextPane + } + + @Test + fun testInitialState() { + assertEquals(sourceCode, textPane.text) + val attrs = textPane.styledDocument.getCharacterElement(0).attributes + // Cursor should be at 0 (background LIGHT_GRAY) + assertEquals(Color.LIGHT_GRAY, StyleConstants.getBackground(attrs)) + } + + @Test + fun testTypingCorrectCharacter() { + simulateType('p') + + // Character 'p' (index 0) should now be GREEN + val attrs0 = textPane.styledDocument.getCharacterElement(0).attributes + assertEquals(Color.GREEN, StyleConstants.getForeground(attrs0)) + + // Cursor should move to index 1 + val attrs1 = textPane.styledDocument.getCharacterElement(1).attributes + assertEquals(Color.LIGHT_GRAY, StyleConstants.getBackground(attrs1)) + } + + @Test + fun testTypingWrongCharacter() { + simulateType('x') + + // Character at index 0 should be RED + val attrs = textPane.styledDocument.getCharacterElement(0).attributes + assertEquals(Color.RED, StyleConstants.getForeground(attrs)) + assertTrue(StyleConstants.isUnderline(attrs)) + } + + @Test + fun testSkipLeadingWhitespaceAfterNewline() { + val codeWithNewline = "a\n b" + val dialog = TypingDialog(project, codeWithNewline) + val pane = dialog.getPreferredFocusedComponent() as JTextPane + + // Type 'a' + val keyEventA = KeyEvent(pane, KeyEvent.KEY_TYPED, System.currentTimeMillis(), 0, KeyEvent.VK_UNDEFINED, 'a') + pane.keyListeners.forEach { it.keyTyped(keyEventA) } + + // Type Enter + val keyEventEnter = KeyEvent(pane, KeyEvent.KEY_PRESSED, System.currentTimeMillis(), 0, KeyEvent.VK_ENTER, KeyEvent.CHAR_UNDEFINED) + pane.keyListeners.forEach { it.keyPressed(keyEventEnter) } + + // After Enter, it should skip 4 spaces and land on 'b' + // Index 0: 'a' (Correct) + // Index 1: '\n' (Correct) + // Index 2,3,4,5: ' ' (Skipped - Correct) + // Index 6: 'b' (Cursor) + + val attrsB = pane.styledDocument.getCharacterElement(6).attributes + assertEquals(Color.LIGHT_GRAY, StyleConstants.getBackground(attrsB)) + + val attrsSpace = pane.styledDocument.getCharacterElement(2).attributes + assertEquals(Color.GREEN, StyleConstants.getForeground(attrsSpace)) + } + + private fun simulateType(char: Char) { + val keyEvent = KeyEvent(textPane, KeyEvent.KEY_TYPED, System.currentTimeMillis(), 0, KeyEvent.VK_UNDEFINED, char) + textPane.keyListeners.forEach { it.keyTyped(keyEvent) } + } + + private fun simulateKeyPress(keyCode: Int) { + val keyEvent = KeyEvent(textPane, KeyEvent.KEY_PRESSED, System.currentTimeMillis(), 0, keyCode, KeyEvent.CHAR_UNDEFINED) + textPane.keyListeners.forEach { it.keyPressed(keyEvent) } + } +} From d098708a766b10a7751956d1bf16a36e7a4f0b73 Mon Sep 17 00:00:00 2001 From: Yauheni Slizh Date: Sat, 31 Jan 2026 22:15:10 +0100 Subject: [PATCH 3/4] Fix issue with dragging dialog screen --- .github/workflows/pr-check.yml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .github/workflows/pr-check.yml diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml new file mode 100644 index 0000000..d90a41b --- /dev/null +++ b/.github/workflows/pr-check.yml @@ -0,0 +1,28 @@ +name: PR Check + +on: + pull_request: + branches: [ main, master ] + +jobs: + check: + name: ktlint and unit tests + runs-on: ubuntu-latest + steps: + - name: Fetch Sources + uses: actions/checkout@v4 + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: '21' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Run Ktlint Check + run: ./gradlew ktlintCheck + + - name: Run Unit Tests + run: ./gradlew test From 0798f49da3efb74493360925c9786e7a8873d7d8 Mon Sep 17 00:00:00 2001 From: Yauheni Slizh Date: Sat, 31 Jan 2026 22:18:40 +0100 Subject: [PATCH 4/4] Bump version to 1.0.4 --- build.gradle.kts | 13 ++++- .../typingplugin/actions/StartTypingAction.kt | 5 +- .../kiolk/typingplugin/ui/TypingDialog.kt | 37 ++++++++++++- .../actions/StartTypingActionTest.kt | 55 ++++++++++--------- .../kiolk/typingplugin/ui/TypingDialogTest.kt | 20 +++---- 5 files changed, 90 insertions(+), 40 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index c027319..a858d4d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,7 +7,7 @@ plugins { id("org.jlleitschuh.gradle.ktlint") version "12.1.2" } -version = "1.0.3" +version = "1.0.4" group = "com.github.kiolk.typingplugin" @@ -35,6 +35,17 @@ dependencies { intellijPlatform { pluginConfiguration { name = "Typing Training" + description = + """ + Improved stability and usability. + - Added ability to drag typing dialog to any position on the screen. + - Improved stability. + """.trimIndent() + changeNotes = + """ + - Added ability to drag typing dialog to any position on the screen. + - Improved stability. + """.trimIndent() ideaVersion { sinceBuild = "241" untilBuild = "253.*" diff --git a/src/main/kotlin/com/github/kiolk/typingplugin/actions/StartTypingAction.kt b/src/main/kotlin/com/github/kiolk/typingplugin/actions/StartTypingAction.kt index 15df628..168ac5d 100644 --- a/src/main/kotlin/com/github/kiolk/typingplugin/actions/StartTypingAction.kt +++ b/src/main/kotlin/com/github/kiolk/typingplugin/actions/StartTypingAction.kt @@ -32,7 +32,10 @@ class StartTypingAction : AnAction() { return textToType.trimIndent() } - fun createTypingDialog(project: Project, text: String): TypingDialog { + fun createTypingDialog( + project: Project, + text: String, + ): TypingDialog { return TypingDialog(project, text) } diff --git a/src/main/kotlin/com/github/kiolk/typingplugin/ui/TypingDialog.kt b/src/main/kotlin/com/github/kiolk/typingplugin/ui/TypingDialog.kt index d3d6f97..5256044 100644 --- a/src/main/kotlin/com/github/kiolk/typingplugin/ui/TypingDialog.kt +++ b/src/main/kotlin/com/github/kiolk/typingplugin/ui/TypingDialog.kt @@ -8,11 +8,15 @@ import com.intellij.openapi.ui.Messages import com.intellij.ui.components.JBScrollPane import java.awt.BorderLayout import java.awt.Color +import java.awt.Point import java.awt.event.KeyAdapter import java.awt.event.KeyEvent +import java.awt.event.MouseAdapter +import java.awt.event.MouseEvent import javax.swing.JComponent import javax.swing.JPanel import javax.swing.JTextPane +import javax.swing.SwingUtilities import javax.swing.text.SimpleAttributeSet import javax.swing.text.StyleConstants @@ -45,12 +49,38 @@ class TypingDialog(private val project: Project, private val sourceCode: String) init { title = "Typing Training" + isModal = false init() } + override fun getDimensionServiceKey(): String? = "com.github.kiolk.typingplugin.ui.TypingDialog" + override fun createCenterPanel(): JComponent { val panel = JPanel(BorderLayout()) + val dragListener = + object : MouseAdapter() { + private var initialScreenClick: Point? = null + private var initialWindowLocation: Point? = null + + override fun mousePressed(e: MouseEvent) { + initialScreenClick = e.locationOnScreen + initialWindowLocation = SwingUtilities.getWindowAncestor(panel)?.location + } + + override fun mouseDragged(e: MouseEvent) { + val window = SwingUtilities.getWindowAncestor(panel) + if (window != null && initialScreenClick != null && initialWindowLocation != null) { + val deltaX = e.locationOnScreen.x - initialScreenClick!!.x + val deltaY = e.locationOnScreen.y - initialScreenClick!!.y + window.setLocation(initialWindowLocation!!.x + deltaX, initialWindowLocation!!.y + deltaY) + } + } + } + + panel.addMouseListener(dragListener) + panel.addMouseMotionListener(dragListener) + textPane.apply { text = sourceCode isEditable = false @@ -86,9 +116,14 @@ class TypingDialog(private val project: Project, private val sourceCode: String) } }, ) + addMouseListener(dragListener) + addMouseMotionListener(dragListener) } - panel.add(JBScrollPane(textPane), BorderLayout.CENTER) + val scrollPane = JBScrollPane(textPane) + scrollPane.addMouseListener(dragListener) + scrollPane.addMouseMotionListener(dragListener) + panel.add(scrollPane, BorderLayout.CENTER) panel.preferredSize = java.awt.Dimension(800, 600) return panel diff --git a/src/test/kotlin/com/github/kiolk/typingplugin/actions/StartTypingActionTest.kt b/src/test/kotlin/com/github/kiolk/typingplugin/actions/StartTypingActionTest.kt index 39b18bc..0844bd1 100644 --- a/src/test/kotlin/com/github/kiolk/typingplugin/actions/StartTypingActionTest.kt +++ b/src/test/kotlin/com/github/kiolk/typingplugin/actions/StartTypingActionTest.kt @@ -1,19 +1,18 @@ package com.github.kiolk.typingplugin.actions -import com.intellij.testFramework.fixtures.BasePlatformTestCase import com.intellij.openapi.actionSystem.CommonDataKeys import com.intellij.testFramework.TestActionEvent +import com.intellij.testFramework.fixtures.BasePlatformTestCase import org.junit.Test -import org.junit.Assert.* class StartTypingActionTest : BasePlatformTestCase() { - @Test fun testActionIsDisabledWhenNoEditor() { val action = StartTypingAction() - val event = TestActionEvent.createTestEvent(action) { dataId -> - if (CommonDataKeys.EDITOR.name == dataId) null else null - } + val event = + TestActionEvent.createTestEvent(action) { dataId -> + if (CommonDataKeys.EDITOR.name == dataId) null else null + } action.update(event) @@ -24,13 +23,14 @@ class StartTypingActionTest : BasePlatformTestCase() { fun testActionIsEnabledWhenEditorIsPresent() { myFixture.configureByText("Test.java", "public class Test {}") val action = StartTypingAction() - val event = TestActionEvent.createTestEvent(action) { dataId -> - when (dataId) { - CommonDataKeys.EDITOR.name -> myFixture.editor - CommonDataKeys.PROJECT.name -> project - else -> null + val event = + TestActionEvent.createTestEvent(action) { dataId -> + when (dataId) { + CommonDataKeys.EDITOR.name -> myFixture.editor + CommonDataKeys.PROJECT.name -> project + else -> null + } } - } action.update(event) @@ -42,13 +42,14 @@ class StartTypingActionTest : BasePlatformTestCase() { fun testActionTextChangesWhenTextIsSelected() { myFixture.configureByText("Test.java", "public class Test {}") val action = StartTypingAction() - val event = TestActionEvent.createTestEvent(action) { dataId -> - when (dataId) { - CommonDataKeys.EDITOR.name -> myFixture.editor - CommonDataKeys.PROJECT.name -> project - else -> null + val event = + TestActionEvent.createTestEvent(action) { dataId -> + when (dataId) { + CommonDataKeys.EDITOR.name -> myFixture.editor + CommonDataKeys.PROJECT.name -> project + else -> null + } } - } action.update(event) @@ -61,33 +62,35 @@ class StartTypingActionTest : BasePlatformTestCase() { val content = "public class Test {}" myFixture.configureByText("Test.java", content) val action = StartTypingAction() - + val cleanedText = action.getCleanedText(myFixture.editor) - + assertEquals(content, cleanedText) } @Test fun testGetCleanedTextReturnsTrimmedSelection() { // We align the selection start with the indentation we want to preserve/trim consistently. - val content = """ + val content = + """ public class Test { public void main() { System.out.println("Hello"); System.out.println("World"); } } - """.trimIndent() + """.trimIndent() myFixture.configureByText("Test.java", content) val action = StartTypingAction() - + val cleanedText = action.getCleanedText(myFixture.editor) - - val expected = """ + + val expected = + """ System.out.println("Hello"); System.out.println("World"); - """.trimIndent() + """.trimIndent() assertEquals(expected, cleanedText) } } diff --git a/src/test/kotlin/com/github/kiolk/typingplugin/ui/TypingDialogTest.kt b/src/test/kotlin/com/github/kiolk/typingplugin/ui/TypingDialogTest.kt index 1dcdafc..3ff2f3d 100644 --- a/src/test/kotlin/com/github/kiolk/typingplugin/ui/TypingDialogTest.kt +++ b/src/test/kotlin/com/github/kiolk/typingplugin/ui/TypingDialogTest.kt @@ -2,21 +2,19 @@ package com.github.kiolk.typingplugin.ui import com.intellij.testFramework.fixtures.BasePlatformTestCase import org.junit.Test -import org.junit.Assert.* import java.awt.Color import java.awt.event.KeyEvent import javax.swing.JTextPane import javax.swing.text.StyleConstants class TypingDialogTest : BasePlatformTestCase() { - private lateinit var dialog: TypingDialog private lateinit var textPane: JTextPane private val sourceCode = "public class Test {}" override fun setUp() { super.setUp() - // TypingDialog needs to be initialized on EDT potentially, + // TypingDialog needs to be initialized on EDT potentially, // but for unit testing logic we might get away with it or use invokeAndWait. dialog = TypingDialog(project, sourceCode) textPane = dialog.getPreferredFocusedComponent() as JTextPane @@ -33,11 +31,11 @@ class TypingDialogTest : BasePlatformTestCase() { @Test fun testTypingCorrectCharacter() { simulateType('p') - + // Character 'p' (index 0) should now be GREEN val attrs0 = textPane.styledDocument.getCharacterElement(0).attributes assertEquals(Color.GREEN, StyleConstants.getForeground(attrs0)) - + // Cursor should move to index 1 val attrs1 = textPane.styledDocument.getCharacterElement(1).attributes assertEquals(Color.LIGHT_GRAY, StyleConstants.getBackground(attrs1)) @@ -46,7 +44,7 @@ class TypingDialogTest : BasePlatformTestCase() { @Test fun testTypingWrongCharacter() { simulateType('x') - + // Character at index 0 should be RED val attrs = textPane.styledDocument.getCharacterElement(0).attributes assertEquals(Color.RED, StyleConstants.getForeground(attrs)) @@ -58,24 +56,24 @@ class TypingDialogTest : BasePlatformTestCase() { val codeWithNewline = "a\n b" val dialog = TypingDialog(project, codeWithNewline) val pane = dialog.getPreferredFocusedComponent() as JTextPane - + // Type 'a' val keyEventA = KeyEvent(pane, KeyEvent.KEY_TYPED, System.currentTimeMillis(), 0, KeyEvent.VK_UNDEFINED, 'a') pane.keyListeners.forEach { it.keyTyped(keyEventA) } - + // Type Enter val keyEventEnter = KeyEvent(pane, KeyEvent.KEY_PRESSED, System.currentTimeMillis(), 0, KeyEvent.VK_ENTER, KeyEvent.CHAR_UNDEFINED) pane.keyListeners.forEach { it.keyPressed(keyEventEnter) } - + // After Enter, it should skip 4 spaces and land on 'b' // Index 0: 'a' (Correct) // Index 1: '\n' (Correct) // Index 2,3,4,5: ' ' (Skipped - Correct) // Index 6: 'b' (Cursor) - + val attrsB = pane.styledDocument.getCharacterElement(6).attributes assertEquals(Color.LIGHT_GRAY, StyleConstants.getBackground(attrsB)) - + val attrsSpace = pane.styledDocument.getCharacterElement(2).attributes assertEquals(Color.GREEN, StyleConstants.getForeground(attrsSpace)) }