From 2cb2dfa0b2b3ffae1057b31d198a1e6d3e74af14 Mon Sep 17 00:00:00 2001 From: akshay ashok Date: Fri, 29 May 2026 11:19:25 +0530 Subject: [PATCH] horizontal view added --- .../toolWindow/PomodoroToolWindowPanel.kt | 235 ++++++++++++------ .../ui/components/CircularTimerPanel.kt | 47 ++-- 2 files changed, 181 insertions(+), 101 deletions(-) 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 dae7410..da0b08d 100644 --- a/src/main/kotlin/com/github/akshayashokcode/devfocus/toolWindow/PomodoroToolWindowPanel.kt +++ b/src/main/kotlin/com/github/akshayashokcode/devfocus/toolWindow/PomodoroToolWindowPanel.kt @@ -18,23 +18,26 @@ import java.awt.Color import java.awt.Dimension import java.awt.FlowLayout import java.awt.Font +import java.awt.GridBagConstraints +import java.awt.GridBagLayout import java.awt.event.ComponentAdapter import java.awt.event.ComponentEvent import javax.swing.* class PomodoroToolWindowPanel(private val project: Project) : JBPanel>(BorderLayout()), Disposable { - private val timerService = project.getService(PomodoroTimerService::class.java) ?: error("PomodoroTimerService not available") + private val timerService = project.getService(PomodoroTimerService::class.java) + ?: error("PomodoroTimerService not available") - // Layout orientation tracking - private var isHorizontalLayout = false + private enum class LayoutMode { COMPACT, VERTICAL, HORIZONTAL } + private var currentLayout = LayoutMode.VERTICAL // Mode selector private val modeComboBox = JComboBox(PomodoroMode.entries.toTypedArray()).apply { selectedItem = PomodoroMode.CLASSIC } - // Setting button + // Settings button private val settingsButton = JButton(AllIcons.General.Settings).apply { toolTipText = "Settings" isBorderPainted = false @@ -57,13 +60,10 @@ class PomodoroToolWindowPanel(private val project: Project) : JBPanel private val phaseLabel = JLabel("Focus").apply { horizontalAlignment = SwingConstants.CENTER font = font.deriveFont(Font.BOLD, 13f) - foreground = Color(74, 144, 226) // Matches workColor in CircularTimerPanel + foreground = Color(74, 144, 226) } - // Circular timer display private val circularTimer = CircularTimerPanel() - - // Session indicator with tomato icons private val sessionIndicator = SessionIndicatorPanel() // Control buttons @@ -99,76 +99,193 @@ class PomodoroToolWindowPanel(private val project: Project) : JBPanel setupLayoutListener() } + // --------------------------------------------------------------------------- + // Layout routing + // --------------------------------------------------------------------------- + private fun buildUI() { - if(isHorizontalLayout) { - buildHorizontalLayout() - } else { - buildVerticalLayout() + when (currentLayout) { + LayoutMode.COMPACT -> buildCompactLayout() + LayoutMode.VERTICAL -> buildVerticalLayout() + LayoutMode.HORIZONTAL -> buildHorizontalLayout() + } + } + + /** + * Compact: either dimension < 160px. + * Just the circular timer + a row of buttons. Everything else hidden. + */ + private fun buildCompactLayout() { + val timerPanel = JPanel(BorderLayout()).apply { + border = BorderFactory.createEmptyBorder(6, 6, 4, 6) + add(circularTimer, BorderLayout.CENTER) + } + val buttonPanel = JPanel(FlowLayout(FlowLayout.CENTER, 4, 2)).apply { + add(startButton) + add(pauseButton) + add(resetButton) } + add(timerPanel, BorderLayout.CENTER) + add(buttonPanel, BorderLayout.SOUTH) } + /** + * Vertical: height >= width. + * Mode selector at top. Timer fills all remaining vertical space via + * BorderLayout.CENTER so it grows/shrinks naturally. Controls pinned at bottom. + * + * Scenarios handled: + * - Tall + narrow → small circle (min(width, timerHeight) drives diameter) + * - Tall + wide → large circle (width becomes the constraint) + */ private fun buildVerticalLayout() { - // Top panel with mode selector val topPanel = JPanel(BorderLayout(5, 5)).apply { - border = BorderFactory.createEmptyBorder(10, 10, 5, 10) + border = BorderFactory.createEmptyBorder(10, 10, 4, 10) add(modeComboBox, BorderLayout.CENTER) add(settingsButton, BorderLayout.EAST) } - // Info panel - val infoPanel = JPanel(FlowLayout(FlowLayout.CENTER, 0, 5)).apply { + val infoPanel = JPanel(FlowLayout(FlowLayout.CENTER, 0, 4)).apply { add(infoLabel) } - // Timer panel + // Timer lives in CENTER — it stretches to fill whatever height is left val timerPanel = JPanel(BorderLayout()).apply { - border = BorderFactory.createEmptyBorder(15, 10, 10, 10) + border = BorderFactory.createEmptyBorder(8, 10, 8, 10) add(circularTimer, BorderLayout.CENTER) } - // Session text label panel - val sessionPanel = JPanel(FlowLayout(FlowLayout.CENTER, 0, 5)).apply { + val phaseLabelPanel = JPanel(FlowLayout(FlowLayout.CENTER, 0, 2)).apply { + add(phaseLabel) + } + val sessionPanel = JPanel(FlowLayout(FlowLayout.CENTER, 0, 2)).apply { add(sessionTextLabel) } - - // Progress panel val progressPanel = JPanel(BorderLayout(5, 5)).apply { - border = BorderFactory.createEmptyBorder(5, 20, 10, 20) + border = BorderFactory.createEmptyBorder(4, 20, 4, 20) add(sessionIndicator, BorderLayout.CENTER) } - - // Button panel val buttonPanel = JPanel(FlowLayout(FlowLayout.CENTER, 8, 5)).apply { add(startButton) add(pauseButton) add(resetButton) } - // Phase label panel - val phaseLabelPanel = JPanel(FlowLayout(FlowLayout.CENTER, 0, 2)).apply { - add(phaseLabel) - } - - // Center content - val centerPanel = JPanel().apply { + // Fixed-height controls below the timer + val controlsPanel = JPanel().apply { layout = BoxLayout(this, BoxLayout.Y_AXIS) - add(infoPanel) - add(timerPanel) add(phaseLabelPanel) add(sessionPanel) add(progressPanel) add(buttonPanel) } + val centerPanel = JPanel(BorderLayout()).apply { + add(infoPanel, BorderLayout.NORTH) + add(timerPanel, BorderLayout.CENTER) // ← grows with panel + add(controlsPanel, BorderLayout.SOUTH) + } + add(topPanel, BorderLayout.NORTH) add(centerPanel, BorderLayout.CENTER) add(settingsPanel, BorderLayout.SOUTH) } + /** + * Horizontal: width > height. + * Mode selector spans the top. Timer takes left 55%, controls take right 45%. + * + * Scenarios handled: + * - Wide + tall → large circle (height drives diameter), ample control space + * - Wide + short → smaller circle, controls stack compactly on the right + */ private fun buildHorizontalLayout() { - buildVerticalLayout() + val topPanel = JPanel(BorderLayout(5, 5)).apply { + border = BorderFactory.createEmptyBorder(8, 10, 4, 10) + add(modeComboBox, BorderLayout.CENTER) + add(settingsButton, BorderLayout.EAST) + } + + val timerPanel = JPanel(BorderLayout()).apply { + border = BorderFactory.createEmptyBorder(8, 12, 8, 6) + add(circularTimer, BorderLayout.CENTER) + } + + val phaseLabelPanel = JPanel(FlowLayout(FlowLayout.CENTER, 0, 2)).apply { add(phaseLabel) } + val sessionPanel = JPanel(FlowLayout(FlowLayout.CENTER, 0, 2)).apply { add(sessionTextLabel) } + val progressPanel = JPanel(BorderLayout(5, 5)).apply { + border = BorderFactory.createEmptyBorder(4, 8, 4, 8) + add(sessionIndicator, BorderLayout.CENTER) + } + val buttonPanel = JPanel(FlowLayout(FlowLayout.CENTER, 6, 4)).apply { + add(startButton) + add(pauseButton) + add(resetButton) + } + + // Controls centered vertically on the right side + val rightPanel = JPanel().apply { + layout = BoxLayout(this, BoxLayout.Y_AXIS) + border = BorderFactory.createEmptyBorder(4, 4, 4, 12) + add(Box.createVerticalGlue()) + add(phaseLabelPanel) + add(sessionPanel) + add(progressPanel) + add(buttonPanel) + add(Box.createVerticalGlue()) + } + + // Split: timer 55% | controls 45% + val splitPanel = JPanel(GridBagLayout()).apply { + val gbc = GridBagConstraints().apply { + fill = GridBagConstraints.BOTH + weighty = 1.0 + } + gbc.weightx = 0.55; gbc.gridx = 0; add(timerPanel, gbc) + gbc.weightx = 0.45; gbc.gridx = 1; add(rightPanel, gbc) + } + + add(topPanel, BorderLayout.NORTH) + add(splitPanel, BorderLayout.CENTER) + add(settingsPanel, BorderLayout.SOUTH) + } + + // --------------------------------------------------------------------------- + // Responsive layout detection + // --------------------------------------------------------------------------- + + private fun setupLayoutListener() { + addComponentListener(object : ComponentAdapter() { + override fun componentResized(e: ComponentEvent?) { + checkAndUpdateLayout() + } + }) + } + + private fun checkAndUpdateLayout() { + val newLayout = when { + width < 160 || height < 160 -> LayoutMode.COMPACT + width > height -> LayoutMode.HORIZONTAL + else -> LayoutMode.VERTICAL + } + if (newLayout != currentLayout) { + currentLayout = newLayout + rebuildLayout() + } + } + + private fun rebuildLayout() { + removeAll() + buildUI() + updateSettingsPanelVisibility() + revalidate() + repaint() } + // --------------------------------------------------------------------------- + // Listeners & helpers + // --------------------------------------------------------------------------- + private fun setupListeners() { startButton.addActionListener { timerService.start() } pauseButton.addActionListener { timerService.pause() } @@ -191,7 +308,8 @@ class PomodoroToolWindowPanel(private val project: Project) : JBPanel private fun updateSettingsPanelVisibility() { val isCustom = modeComboBox.selectedItem == PomodoroMode.CUSTOM - settingsPanel.isVisible = isCustom + // Never show the custom settings panel in compact mode — no room for it + settingsPanel.isVisible = isCustom && currentLayout != LayoutMode.COMPACT revalidate() repaint() } @@ -206,35 +324,9 @@ class PomodoroToolWindowPanel(private val project: Project) : JBPanel 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() { - removeAll() - buildUI() - updateSettingsPanelVisibility() - revalidate() - repaint() - } + // --------------------------------------------------------------------------- + // Timer observation + // --------------------------------------------------------------------------- private fun observeTimer() { timeJob = scope.launch { @@ -254,18 +346,15 @@ class PomodoroToolWindowPanel(private val project: Project) : JBPanel pauseButton.isEnabled = it == PomodoroTimerService.TimerState.RUNNING resetButton.isEnabled = it != PomodoroTimerService.TimerState.IDLE - // Update start button text based on state startButton.text = when (it) { PomodoroTimerService.TimerState.IDLE -> "Start" else -> "Resume" } - // Clear all default values startButton.putClientProperty("JButton.buttonType", null) pauseButton.putClientProperty("JButton.buttonType", null) resetButton.putClientProperty("JButton.buttonType", null) - // Update default button (blue highlight) based on state when (it) { PomodoroTimerService.TimerState.IDLE -> { startButton.putClientProperty("JButton.buttonType", "default") @@ -281,7 +370,6 @@ class PomodoroToolWindowPanel(private val project: Project) : JBPanel } } - // 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 && @@ -290,12 +378,11 @@ class PomodoroToolWindowPanel(private val project: Project) : JBPanel 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){ + } else if (isTrulyIdle) { updateSettingsPanelVisibility() } } @@ -322,10 +409,10 @@ class PomodoroToolWindowPanel(private val project: Project) : JBPanel sessionIndicator.updateSessions(session, settings.sessionsPerRound, isBreak) if (isBreak) { phaseLabel.text = "Break" - phaseLabel.foreground = Color(243, 156, 18) // Matches breakColor + phaseLabel.foreground = Color(243, 156, 18) } else { phaseLabel.text = "Focus" - phaseLabel.foreground = Color(74, 144, 226) // Matches workColor + phaseLabel.foreground = Color(74, 144, 226) } } } @@ -339,4 +426,4 @@ class PomodoroToolWindowPanel(private val project: Project) : JBPanel 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 5ca592b..384feac 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 @@ -10,20 +10,17 @@ class CircularTimerPanel : JPanel() { private var progress: Float = 1.0f // 0.0 to 1.0 (1.0 = full, 0.0 = empty) private var isBreakTime: Boolean = false - // Colors following UX best practices - private val workColor = Color(74, 144, 226) // Blue for focus/work - private val breakColor = Color(243, 156, 18) // Orange for rest - // Dynamic so it adapts to light/dark themes + private val workColor = Color(74, 144, 226) + private val breakColor = Color(243, 156, 18) private val backgroundColor: Color get() = UIManager.getColor("Separator.separatorColor") ?: UIManager.getColor("Component.borderColor") ?: Color(200, 200, 200) - private val diameter = 180 - private val strokeWidth = 12f - init { - preferredSize = Dimension(diameter + 40, diameter + 40) + minimumSize = Dimension(80, 80) + preferredSize = Dimension(200, 200) + maximumSize = Dimension(Int.MAX_VALUE, Int.MAX_VALUE) isOpaque = false } @@ -38,43 +35,39 @@ class CircularTimerPanel : JPanel() { super.paintComponent(g) val g2d = g as Graphics2D - // Enable antialiasing for smooth circles g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON) g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON) + // Derive all dimensions from actual panel size so it scales at any size + val padding = 16 + val diameter = (minOf(width, height) - padding).coerceIn(60, 180) + val strokeWidth = (diameter * 0.065f).coerceIn(5f, 15f) + val fontSize = (diameter * 0.19f).coerceIn(12f, 44f).toInt() + val centerX = width / 2 val centerY = height / 2 val radius = diameter / 2 - // Calculate bounds for the arc val arcX = centerX - radius val arcY = centerY - radius - val arcSize = diameter - // Draw background circle (full circle in light gray) + // Background track g2d.stroke = BasicStroke(strokeWidth, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND) g2d.color = backgroundColor - g2d.drawArc(arcX, arcY, arcSize, arcSize, 0, 360) + g2d.drawArc(arcX, arcY, diameter, diameter, 0, 360) - // Draw progress arc (clockwise from top, depleting) - // Start at 90 degrees (top of circle) and go clockwise + // Progress arc (clockwise from top, depleting) val arcAngle = (360 * progress).toInt() g2d.color = if (isBreakTime) breakColor else workColor g2d.stroke = BasicStroke(strokeWidth, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND) - g2d.drawArc(arcX, arcY, arcSize, arcSize, 90, -arcAngle) // Negative for clockwise + g2d.drawArc(arcX, arcY, diameter, diameter, 90, -arcAngle) - // Draw time text in center + // Time text centered inside arc g2d.color = UIManager.getColor("Label.foreground") ?: Color.BLACK - val font = Font("SansSerif", Font.BOLD, 36) - g2d.font = font - + g2d.font = Font("SansSerif", Font.BOLD, fontSize) val metrics = g2d.fontMetrics - val textWidth = metrics.stringWidth(timeText) - val textHeight = metrics.height - - val textX = centerX - textWidth / 2 - val textY = centerY + textHeight / 4 - + val textX = centerX - metrics.stringWidth(timeText) / 2 + val textY = centerY + metrics.height / 4 g2d.drawString(timeText, textX, textY) } -} \ No newline at end of file +}