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..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,6 +17,7 @@ class TypingService(private val project: Project) : PersistentStateComponent { data class State( var results: MutableList = mutableListOf(), + var fontSize: Int = 14, ) private var myState = State() @@ -27,11 +28,24 @@ 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() + 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..e027ded 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,25 @@ 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.GraphicsEnvironment 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 +36,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,23 +66,23 @@ 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 private var initialWindowLocation: Point? = null override fun mousePressed(e: MouseEvent) { + if (GraphicsEnvironment.isHeadless()) return initialScreenClick = e.locationOnScreen - initialWindowLocation = SwingUtilities.getWindowAncestor(panel)?.location + initialWindowLocation = SwingUtilities.getWindowAncestor(centerPanel)?.location } override fun mouseDragged(e: MouseEvent) { - val window = SwingUtilities.getWindowAncestor(panel) + if (GraphicsEnvironment.isHeadless()) return + 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 +91,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 +141,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 +158,72 @@ 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 panel + 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 = + if (GraphicsEnvironment.isHeadless()) { + Dimension(800, 600) + } else { + Toolkit.getDefaultToolkit().screenSize + } + val maxAvailableWidth = (screenSize.width * 0.9).toInt() + val maxAvailableHeight = (screenSize.height * 0.9).toInt() + + 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 +317,28 @@ 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.
  • ]]>