From 1e817d3e23a389caf4ffc7967e241e6cab5b1794 Mon Sep 17 00:00:00 2001 From: Yauheni Slizh <30602586+Kiolk@users.noreply.github.com> Date: Wed, 4 Feb 2026 21:43:32 +0100 Subject: [PATCH 1/4] Release v1.0.7: Implement dynamic resizing, zooming, persistence, and smart scrolling. Fixes #5, Fixes #6 --- build.gradle.kts | 10 +- .../typingplugin/service/TypingService.kt | 7 + .../kiolk/typingplugin/ui/TypingDialog.kt | 121 ++++++++++++++++-- src/main/resources/META-INF/plugin.xml | 9 +- 4 files changed, 122 insertions(+), 25 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 370611c..84afbe8 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.6" +version = "1.0.7" group = "com.github.kiolk.typingplugin" @@ -36,14 +36,6 @@ dependencies { intellijPlatform { pluginConfiguration { name = "Typing Training" - changeNotes = - """ - - Added performance chart at the end of each session. - - Implemented persistent statistics across IDE sessions. - - Unified session summary and performance chart into a single dialog. - - Added detailed logging for typing events and errors. - - Improved chart visualization with whole number attempt axis. - """.trimIndent() ideaVersion { sinceBuild = "241" untilBuild = "253.*" diff --git a/src/main/kotlin/com/github/kiolk/typingplugin/service/TypingService.kt b/src/main/kotlin/com/github/kiolk/typingplugin/service/TypingService.kt index 0a829b1..e39fc58 100644 --- a/src/main/kotlin/com/github/kiolk/typingplugin/service/TypingService.kt +++ b/src/main/kotlin/com/github/kiolk/typingplugin/service/TypingService.kt @@ -17,6 +17,7 @@ class TypingService(private val project: Project) : PersistentStateComponent { data class State( var results: MutableList = mutableListOf(), + var fontSize: Int = 14, // Default font size ) private var myState = State() @@ -32,6 +33,12 @@ class TypingService(private val project: Project) : fun getResults(): List = myState.results.toList() + fun getFontSize(): Int = myState.fontSize + + fun setFontSize(size: Int) { + myState.fontSize = size + } + override fun getState(): State = myState override fun loadState(state: State) { 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 3f39068..7df5454 100644 --- a/src/main/kotlin/com/github/kiolk/typingplugin/ui/TypingDialog.kt +++ b/src/main/kotlin/com/github/kiolk/typingplugin/ui/TypingDialog.kt @@ -9,17 +9,24 @@ import com.intellij.openapi.ui.DialogWrapper import com.intellij.ui.components.JBScrollPane import java.awt.BorderLayout import java.awt.Color +import java.awt.Dimension +import java.awt.Font import java.awt.Point +import java.awt.Rectangle +import java.awt.Toolkit import java.awt.event.KeyAdapter import java.awt.event.KeyEvent import java.awt.event.MouseAdapter import java.awt.event.MouseEvent +import java.awt.event.MouseWheelEvent 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 +import kotlin.math.max +import kotlin.math.min class TypingDialog(private val project: Project, private val sourceCode: String) : DialogWrapper(project) { private var currentIndex = 0 @@ -28,6 +35,8 @@ class TypingDialog(private val project: Project, private val sourceCode: String) private val textPane = JTextPane() private val skippedIndices = mutableSetOf() private val log = logger() + private val centerPanel = JPanel(BorderLayout()) + private val typingService = TypingService.getInstance(project) // Styles private val ghostAttributes = @@ -56,11 +65,9 @@ class TypingDialog(private val project: Project, private val sourceCode: String) log.info("TypingDialog initialized with source code length: ${sourceCode.length}") } - override fun getDimensionServiceKey(): String? = "com.github.kiolk.typingplugin.ui.TypingDialog" + override fun getDimensionServiceKey(): String? = null // Disable saving dimension to allow auto-resize based on content override fun createCenterPanel(): JComponent { - val panel = JPanel(BorderLayout()) - val dragListener = object : MouseAdapter() { private var initialScreenClick: Point? = null @@ -68,11 +75,11 @@ class TypingDialog(private val project: Project, private val sourceCode: String) override fun mousePressed(e: MouseEvent) { initialScreenClick = e.locationOnScreen - initialWindowLocation = SwingUtilities.getWindowAncestor(panel)?.location + initialWindowLocation = SwingUtilities.getWindowAncestor(centerPanel)?.location } override fun mouseDragged(e: MouseEvent) { - val window = SwingUtilities.getWindowAncestor(panel) + val window = SwingUtilities.getWindowAncestor(centerPanel) if (window != null && initialScreenClick != null && initialWindowLocation != null) { val deltaX = e.locationOnScreen.x - initialScreenClick!!.x val deltaY = e.locationOnScreen.y - initialScreenClick!!.y @@ -81,14 +88,18 @@ class TypingDialog(private val project: Project, private val sourceCode: String) } } - panel.addMouseListener(dragListener) - panel.addMouseMotionListener(dragListener) + centerPanel.addMouseListener(dragListener) + centerPanel.addMouseMotionListener(dragListener) textPane.apply { text = sourceCode isEditable = false background = EditorColorsManager.getInstance().globalScheme.defaultBackground - font = EditorColorsManager.getInstance().globalScheme.getFont(EditorFontType.PLAIN) + + // Load saved font size or use default + val savedSize = typingService.getFontSize() + val baseFont = EditorColorsManager.getInstance().globalScheme.getFont(EditorFontType.PLAIN) + font = Font(baseFont.name, baseFont.style, savedSize) StyleConstants.setBackground(ghostAttributes, background) StyleConstants.setBackground(correctAttributes, background) @@ -127,6 +138,16 @@ class TypingDialog(private val project: Project, private val sourceCode: String) } }, ) + + addMouseWheelListener { e -> + if (e.isControlDown) { + handleZoom(e) + } else { + // Pass to parent if not zooming + parent.dispatchEvent(e) + } + } + addMouseListener(dragListener) addMouseMotionListener(dragListener) } @@ -134,10 +155,67 @@ class TypingDialog(private val project: Project, private val sourceCode: String) val scrollPane = JBScrollPane(textPane) scrollPane.addMouseListener(dragListener) scrollPane.addMouseMotionListener(dragListener) - panel.add(scrollPane, BorderLayout.CENTER) - panel.preferredSize = java.awt.Dimension(800, 600) + centerPanel.add(scrollPane, BorderLayout.CENTER) + + updateWindowSize() + + return centerPanel + } + + private fun handleZoom(e: MouseWheelEvent) { + val currentFont = textPane.font + val newSize = if (e.wheelRotation < 0) currentFont.size + 1 else max(8, currentFont.size - 1) + + if (newSize != currentFont.size) { + textPane.font = Font(currentFont.name, currentFont.style, newSize) + + // Persist the new font size + typingService.setFontSize(newSize) + + val window = SwingUtilities.getWindowAncestor(centerPanel) + if (window != null) { + val oldSize = window.size + val oldLocation = window.location + + updateWindowSize() + + val newPreferredSize = centerPanel.preferredSize + val decorationWidth = oldSize.width - centerPanel.width + val decorationHeight = oldSize.height - centerPanel.height + + val newWidth = newPreferredSize.width + decorationWidth + val newHeight = newPreferredSize.height + decorationHeight + + window.setSize(newWidth, newHeight) + window.setLocation( + oldLocation.x - (newWidth - oldSize.width) / 2, + oldLocation.y - (newHeight - oldSize.height) / 2 + ) + + centerPanel.revalidate() + window.validate() + window.repaint() + + // Ensure cursor is still visible and has padding after zoom + updateCursor() + } + } + } + + private fun updateWindowSize() { + val metrics = textPane.getFontMetrics(textPane.font) + val lines = sourceCode.lines() + val maxLineWidth = lines.maxOfOrNull { metrics.stringWidth(it) } ?: 0 + val totalHeight = lines.size * metrics.height + + val screenSize = Toolkit.getDefaultToolkit().screenSize + val maxAvailableWidth = (screenSize.width * 0.9).toInt() + val maxAvailableHeight = (screenSize.height * 0.9).toInt() - return panel + val preferredWidth = min(max(maxLineWidth + 60, 400), maxAvailableWidth) + val preferredHeight = min(max(totalHeight + 60, 200), maxAvailableHeight) + + centerPanel.preferredSize = Dimension(preferredWidth, preferredHeight) } override fun getPreferredFocusedComponent(): JComponent? { @@ -231,6 +309,27 @@ class TypingDialog(private val project: Project, private val sourceCode: String) textPane.styledDocument.setCharacterAttributes(currentIndex, 1, style, true) textPane.caretPosition = currentIndex + + // Auto-scroll to keep cursor visible with padding of at least 3 characters + try { + val rect = textPane.modelToView(currentIndex) + if (rect != null) { + val metrics = textPane.getFontMetrics(textPane.font) + // Calculate padding based on 3 widest characters ('W') and line height + val horizontalPadding = metrics.stringWidth("WWW") + val verticalPadding = metrics.height + + val paddedRect = Rectangle( + rect.x - horizontalPadding, + rect.y - verticalPadding, + rect.width + 2 * horizontalPadding, + rect.height + 2 * verticalPadding + ) + textPane.scrollRectToVisible(paddedRect) + } + } catch (e: Exception) { + log.warn("Could not scroll to cursor", e) + } } } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index e8c56d6..258e18d 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -25,12 +25,11 @@ ]]> -
  • Right-click action to start typing any file or selection.
  • -
  • Ghost text overlay for guidance.
  • -
  • Real-time error highlighting.
  • -
  • Session summary with WPM and Accuracy statistics.
  • +
  • Dynamic Window Resizing: The typing dialog now automatically adjusts its size based on the text content, making better use of screen space.
  • +
  • Interactive Text Zooming: Use Ctrl + Scroll Wheel to resize the text font. The dialog window will dynamically resize to fit the new text size.
  • +
  • Persistent Settings: Your preferred font size is now saved across sessions and IDE restarts.
  • +
  • Smart Auto-Scrolling: Improved cursor visibility with automatic scrolling that maintains a comfortable margin (at least 3 characters) from the window edges.
  • ]]>
    From 5a0ec176e49587e7225fb576d27befc4ddd8a2e7 Mon Sep 17 00:00:00 2001 From: Yauheni Slizh Date: Wed, 4 Feb 2026 22:58:38 +0100 Subject: [PATCH 2/4] Fix ktlint issues --- .../typingplugin/service/TypingService.kt | 13 +++-- .../kiolk/typingplugin/ui/TypingDialog.kt | 47 ++++++++++--------- 2 files changed, 34 insertions(+), 26 deletions(-) diff --git a/src/main/kotlin/com/github/kiolk/typingplugin/service/TypingService.kt b/src/main/kotlin/com/github/kiolk/typingplugin/service/TypingService.kt index e39fc58..29424e8 100644 --- a/src/main/kotlin/com/github/kiolk/typingplugin/service/TypingService.kt +++ b/src/main/kotlin/com/github/kiolk/typingplugin/service/TypingService.kt @@ -3,8 +3,8 @@ package com.github.kiolk.typingplugin.service import com.github.kiolk.typingplugin.model.TypingResult import com.intellij.openapi.components.PersistentStateComponent import com.intellij.openapi.components.Service -import com.intellij.openapi.components.Storage import com.intellij.openapi.components.State +import com.intellij.openapi.components.Storage import com.intellij.openapi.components.service import com.intellij.openapi.project.Project @@ -17,7 +17,7 @@ class TypingService(private val project: Project) : PersistentStateComponent { data class State( var results: MutableList = mutableListOf(), - var fontSize: Int = 14, // Default font size + var fontSize: Int = 14, ) private var myState = State() @@ -28,7 +28,14 @@ class TypingService(private val project: Project) : accuracy: Double, ) { val attemptNumber = myState.results.size + 1 - myState.results.add(TypingResult(attemptNumber, wpm, errorsPerMinute, accuracy)) + myState.results.add( + TypingResult( + attemptNumber, + wpm, + errorsPerMinute, + accuracy, + ), + ) } fun getResults(): List = myState.results.toList() 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 7df5454..ac3f1b3 100644 --- a/src/main/kotlin/com/github/kiolk/typingplugin/ui/TypingDialog.kt +++ b/src/main/kotlin/com/github/kiolk/typingplugin/ui/TypingDialog.kt @@ -95,7 +95,7 @@ class TypingDialog(private val project: Project, private val sourceCode: String) text = sourceCode isEditable = false background = EditorColorsManager.getInstance().globalScheme.defaultBackground - + // Load saved font size or use default val savedSize = typingService.getFontSize() val baseFont = EditorColorsManager.getInstance().globalScheme.getFont(EditorFontType.PLAIN) @@ -138,7 +138,7 @@ class TypingDialog(private val project: Project, private val sourceCode: String) } }, ) - + addMouseWheelListener { e -> if (e.isControlDown) { handleZoom(e) @@ -147,7 +147,7 @@ class TypingDialog(private val project: Project, private val sourceCode: String) parent.dispatchEvent(e) } } - + addMouseListener(dragListener) addMouseMotionListener(dragListener) } @@ -156,46 +156,46 @@ class TypingDialog(private val project: Project, private val sourceCode: String) scrollPane.addMouseListener(dragListener) scrollPane.addMouseMotionListener(dragListener) centerPanel.add(scrollPane, BorderLayout.CENTER) - + updateWindowSize() return centerPanel } - + private fun handleZoom(e: MouseWheelEvent) { val currentFont = textPane.font val newSize = if (e.wheelRotation < 0) currentFont.size + 1 else max(8, currentFont.size - 1) - + if (newSize != currentFont.size) { textPane.font = Font(currentFont.name, currentFont.style, newSize) - + // Persist the new font size typingService.setFontSize(newSize) - + val window = SwingUtilities.getWindowAncestor(centerPanel) if (window != null) { val oldSize = window.size val oldLocation = window.location - + updateWindowSize() - + val newPreferredSize = centerPanel.preferredSize val decorationWidth = oldSize.width - centerPanel.width val decorationHeight = oldSize.height - centerPanel.height - + val newWidth = newPreferredSize.width + decorationWidth val newHeight = newPreferredSize.height + decorationHeight - + window.setSize(newWidth, newHeight) window.setLocation( oldLocation.x - (newWidth - oldSize.width) / 2, - oldLocation.y - (newHeight - oldSize.height) / 2 + oldLocation.y - (newHeight - oldSize.height) / 2, ) - + centerPanel.revalidate() window.validate() window.repaint() - + // Ensure cursor is still visible and has padding after zoom updateCursor() } @@ -309,7 +309,7 @@ class TypingDialog(private val project: Project, private val sourceCode: String) textPane.styledDocument.setCharacterAttributes(currentIndex, 1, style, true) textPane.caretPosition = currentIndex - + // Auto-scroll to keep cursor visible with padding of at least 3 characters try { val rect = textPane.modelToView(currentIndex) @@ -318,13 +318,14 @@ class TypingDialog(private val project: Project, private val sourceCode: String) // Calculate padding based on 3 widest characters ('W') and line height val horizontalPadding = metrics.stringWidth("WWW") val verticalPadding = metrics.height - - val paddedRect = Rectangle( - rect.x - horizontalPadding, - rect.y - verticalPadding, - rect.width + 2 * horizontalPadding, - rect.height + 2 * verticalPadding - ) + + val paddedRect = + Rectangle( + rect.x - horizontalPadding, + rect.y - verticalPadding, + rect.width + 2 * horizontalPadding, + rect.height + 2 * verticalPadding, + ) textPane.scrollRectToVisible(paddedRect) } } catch (e: Exception) { From a288c84afceaf220e6a56e06e57c5839c78974cb Mon Sep 17 00:00:00 2001 From: Yauheni Slizh Date: Wed, 4 Feb 2026 23:06:41 +0100 Subject: [PATCH 3/4] Fix unit tests issues --- .../com/github/kiolk/typingplugin/ui/TypingDialog.kt | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) 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 ac3f1b3..e027ded 100644 --- a/src/main/kotlin/com/github/kiolk/typingplugin/ui/TypingDialog.kt +++ b/src/main/kotlin/com/github/kiolk/typingplugin/ui/TypingDialog.kt @@ -11,6 +11,7 @@ import java.awt.BorderLayout import java.awt.Color import java.awt.Dimension import java.awt.Font +import java.awt.GraphicsEnvironment import java.awt.Point import java.awt.Rectangle import java.awt.Toolkit @@ -74,11 +75,13 @@ class TypingDialog(private val project: Project, private val sourceCode: String) private var initialWindowLocation: Point? = null override fun mousePressed(e: MouseEvent) { + if (GraphicsEnvironment.isHeadless()) return initialScreenClick = e.locationOnScreen initialWindowLocation = SwingUtilities.getWindowAncestor(centerPanel)?.location } override fun mouseDragged(e: MouseEvent) { + if (GraphicsEnvironment.isHeadless()) return val window = SwingUtilities.getWindowAncestor(centerPanel) if (window != null && initialScreenClick != null && initialWindowLocation != null) { val deltaX = e.locationOnScreen.x - initialScreenClick!!.x @@ -144,7 +147,7 @@ class TypingDialog(private val project: Project, private val sourceCode: String) handleZoom(e) } else { // Pass to parent if not zooming - parent.dispatchEvent(e) + parent?.dispatchEvent(e) } } @@ -208,7 +211,12 @@ class TypingDialog(private val project: Project, private val sourceCode: String) val maxLineWidth = lines.maxOfOrNull { metrics.stringWidth(it) } ?: 0 val totalHeight = lines.size * metrics.height - val screenSize = Toolkit.getDefaultToolkit().screenSize + val screenSize = + if (GraphicsEnvironment.isHeadless()) { + Dimension(800, 600) + } else { + Toolkit.getDefaultToolkit().screenSize + } val maxAvailableWidth = (screenSize.width * 0.9).toInt() val maxAvailableHeight = (screenSize.height * 0.9).toInt() From 19a9aed6eb5c0ecf4189d0d7c5c0517cf89a5336 Mon Sep 17 00:00:00 2001 From: Yauheni Slizh <30602586+Kiolk@users.noreply.github.com> Date: Wed, 4 Feb 2026 23:06:58 +0100 Subject: [PATCH 4/4] Fix ktlint style violations and HeadlessException in tests. Update v1.0.7 release candidate.