diff --git a/src/main/kotlin/com/github/akshayashokcode/devfocus/model/PomodoroMode.kt b/src/main/kotlin/com/github/akshayashokcode/devfocus/model/PomodoroMode.kt index 374ea81..e61b709 100644 --- a/src/main/kotlin/com/github/akshayashokcode/devfocus/model/PomodoroMode.kt +++ b/src/main/kotlin/com/github/akshayashokcode/devfocus/model/PomodoroMode.kt @@ -11,7 +11,7 @@ enum class PomodoroMode( ) { CLASSIC( displayName = "Classic Pomodoro", - emoji = "🍅", + emoji = "\uD83C\uDFAF", sessionMinutes = 25, breakMinutes = 5, sessionsPerRound = 4, diff --git a/src/main/kotlin/com/github/akshayashokcode/devfocus/services/pomodoro/PomodoroTimerService.kt b/src/main/kotlin/com/github/akshayashokcode/devfocus/services/pomodoro/PomodoroTimerService.kt index 4ff1091..47fb800 100644 --- a/src/main/kotlin/com/github/akshayashokcode/devfocus/services/pomodoro/PomodoroTimerService.kt +++ b/src/main/kotlin/com/github/akshayashokcode/devfocus/services/pomodoro/PomodoroTimerService.kt @@ -31,7 +31,7 @@ class PomodoroTimerService(private val project: Project) { private var job: Job? = null private var settings = PomodoroMode.CLASSIC.toSettings() - private var currentPhase = TimerPhase.WORK + private var internalPhase = TimerPhase.WORK private var remainingTimeMs: Long = TimeUnit.MINUTES.toMillis(settings.sessionMinutes.toLong()) private val _timeLeft = MutableStateFlow(formatTime(remainingTimeMs)) @@ -43,6 +43,9 @@ class PomodoroTimerService(private val project: Project) { private val _currentSession = MutableStateFlow(1) val currentSession: StateFlow = _currentSession + private val _currentPhase = MutableStateFlow(TimerPhase.WORK) + val currentPhase: StateFlow = _currentPhase + private val _settings = MutableStateFlow(settings) val settingsFlow: StateFlow = _settings @@ -70,7 +73,7 @@ class PomodoroTimerService(private val project: Project) { val currentSessionNum = _currentSession.value val totalSessions = settings.sessionsPerRound - if (currentPhase == TimerPhase.WORK) { + if (internalPhase == TimerPhase.WORK) { // Work session complete if (currentSessionNum >= totalSessions) { // Last session complete - all done! @@ -84,23 +87,27 @@ class PomodoroTimerService(private val project: Project) { .notify(project) // Reset to initial state - currentPhase = TimerPhase.WORK + internalPhase = TimerPhase.WORK + _currentPhase.value = TimerPhase.WORK _currentSession.value = 1 remainingTimeMs = TimeUnit.MINUTES.toMillis(settings.sessionMinutes.toLong()) _timeLeft.value = formatTime(remainingTimeMs) } else { // Work session complete - start break + _currentSession.value = currentSessionNum + 1 + NotificationGroupManager.getInstance() .getNotificationGroup(NOTIFICATION_GROUP_ID) .createNotification( - "\uD83C\uDF45 Session Complete!", + "✅ Session $currentSessionNum Complete!", "Great work! Starting ${settings.breakMinutes}-minute break ☕.", NotificationType.INFORMATION ) .notify(project) // Start break timer - currentPhase = TimerPhase.BREAK + internalPhase = TimerPhase.BREAK + _currentPhase.value = TimerPhase.BREAK remainingTimeMs = TimeUnit.MINUTES.toMillis(settings.breakMinutes.toLong()) _timeLeft.value = formatTime(remainingTimeMs) start() @@ -113,13 +120,14 @@ class PomodoroTimerService(private val project: Project) { .getNotificationGroup(NOTIFICATION_GROUP_ID) .createNotification( "☕ Break Complete!", - "Starting session ${currentSessionNum + 1} of $totalSessions.", + "Starting session $currentSessionNum of $totalSessions.", NotificationType.INFORMATION ) .notify(project) // Start next work session - currentPhase = TimerPhase.WORK + internalPhase = TimerPhase.WORK + _currentPhase.value = TimerPhase.WORK _currentSession.value = currentSessionNum + 1 remainingTimeMs = TimeUnit.MINUTES.toMillis(settings.sessionMinutes.toLong()) _timeLeft.value = formatTime(remainingTimeMs) @@ -141,7 +149,8 @@ class PomodoroTimerService(private val project: Project) { job = null // Reset to initial state - currentPhase = TimerPhase.WORK + internalPhase = TimerPhase.WORK + _currentPhase.value = TimerPhase.WORK remainingTimeMs = TimeUnit.MINUTES.toMillis(settings.sessionMinutes.toLong()) _timeLeft.value = formatTime(remainingTimeMs) _currentSession.value = 1 @@ -162,7 +171,8 @@ class PomodoroTimerService(private val project: Project) { settings = newSettings _settings.value = newSettings - currentPhase = TimerPhase.WORK + internalPhase = TimerPhase.WORK + _currentPhase.value = TimerPhase.WORK remainingTimeMs = TimeUnit.MINUTES.toMillis(newSettings.sessionMinutes.toLong()) _timeLeft.value = formatTime(remainingTimeMs) _currentSession.value = 1 @@ -176,7 +186,7 @@ class PomodoroTimerService(private val project: Project) { fun getSettings(): PomodoroSettings = settings fun getProgress(): Float { - val totalMs = if (currentPhase == TimerPhase.WORK) { + val totalMs = if (internalPhase == TimerPhase.WORK) { TimeUnit.MINUTES.toMillis(settings.sessionMinutes.toLong()) } else { TimeUnit.MINUTES.toMillis(settings.breakMinutes.toLong()) diff --git a/src/main/kotlin/com/github/akshayashokcode/devfocus/toolWindow/DevFocusToolWindowFactory.kt b/src/main/kotlin/com/github/akshayashokcode/devfocus/toolWindow/DevFocusToolWindowFactory.kt index ddaf389..7826f80 100644 --- a/src/main/kotlin/com/github/akshayashokcode/devfocus/toolWindow/DevFocusToolWindowFactory.kt +++ b/src/main/kotlin/com/github/akshayashokcode/devfocus/toolWindow/DevFocusToolWindowFactory.kt @@ -11,6 +11,7 @@ class DevFocusToolWindowFactory : ToolWindowFactory { override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) { val panel = PomodoroToolWindowPanel(project) val content = ContentFactory.getInstance().createContent(panel, "", false) + content.setDisposer(panel) toolWindow.contentManager.addContent(content) } diff --git a/src/main/kotlin/com/github/akshayashokcode/devfocus/toolWindow/PomodoroToolWindowPanel.kt b/src/main/kotlin/com/github/akshayashokcode/devfocus/toolWindow/PomodoroToolWindowPanel.kt index 29c734c..cf4f23a 100644 --- a/src/main/kotlin/com/github/akshayashokcode/devfocus/toolWindow/PomodoroToolWindowPanel.kt +++ b/src/main/kotlin/com/github/akshayashokcode/devfocus/toolWindow/PomodoroToolWindowPanel.kt @@ -6,17 +6,26 @@ import com.github.akshayashokcode.devfocus.services.pomodoro.PomodoroTimerServic import com.github.akshayashokcode.devfocus.ui.components.CircularTimerPanel import com.github.akshayashokcode.devfocus.ui.components.SessionIndicatorPanel import com.github.akshayashokcode.devfocus.ui.settings.PomodoroSettingsPanel +import com.intellij.openapi.Disposable import com.intellij.openapi.project.Project import com.intellij.ui.components.JBPanel import kotlinx.coroutines.* import kotlinx.coroutines.flow.collectLatest -import java.awt.* +import java.awt.BorderLayout +import java.awt.Dimension +import java.awt.FlowLayout +import java.awt.Font +import java.awt.event.ComponentAdapter +import java.awt.event.ComponentEvent import javax.swing.* -class PomodoroToolWindowPanel(private val project: Project) : JBPanel>(BorderLayout()) { +class PomodoroToolWindowPanel(private val project: Project) : JBPanel>(BorderLayout()), Disposable { private val timerService = project.getService(PomodoroTimerService::class.java) ?: error("PomodoroTimerService not available") + // Layout orientation tracking + private var isHorizontalLayout = false + // Mode selector private val modeComboBox = JComboBox(PomodoroMode.entries.toTypedArray()).apply { selectedItem = PomodoroMode.CLASSIC @@ -25,7 +34,12 @@ class PomodoroToolWindowPanel(private val project: Project) : JBPanel // Info label showing current mode settings private val infoLabel = JLabel("📊 25 min work • 5 min break").apply { horizontalAlignment = SwingConstants.CENTER - font = font.deriveFont(Font.PLAIN, 12f) + font = font.deriveFont(Font.BOLD, 12f) + } + + private val sessionTextLabel = JLabel("Session 1 of 4").apply { + horizontalAlignment = SwingConstants.CENTER + font = font.deriveFont(Font.BOLD, 14f) } // Circular timer display @@ -35,9 +49,18 @@ class PomodoroToolWindowPanel(private val project: Project) : JBPanel private val sessionIndicator = SessionIndicatorPanel() // Control buttons - private val startButton = JButton("Start") - private val pauseButton = JButton("Pause") - private val resetButton = JButton("Reset") + private val startButton = JButton("Start").apply { + preferredSize = Dimension(80, 32) + // Make it a prominent primary button + putClientProperty("JButton.buttonType", "default") + font = font.deriveFont(Font.BOLD) + } + private val pauseButton = JButton("Pause").apply { + preferredSize = Dimension(80, 32) + } + private val resetButton = JButton("Reset").apply { + preferredSize = Dimension(80, 32) + } // Custom settings panel (only visible when Custom mode selected) private val settingsPanel = PomodoroSettingsPanel { session, breakTime, sessions -> @@ -50,15 +73,25 @@ class PomodoroToolWindowPanel(private val project: Project) : JBPanel private var stateJob: Job? = null private var timeJob: Job? = null private var sessionJob: Job? = null + private var phaseJob: Job? = null init { buildUI() setupListeners() observeTimer() updateSettingsPanelVisibility() + setupLayoutListener() } private fun buildUI() { + if(isHorizontalLayout) { + buildHorizontalLayout() + } else { + buildVerticalLayout() + } + } + + private fun buildVerticalLayout() { // Top panel with mode selector val topPanel = JPanel(BorderLayout(5, 5)).apply { border = BorderFactory.createEmptyBorder(10, 10, 5, 10) @@ -66,24 +99,29 @@ class PomodoroToolWindowPanel(private val project: Project) : JBPanel } // Info panel - val infoPanel = JPanel(FlowLayout(FlowLayout.CENTER)).apply { + val infoPanel = JPanel(FlowLayout(FlowLayout.CENTER, 0, 5)).apply { add(infoLabel) } // Timer panel val timerPanel = JPanel(BorderLayout()).apply { - border = BorderFactory.createEmptyBorder(20, 10, 20, 10) + border = BorderFactory.createEmptyBorder(15, 10, 10, 10) add(circularTimer, BorderLayout.CENTER) } + // Session text label panel + val sessionPanel = JPanel(FlowLayout(FlowLayout.CENTER, 0, 5)).apply { + add(sessionTextLabel) + } + // Progress panel val progressPanel = JPanel(BorderLayout(5, 5)).apply { - border = BorderFactory.createEmptyBorder(0, 20, 10, 20) + border = BorderFactory.createEmptyBorder(5, 20, 10, 20) add(sessionIndicator, BorderLayout.CENTER) } // Button panel - val buttonPanel = JPanel(FlowLayout(FlowLayout.CENTER, 10, 5)).apply { + val buttonPanel = JPanel(FlowLayout(FlowLayout.CENTER, 8, 5)).apply { add(startButton) add(pauseButton) add(resetButton) @@ -94,6 +132,7 @@ class PomodoroToolWindowPanel(private val project: Project) : JBPanel layout = BoxLayout(this, BoxLayout.Y_AXIS) add(infoPanel) add(timerPanel) + add(sessionPanel) add(progressPanel) add(buttonPanel) } @@ -103,6 +142,10 @@ class PomodoroToolWindowPanel(private val project: Project) : JBPanel add(settingsPanel, BorderLayout.SOUTH) } + private fun buildHorizontalLayout() { + buildVerticalLayout() + } + private fun setupListeners() { startButton.addActionListener { timerService.start() } pauseButton.addActionListener { timerService.pause() } @@ -131,7 +174,49 @@ class PomodoroToolWindowPanel(private val project: Project) : JBPanel } private fun updateProgressBar(totalSessions: Int) { - sessionIndicator.updateSessions(timerService.currentSession.value, totalSessions) + val currentSession = timerService.currentSession.value + sessionIndicator.updateSessions(currentSession, totalSessions) + sessionTextLabel.text = "Session $currentSession of $totalSessions" + } + + private fun setupLayoutListener() { + addComponentListener(object : ComponentAdapter() { + override fun componentResized(e: ComponentEvent?) { + checkAndUpdateLayout() + } + }) + } + + private fun checkAndUpdateLayout() { + val width = width + val height = height + + // Determine if we should use horizontal layout (width > height * 1.5) + val shouldBeHorizontal = width > height * 1.5 + + // Only rebuild if layout orientation changed + if (shouldBeHorizontal != isHorizontalLayout) { + isHorizontalLayout = shouldBeHorizontal + rebuildLayout() + } + } + + private fun rebuildLayout() { + // Remove all components + removeAll() + + // Rebuild UI with new layout + buildUI() + + // Reconnect listeners (buttons are recreated, need new listeners) + setupListeners() + + // Update setting panel visibility + updateSettingsPanelVisibility() + + // Refresh the panel + revalidate() + repaint() } private fun observeTimer() { @@ -139,7 +224,8 @@ class PomodoroToolWindowPanel(private val project: Project) : JBPanel timerService.timeLeft.collectLatest { time -> SwingUtilities.invokeLater { val progress = timerService.getProgress() - circularTimer.updateTimer(time, progress, false) + val isBreak = timerService.currentPhase.value == PomodoroTimerService.TimerPhase.BREAK + circularTimer.updateTimer(time, progress, isBreak) } } } @@ -150,8 +236,24 @@ class PomodoroToolWindowPanel(private val project: Project) : JBPanel startButton.isEnabled = it != PomodoroTimerService.TimerState.RUNNING pauseButton.isEnabled = it == PomodoroTimerService.TimerState.RUNNING resetButton.isEnabled = it != PomodoroTimerService.TimerState.IDLE - // Disable mode selector when timer is active (running or paused) - modeComboBox.isEnabled = it == PomodoroTimerService.TimerState.IDLE + + // Check if we're truly idle (session and work phase) or just transitioning + val currentSession = timerService.currentSession.value + val currentPhase = timerService.currentPhase.value + val isTrulyIdle = it == PomodoroTimerService.TimerState.IDLE && + currentSession == 1 && + currentPhase == PomodoroTimerService.TimerPhase.WORK + + modeComboBox.isEnabled = isTrulyIdle + + // Hide custom settings panel when timer is active + if (!isTrulyIdle && modeComboBox.selectedItem == PomodoroMode.CUSTOM) { + settingsPanel.isVisible = false + revalidate() + repaint() + } else if (isTrulyIdle){ + updateSettingsPanelVisibility() + } } } } @@ -160,16 +262,30 @@ class PomodoroToolWindowPanel(private val project: Project) : JBPanel timerService.currentSession.collectLatest { session -> SwingUtilities.invokeLater { val settings = timerService.getSettings() - sessionIndicator.updateSessions(session, settings.sessionsPerRound) + val isBreak = timerService.currentPhase.value == PomodoroTimerService.TimerPhase.BREAK + sessionIndicator.updateSessions(session, settings.sessionsPerRound, isBreak) + sessionTextLabel.text = "Session $session of ${settings.sessionsPerRound}" + } + } + } + + phaseJob = scope.launch { + timerService.currentPhase.collectLatest { phase -> + SwingUtilities.invokeLater { + val settings = timerService.getSettings() + val session = timerService.currentSession.value + val isBreak = phase == PomodoroTimerService.TimerPhase.BREAK + sessionIndicator.updateSessions(session, settings.sessionsPerRound, isBreak) } } } } - fun dispose() { + override fun dispose() { stateJob?.cancel() timeJob?.cancel() sessionJob?.cancel() + phaseJob?.cancel() scope.cancel() } } \ No newline at end of file diff --git a/src/main/kotlin/com/github/akshayashokcode/devfocus/ui/components/CircularTimerPanel.kt b/src/main/kotlin/com/github/akshayashokcode/devfocus/ui/components/CircularTimerPanel.kt index 2bd455f..0960d30 100644 --- a/src/main/kotlin/com/github/akshayashokcode/devfocus/ui/components/CircularTimerPanel.kt +++ b/src/main/kotlin/com/github/akshayashokcode/devfocus/ui/components/CircularTimerPanel.kt @@ -12,11 +12,11 @@ class CircularTimerPanel : JPanel() { // Colors following UX best practices private val workColor = Color(74, 144, 226) // Blue for focus/work - private val breakColor = Color(80, 200, 120) // Green for rest + private val breakColor = Color(243, 156, 18) // Orange for rest private val backgroundColor = Color(224, 224, 224) // Light gray private val diameter = 180 - private val strokeWidth = 10f + private val strokeWidth = 12f init { preferredSize = Dimension(diameter + 40, diameter + 40) diff --git a/src/main/kotlin/com/github/akshayashokcode/devfocus/ui/components/SessionIndicatorPanel.kt b/src/main/kotlin/com/github/akshayashokcode/devfocus/ui/components/SessionIndicatorPanel.kt index a08dfb3..b7c6e61 100644 --- a/src/main/kotlin/com/github/akshayashokcode/devfocus/ui/components/SessionIndicatorPanel.kt +++ b/src/main/kotlin/com/github/akshayashokcode/devfocus/ui/components/SessionIndicatorPanel.kt @@ -7,18 +7,25 @@ class SessionIndicatorPanel : JPanel() { private var currentSession: Int = 1 private var totalSessions: Int = 4 + private var isBreakTime: Boolean = false - private val tomatoSize = 24 - private val spacing = 8 + private val completedColor = Color(39, 174, 96) + private val workColor = Color(74, 144, 226) + private val breakColor = Color(243, 156, 18) + private val upcomingColor = Color(149, 165, 166) + + private val indicatorSize = 30 + private val spacing = 12 init { isOpaque = false - preferredSize = Dimension(200, 40) + preferredSize = Dimension(200, 50) } - fun updateSessions(current: Int, total: Int) { + fun updateSessions(current: Int, total: Int, isBreak: Boolean = false) { this.currentSession = current this.totalSessions = total + this.isBreakTime = isBreak repaint() } @@ -28,49 +35,57 @@ class SessionIndicatorPanel : JPanel() { g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON) - val totalWidth = (tomatoSize * totalSessions) + (spacing * (totalSessions - 1)) + val totalWidth = (indicatorSize * totalSessions) + (spacing * (totalSessions - 1)) val startX = (width - totalWidth) / 2 - val startY = (height - tomatoSize) / 2 + val startY = (height - indicatorSize) / 2 for (i in 1..totalSessions) { - val x = startX + (i - 1) * (tomatoSize + spacing) + val x = startX + (i - 1) * (indicatorSize + spacing) if (i < currentSession) { - // Completed session - filled tomato - drawFilledTomato(g2d, x, startY) + // Completed session + drawCompletedSession(g2d, x, startY) } else if (i == currentSession) { - // Current session - outlined tomato with pulse effect - drawCurrentTomato(g2d, x, startY) + // Current session + drawCurrentSession(g2d, x, startY) } else { - // Future session - empty circle - drawEmptyCircle(g2d, x, startY) + // Upcoming session + drawUpcomingSession(g2d, x, startY) } } } - private fun drawFilledTomato(g2d: Graphics2D, x: Int, y: Int) { - g2d.color = Color(231, 76, 60) // Tomato red - g2d.fillOval(x, y, tomatoSize, tomatoSize) + private fun drawCompletedSession(g2d: Graphics2D, x: Int, y: Int) { + g2d.color = completedColor + g2d.fillOval(x, y, indicatorSize, indicatorSize) - // Add small leaf/stem on top - g2d.color = Color(46, 204, 113) // Green - val leafSize = 6 - g2d.fillOval(x + tomatoSize / 2 - leafSize / 2, y - 2, leafSize, leafSize) - } + // Draw white checkmark + g2d.color = Color.WHITE + g2d.stroke = BasicStroke(2.5f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND) - private fun drawCurrentTomato(g2d: Graphics2D, x: Int, y: Int) { - // Outlined tomato for current session - g2d.color = Color(231, 76, 60) - g2d.stroke = BasicStroke(2.5f) - g2d.drawOval(x + 2, y + 2, tomatoSize - 4, tomatoSize - 4) + val checkSize = indicatorSize / 2 + val centerX = x + indicatorSize / 2 + val centerY = y + indicatorSize / 2 + + val x1 = centerX - checkSize / 3 + val y1 = centerY + val x2 = centerX - checkSize / 6 + val y2 = centerY + checkSize / 3 + g2d.drawLine(x1, y1, x2, y2) - // Small filled center - g2d.fillOval(x + tomatoSize / 2 - 3, y + tomatoSize / 2 - 3, 6, 6) + val x3 = centerX + checkSize / 2 + val y3 = centerY - checkSize / 3 + g2d.drawLine(x2, y2, x3, y3) } - private fun drawEmptyCircle(g2d: Graphics2D, x: Int, y: Int) { - g2d.color = Color(189, 195, 199) // Light gray - g2d.stroke = BasicStroke(2f) - g2d.drawOval(x + 2, y + 2, tomatoSize - 4, tomatoSize - 4) + private fun drawCurrentSession(g2d: Graphics2D, x: Int, y: Int) { + g2d.color = if (isBreakTime) breakColor else workColor + g2d.fillOval(x, y, indicatorSize, indicatorSize) + } + + private fun drawUpcomingSession(g2d: Graphics2D, x: Int, y: Int) { + g2d.color = upcomingColor + g2d.stroke = BasicStroke(2.5f) + g2d.drawOval(x + 2, y + 2, indicatorSize - 4, indicatorSize - 4) } } \ No newline at end of file diff --git a/src/main/kotlin/com/github/akshayashokcode/devfocus/ui/settings/PomodoroSettingsPanel.kt b/src/main/kotlin/com/github/akshayashokcode/devfocus/ui/settings/PomodoroSettingsPanel.kt index a1b935e..71e5f4b 100644 --- a/src/main/kotlin/com/github/akshayashokcode/devfocus/ui/settings/PomodoroSettingsPanel.kt +++ b/src/main/kotlin/com/github/akshayashokcode/devfocus/ui/settings/PomodoroSettingsPanel.kt @@ -3,20 +3,31 @@ package com.github.akshayashokcode.devfocus.ui.settings import com.github.akshayashokcode.devfocus.util.SettingsValidationResult import com.github.akshayashokcode.devfocus.util.validateSettings import java.awt.Color +import java.awt.Dimension import java.awt.GridLayout import javax.swing.* import javax.swing.border.LineBorder class PomodoroSettingsPanel( private val applySettingsCallback: (Int, Int, Int) -> Unit -) : JPanel(GridLayout(4, 2, 5, 5)) { +) : JPanel(GridLayout(4, 2, 8, 5)) { - private val sessionField = JTextField("25") - private val breakField = JTextField("5") - private val sessionsField = JTextField("4") - private val applyButton = JButton("Apply") + private val sessionField = JTextField("25").apply { + preferredSize = Dimension(60, 28) + } + private val breakField = JTextField("5").apply { + preferredSize = Dimension(60, 28) + } + private val sessionsField = JTextField("4").apply { + preferredSize = Dimension(60, 28) + } + private val applyButton = JButton("Apply").apply { + preferredSize = Dimension(100, 32) + } init { + border = BorderFactory.createEmptyBorder(10, 15, 10, 15) + add(JLabel("Session Duration (min):")) add(sessionField) add(JLabel("Break Duration (min):"))