Size the board and the rail by proportion, not by pixel constants

"Some screens scroll sideways, some don't" had two causes, and both were a
number that was right on one monitor.

The lanes. _KanbanColumn set a 150px floor with a comment claiming seven of
them fit a 1280 window; measured, the row wanted 1242px where 1280 leaves 1091,
so a 1280 screen scrolled and a 1920 one did not — this machine has one of each.
The floor is gone. The seven lanes share the board through the layout's
stretch, which is a proportion of whatever width there is, and the only pixel
question left — how narrow before scrolling beats squeezing — is answered from
the font: eight characters of a title plus padding, so it holds at 125%/150%
scaling too. No horizontal scroll now at 1280×720 through 1936×1048, at three
rail widths each, including the rail dragged to its maximum.

Each lane also grew its own scrollbar: QListWidget's column hint runs 1-6px past
its viewport, and which lanes overflowed changed with the window width — 36 of
38 widths had at least one. Cards word-wrap, so there was never anything to
reach sideways; the bar is off.

The rail. Its 360px ceiling was a quarter of a 1440 screen and more than that of
a 1280 one. It is now 22% of the window, capped at 360 — and recomputed on
resize, which is its own bug fixed: read once at construction off a not-yet-sized
window, it sat at 162px on every monitor, so the handle barely moved.

check_kanban_scroll covers five screen sizes × three rail widths. A rail dragged
to maximum on a 1280 screen still scrolls the board — that is the user's own
trade, so it is reported rather than failed.

19/19 checkers pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nam Pham Dinh Thanh
2026-08-18 13:00:05 +09:00
co-authored by Claude Opus 5
parent a0cef768c9
commit 77976405b1
4 changed files with 178 additions and 16 deletions
+39 -5
View File
@@ -53,11 +53,18 @@ class _KanbanColumn(QListWidget):
# → "Delete N selected" to bulk-remove tasks instead of one at a time.
self.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.setWordWrap(True)
# Narrow enough that all SEVEN lanes fit on one screen, which is what
# the design asks for — at 190 the seventh (Paused) fell off the right
# edge and needed a horizontal scroll to reach.
# 7 × 150 + 6 gaps = 1098px, inside the content area of a 1280 window.
self.setMinimumWidth(150)
# Cards wrap, so there is never anything to reach by scrolling sideways
# — but QListWidget's own column hint runs 1-6px past the viewport, and
# a lane sprouted a horizontal scrollbar at 36 of 38 window widths I
# measured. Which lanes grew one changed with the width, which is why it
# looked like it depended on the screen.
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setResizeMode(QListWidget.Adjust) # re-wrap on every resize
# No pixel floor here. A fixed one is always wrong on some screen:
# 190 lost the seventh lane, 150 still wanted 1242px where a 1280
# window leaves 1091 — so the 1280 monitor scrolled sideways and the
# 1920 one did not, same app, same build. The board divides whatever
# width it has by seven instead; see _fit_lanes().
def dropEvent(self, event): # noqa: N802
source = event.source()
@@ -146,6 +153,9 @@ class ScheduleTaskTab(QWidget):
cols.addWidget(holder)
self.columns[status] = col
self.column_headers[status] = head
self._board_scroll = scroll
self._board_gap = cols.spacing()
scroll.viewport().installEventFilter(self)
self._view_stack.addWidget(scroll)
self.calendar = CalendarView()
self.calendar.edit_task.connect(self._edit_task)
@@ -198,6 +208,30 @@ class ScheduleTaskTab(QWidget):
self._save_and_refresh(dlg.edited_task)
self.status_message.emit(tr("schedtask.msg_created"))
# ---- lane widths ------------------------------------------------------
#
# The seven lanes share the board equally — that is the layout's stretch
# doing the work, so the split is a proportion of whatever width there is,
# on any monitor. The only pixel question left is how narrow a lane may get
# before scrolling sideways beats squeezing, and that is a question about
# TEXT: roughly eight characters of a task title plus its padding. Reading
# it off the font keeps it right at 125%/150% scaling and at a user's own
# font size, where a constant would not be.
_LANE_FLOOR_CH = 8
def eventFilter(self, obj, event): # noqa: N802
from PySide6.QtCore import QEvent
if obj is self._board_scroll.viewport() and event.type() == QEvent.Resize:
self._fit_lanes()
return super().eventFilter(obj, event)
def _fit_lanes(self) -> None:
floor = self.fontMetrics().averageCharWidth() * self._LANE_FLOOR_CH + 24
for col in self.columns.values():
if col.minimumWidth() != floor:
col.setMinimumWidth(floor)
# ---- board rendering ---------------------------------------------------
def _card_text(self, t: dict) -> str:
prio = _PRIORITY_ICONS.get(t.get("priority", "medium"), "")