Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 1 addition & 9 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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.*"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -17,6 +17,7 @@ class TypingService(private val project: Project) :
PersistentStateComponent<TypingService.State> {
data class State(
var results: MutableList<TypingResult> = mutableListOf(),
var fontSize: Int = 14,
)
Comment thread
Kiolk marked this conversation as resolved.

private var myState = State()
Expand All @@ -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<TypingResult> = 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) {
Expand Down
130 changes: 119 additions & 11 deletions src/main/kotlin/com/github/kiolk/typingplugin/ui/TypingDialog.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -28,6 +36,8 @@ class TypingDialog(private val project: Project, private val sourceCode: String)
private val textPane = JTextPane()
private val skippedIndices = mutableSetOf<Int>()
private val log = logger<TypingDialog>()
private val centerPanel = JPanel(BorderLayout())
private val typingService = TypingService.getInstance(project)

// Styles
private val ghostAttributes =
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -127,17 +141,89 @@ 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)
}

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? {
Expand Down Expand Up @@ -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)
}
}
}

Expand Down
9 changes: 4 additions & 5 deletions src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,11 @@
]]></description>

<change-notes><![CDATA[
Initial release of the Typing Training plugin. This version contains the basic functionality including:
<ul>
<li>Right-click action to start typing any file or selection.</li>
<li>Ghost text overlay for guidance.</li>
<li>Real-time error highlighting.</li>
<li>Session summary with WPM and Accuracy statistics.</li>
<li><b>Dynamic Window Resizing:</b> The typing dialog now automatically adjusts its size based on the text content, making better use of screen space.</li>
<li><b>Interactive Text Zooming:</b> Use <b>Ctrl + Scroll Wheel</b> to resize the text font. The dialog window will dynamically resize to fit the new text size.</li>
<li><b>Persistent Settings:</b> Your preferred font size is now saved across sessions and IDE restarts.</li>
<li><b>Smart Auto-Scrolling:</b> Improved cursor visibility with automatic scrolling that maintains a comfortable margin (at least 3 characters) from the window edges.</li>
</ul>
]]></change-notes>

Expand Down