@@ -18,23 +18,26 @@ import java.awt.Color
1818import java.awt.Dimension
1919import java.awt.FlowLayout
2020import java.awt.Font
21+ import java.awt.GridBagConstraints
22+ import java.awt.GridBagLayout
2123import java.awt.event.ComponentAdapter
2224import java.awt.event.ComponentEvent
2325import javax.swing.*
2426
2527class PomodoroToolWindowPanel (private val project : Project ) : JBPanel<JBPanel<*>>(BorderLayout ()), Disposable {
2628
27- private val timerService = project.getService(PomodoroTimerService ::class .java) ? : error(" PomodoroTimerService not available" )
29+ private val timerService = project.getService(PomodoroTimerService ::class .java)
30+ ? : error(" PomodoroTimerService not available" )
2831
29- // Layout orientation tracking
30- private var isHorizontalLayout = false
32+ private enum class LayoutMode { COMPACT , VERTICAL , HORIZONTAL }
33+ private var currentLayout = LayoutMode . VERTICAL
3134
3235 // Mode selector
3336 private val modeComboBox = JComboBox (PomodoroMode .entries.toTypedArray()).apply {
3437 selectedItem = PomodoroMode .CLASSIC
3538 }
3639
37- // Setting button
40+ // Settings button
3841 private val settingsButton = JButton (AllIcons .General .Settings ).apply {
3942 toolTipText = " Settings"
4043 isBorderPainted = false
@@ -57,13 +60,10 @@ class PomodoroToolWindowPanel(private val project: Project) : JBPanel<JBPanel<*>
5760 private val phaseLabel = JLabel (" Focus" ).apply {
5861 horizontalAlignment = SwingConstants .CENTER
5962 font = font.deriveFont(Font .BOLD , 13f )
60- foreground = Color (74 , 144 , 226 ) // Matches workColor in CircularTimerPanel
63+ foreground = Color (74 , 144 , 226 )
6164 }
6265
63- // Circular timer display
6466 private val circularTimer = CircularTimerPanel ()
65-
66- // Session indicator with tomato icons
6767 private val sessionIndicator = SessionIndicatorPanel ()
6868
6969 // Control buttons
@@ -99,76 +99,193 @@ class PomodoroToolWindowPanel(private val project: Project) : JBPanel<JBPanel<*>
9999 setupLayoutListener()
100100 }
101101
102+ // ---------------------------------------------------------------------------
103+ // Layout routing
104+ // ---------------------------------------------------------------------------
105+
102106 private fun buildUI () {
103- if (isHorizontalLayout) {
104- buildHorizontalLayout()
105- } else {
106- buildVerticalLayout()
107+ when (currentLayout) {
108+ LayoutMode .COMPACT -> buildCompactLayout()
109+ LayoutMode .VERTICAL -> buildVerticalLayout()
110+ LayoutMode .HORIZONTAL -> buildHorizontalLayout()
111+ }
112+ }
113+
114+ /* *
115+ * Compact: either dimension < 160px.
116+ * Just the circular timer + a row of buttons. Everything else hidden.
117+ */
118+ private fun buildCompactLayout () {
119+ val timerPanel = JPanel (BorderLayout ()).apply {
120+ border = BorderFactory .createEmptyBorder(6 , 6 , 4 , 6 )
121+ add(circularTimer, BorderLayout .CENTER )
122+ }
123+ val buttonPanel = JPanel (FlowLayout (FlowLayout .CENTER , 4 , 2 )).apply {
124+ add(startButton)
125+ add(pauseButton)
126+ add(resetButton)
107127 }
128+ add(timerPanel, BorderLayout .CENTER )
129+ add(buttonPanel, BorderLayout .SOUTH )
108130 }
109131
132+ /* *
133+ * Vertical: height >= width.
134+ * Mode selector at top. Timer fills all remaining vertical space via
135+ * BorderLayout.CENTER so it grows/shrinks naturally. Controls pinned at bottom.
136+ *
137+ * Scenarios handled:
138+ * - Tall + narrow → small circle (min(width, timerHeight) drives diameter)
139+ * - Tall + wide → large circle (width becomes the constraint)
140+ */
110141 private fun buildVerticalLayout () {
111- // Top panel with mode selector
112142 val topPanel = JPanel (BorderLayout (5 , 5 )).apply {
113- border = BorderFactory .createEmptyBorder(10 , 10 , 5 , 10 )
143+ border = BorderFactory .createEmptyBorder(10 , 10 , 4 , 10 )
114144 add(modeComboBox, BorderLayout .CENTER )
115145 add(settingsButton, BorderLayout .EAST )
116146 }
117147
118- // Info panel
119- val infoPanel = JPanel (FlowLayout (FlowLayout .CENTER , 0 , 5 )).apply {
148+ val infoPanel = JPanel (FlowLayout (FlowLayout .CENTER , 0 , 4 )).apply {
120149 add(infoLabel)
121150 }
122151
123- // Timer panel
152+ // Timer lives in CENTER — it stretches to fill whatever height is left
124153 val timerPanel = JPanel (BorderLayout ()).apply {
125- border = BorderFactory .createEmptyBorder(15 , 10 , 10 , 10 )
154+ border = BorderFactory .createEmptyBorder(8 , 10 , 8 , 10 )
126155 add(circularTimer, BorderLayout .CENTER )
127156 }
128157
129- // Session text label panel
130- val sessionPanel = JPanel (FlowLayout (FlowLayout .CENTER , 0 , 5 )).apply {
158+ val phaseLabelPanel = JPanel (FlowLayout (FlowLayout .CENTER , 0 , 2 )).apply {
159+ add(phaseLabel)
160+ }
161+ val sessionPanel = JPanel (FlowLayout (FlowLayout .CENTER , 0 , 2 )).apply {
131162 add(sessionTextLabel)
132163 }
133-
134- // Progress panel
135164 val progressPanel = JPanel (BorderLayout (5 , 5 )).apply {
136- border = BorderFactory .createEmptyBorder(5 , 20 , 10 , 20 )
165+ border = BorderFactory .createEmptyBorder(4 , 20 , 4 , 20 )
137166 add(sessionIndicator, BorderLayout .CENTER )
138167 }
139-
140- // Button panel
141168 val buttonPanel = JPanel (FlowLayout (FlowLayout .CENTER , 8 , 5 )).apply {
142169 add(startButton)
143170 add(pauseButton)
144171 add(resetButton)
145172 }
146173
147- // Phase label panel
148- val phaseLabelPanel = JPanel (FlowLayout (FlowLayout .CENTER , 0 , 2 )).apply {
149- add(phaseLabel)
150- }
151-
152- // Center content
153- val centerPanel = JPanel ().apply {
174+ // Fixed-height controls below the timer
175+ val controlsPanel = JPanel ().apply {
154176 layout = BoxLayout (this , BoxLayout .Y_AXIS )
155- add(infoPanel)
156- add(timerPanel)
157177 add(phaseLabelPanel)
158178 add(sessionPanel)
159179 add(progressPanel)
160180 add(buttonPanel)
161181 }
162182
183+ val centerPanel = JPanel (BorderLayout ()).apply {
184+ add(infoPanel, BorderLayout .NORTH )
185+ add(timerPanel, BorderLayout .CENTER ) // ← grows with panel
186+ add(controlsPanel, BorderLayout .SOUTH )
187+ }
188+
163189 add(topPanel, BorderLayout .NORTH )
164190 add(centerPanel, BorderLayout .CENTER )
165191 add(settingsPanel, BorderLayout .SOUTH )
166192 }
167193
194+ /* *
195+ * Horizontal: width > height.
196+ * Mode selector spans the top. Timer takes left 55%, controls take right 45%.
197+ *
198+ * Scenarios handled:
199+ * - Wide + tall → large circle (height drives diameter), ample control space
200+ * - Wide + short → smaller circle, controls stack compactly on the right
201+ */
168202 private fun buildHorizontalLayout () {
169- buildVerticalLayout()
203+ val topPanel = JPanel (BorderLayout (5 , 5 )).apply {
204+ border = BorderFactory .createEmptyBorder(8 , 10 , 4 , 10 )
205+ add(modeComboBox, BorderLayout .CENTER )
206+ add(settingsButton, BorderLayout .EAST )
207+ }
208+
209+ val timerPanel = JPanel (BorderLayout ()).apply {
210+ border = BorderFactory .createEmptyBorder(8 , 12 , 8 , 6 )
211+ add(circularTimer, BorderLayout .CENTER )
212+ }
213+
214+ val phaseLabelPanel = JPanel (FlowLayout (FlowLayout .CENTER , 0 , 2 )).apply { add(phaseLabel) }
215+ val sessionPanel = JPanel (FlowLayout (FlowLayout .CENTER , 0 , 2 )).apply { add(sessionTextLabel) }
216+ val progressPanel = JPanel (BorderLayout (5 , 5 )).apply {
217+ border = BorderFactory .createEmptyBorder(4 , 8 , 4 , 8 )
218+ add(sessionIndicator, BorderLayout .CENTER )
219+ }
220+ val buttonPanel = JPanel (FlowLayout (FlowLayout .CENTER , 6 , 4 )).apply {
221+ add(startButton)
222+ add(pauseButton)
223+ add(resetButton)
224+ }
225+
226+ // Controls centered vertically on the right side
227+ val rightPanel = JPanel ().apply {
228+ layout = BoxLayout (this , BoxLayout .Y_AXIS )
229+ border = BorderFactory .createEmptyBorder(4 , 4 , 4 , 12 )
230+ add(Box .createVerticalGlue())
231+ add(phaseLabelPanel)
232+ add(sessionPanel)
233+ add(progressPanel)
234+ add(buttonPanel)
235+ add(Box .createVerticalGlue())
236+ }
237+
238+ // Split: timer 55% | controls 45%
239+ val splitPanel = JPanel (GridBagLayout ()).apply {
240+ val gbc = GridBagConstraints ().apply {
241+ fill = GridBagConstraints .BOTH
242+ weighty = 1.0
243+ }
244+ gbc.weightx = 0.55 ; gbc.gridx = 0 ; add(timerPanel, gbc)
245+ gbc.weightx = 0.45 ; gbc.gridx = 1 ; add(rightPanel, gbc)
246+ }
247+
248+ add(topPanel, BorderLayout .NORTH )
249+ add(splitPanel, BorderLayout .CENTER )
250+ add(settingsPanel, BorderLayout .SOUTH )
251+ }
252+
253+ // ---------------------------------------------------------------------------
254+ // Responsive layout detection
255+ // ---------------------------------------------------------------------------
256+
257+ private fun setupLayoutListener () {
258+ addComponentListener(object : ComponentAdapter () {
259+ override fun componentResized (e : ComponentEvent ? ) {
260+ checkAndUpdateLayout()
261+ }
262+ })
263+ }
264+
265+ private fun checkAndUpdateLayout () {
266+ val newLayout = when {
267+ width < 160 || height < 160 -> LayoutMode .COMPACT
268+ width > height -> LayoutMode .HORIZONTAL
269+ else -> LayoutMode .VERTICAL
270+ }
271+ if (newLayout != currentLayout) {
272+ currentLayout = newLayout
273+ rebuildLayout()
274+ }
275+ }
276+
277+ private fun rebuildLayout () {
278+ removeAll()
279+ buildUI()
280+ updateSettingsPanelVisibility()
281+ revalidate()
282+ repaint()
170283 }
171284
285+ // ---------------------------------------------------------------------------
286+ // Listeners & helpers
287+ // ---------------------------------------------------------------------------
288+
172289 private fun setupListeners () {
173290 startButton.addActionListener { timerService.start() }
174291 pauseButton.addActionListener { timerService.pause() }
@@ -191,7 +308,8 @@ class PomodoroToolWindowPanel(private val project: Project) : JBPanel<JBPanel<*>
191308
192309 private fun updateSettingsPanelVisibility () {
193310 val isCustom = modeComboBox.selectedItem == PomodoroMode .CUSTOM
194- settingsPanel.isVisible = isCustom
311+ // Never show the custom settings panel in compact mode — no room for it
312+ settingsPanel.isVisible = isCustom && currentLayout != LayoutMode .COMPACT
195313 revalidate()
196314 repaint()
197315 }
@@ -206,35 +324,9 @@ class PomodoroToolWindowPanel(private val project: Project) : JBPanel<JBPanel<*>
206324 sessionTextLabel.text = " Session $currentSession of $totalSessions "
207325 }
208326
209- private fun setupLayoutListener () {
210- addComponentListener(object : ComponentAdapter () {
211- override fun componentResized (e : ComponentEvent ? ) {
212- checkAndUpdateLayout()
213- }
214- })
215- }
216-
217- private fun checkAndUpdateLayout () {
218- val width = width
219- val height = height
220-
221- // Determine if we should use horizontal layout (width > height * 1.5)
222- val shouldBeHorizontal = width > height * 1.5
223-
224- // Only rebuild if layout orientation changed
225- if (shouldBeHorizontal != isHorizontalLayout) {
226- isHorizontalLayout = shouldBeHorizontal
227- rebuildLayout()
228- }
229- }
230-
231- private fun rebuildLayout () {
232- removeAll()
233- buildUI()
234- updateSettingsPanelVisibility()
235- revalidate()
236- repaint()
237- }
327+ // ---------------------------------------------------------------------------
328+ // Timer observation
329+ // ---------------------------------------------------------------------------
238330
239331 private fun observeTimer () {
240332 timeJob = scope.launch {
@@ -254,18 +346,15 @@ class PomodoroToolWindowPanel(private val project: Project) : JBPanel<JBPanel<*>
254346 pauseButton.isEnabled = it == PomodoroTimerService .TimerState .RUNNING
255347 resetButton.isEnabled = it != PomodoroTimerService .TimerState .IDLE
256348
257- // Update start button text based on state
258349 startButton.text = when (it) {
259350 PomodoroTimerService .TimerState .IDLE -> " Start"
260351 else -> " Resume"
261352 }
262353
263- // Clear all default values
264354 startButton.putClientProperty(" JButton.buttonType" , null )
265355 pauseButton.putClientProperty(" JButton.buttonType" , null )
266356 resetButton.putClientProperty(" JButton.buttonType" , null )
267357
268- // Update default button (blue highlight) based on state
269358 when (it) {
270359 PomodoroTimerService .TimerState .IDLE -> {
271360 startButton.putClientProperty(" JButton.buttonType" , " default" )
@@ -281,7 +370,6 @@ class PomodoroToolWindowPanel(private val project: Project) : JBPanel<JBPanel<*>
281370 }
282371 }
283372
284- // Check if we're truly idle (session and work phase) or just transitioning
285373 val currentSession = timerService.currentSession.value
286374 val currentPhase = timerService.currentPhase.value
287375 val isTrulyIdle = it == PomodoroTimerService .TimerState .IDLE &&
@@ -290,12 +378,11 @@ class PomodoroToolWindowPanel(private val project: Project) : JBPanel<JBPanel<*>
290378
291379 modeComboBox.isEnabled = isTrulyIdle
292380
293- // Hide custom settings panel when timer is active
294381 if (! isTrulyIdle && modeComboBox.selectedItem == PomodoroMode .CUSTOM ) {
295382 settingsPanel.isVisible = false
296383 revalidate()
297384 repaint()
298- } else if (isTrulyIdle){
385+ } else if (isTrulyIdle) {
299386 updateSettingsPanelVisibility()
300387 }
301388 }
@@ -322,10 +409,10 @@ class PomodoroToolWindowPanel(private val project: Project) : JBPanel<JBPanel<*>
322409 sessionIndicator.updateSessions(session, settings.sessionsPerRound, isBreak)
323410 if (isBreak) {
324411 phaseLabel.text = " Break"
325- phaseLabel.foreground = Color (243 , 156 , 18 ) // Matches breakColor
412+ phaseLabel.foreground = Color (243 , 156 , 18 )
326413 } else {
327414 phaseLabel.text = " Focus"
328- phaseLabel.foreground = Color (74 , 144 , 226 ) // Matches workColor
415+ phaseLabel.foreground = Color (74 , 144 , 226 )
329416 }
330417 }
331418 }
@@ -339,4 +426,4 @@ class PomodoroToolWindowPanel(private val project: Project) : JBPanel<JBPanel<*>
339426 phaseJob?.cancel()
340427 scope.cancel()
341428 }
342- }
429+ }
0 commit comments