diff --git a/README.md b/README.md index 425cde5..23774df 100644 --- a/README.md +++ b/README.md @@ -286,8 +286,7 @@ through a free program called **Syncthing**, and LinTunes just tells Syncthing what to do. Neither of you can change or delete anything on the other's computer. -You'll both need LinTunes and Syncthing. You **don't** need to be online at the -same time — things simply finish the next time both computers are on. +You'll both need LinTunes and Syncthing. ### 1. Install and start Syncthing (once) diff --git a/lintunes/__init__.py b/lintunes/__init__.py index 8156cc1..3633f2b 100644 --- a/lintunes/__init__.py +++ b/lintunes/__init__.py @@ -1,3 +1,3 @@ """LinTunes — iTunes-style music library manager and player for Linux.""" -__version__ = "0.31.6" +__version__ = "0.31.7" diff --git a/lintunes/gui/main_window.py b/lintunes/gui/main_window.py index e50c4bc..8264c5a 100644 --- a/lintunes/gui/main_window.py +++ b/lintunes/gui/main_window.py @@ -29,7 +29,7 @@ from lintunes.gui.cassette_ui import CassetteUi from lintunes.gui.device_sync_dialog import DeviceSyncSettingsDialog from lintunes.gui.export_dialog import ExportKindDialog, WebMixDialog from lintunes.gui.identify_dialog import IdentifyDialog -from lintunes.gui.sidebar import SidebarPanel +from lintunes.gui.sidebar import SIDEBAR_INSET, SidebarPanel from lintunes.gui.library_view import LibraryView from lintunes.gui.playlist_view import PlaylistView from lintunes.gui.transport import TransportBar @@ -125,6 +125,9 @@ class MainWindow(QMainWindow): splitter.addWidget(self._sidebar) self._content = QStackedWidget() + # The same strip of bare window on the right as the sidebar keeps on + # the left, so the lists sit symmetrically in the window. + self._content.setContentsMargins(0, 0, SIDEBAR_INSET, 0) splitter.addWidget(self._content) splitter.setSizes([220, 1060]) splitter.setCollapsible(1, False) diff --git a/lintunes/gui/transport.py b/lintunes/gui/transport.py index 410f03e..dcd5afb 100644 --- a/lintunes/gui/transport.py +++ b/lintunes/gui/transport.py @@ -20,15 +20,11 @@ ICON_GRAY = QColor("#4A4A4A") TOP_MARGIN = 9 # Between the row of boxes and the header row under it. ROW_GAP = 4 -# The now-playing panel is this share of the bar's width (700px on a 1920px -# screen), within these bounds. The row floats: the space before the first -# control, between each pair and after the last is one equal gap, so the -# whole row scales with the window instead of bunching up anywhere. -NOW_PLAYING_SHARE = 700 / 1920 -NOW_PLAYING_MIN = 220 -NOW_PLAYING_MAX = 1100 -# Floor for each of those gaps. -MIN_GAP = 12 +# The row of boxes is knolled: one GAP before the first control, between +# each pair and after the last, always. The now-playing panel (and the seek +# row under it) takes all the width that's left. +GAP = 16 +NOW_PLAYING_MIN = 180 # Shared height for the boxed side controls (transport, shuffle, bpm) and the # visualizer, so they line up. ~midway between their old heights: the stretchy @@ -381,20 +377,12 @@ class TransportBar(QWidget): # window could never shrink past the width it last had. outer.setSizeConstraint(QLayout.SizeConstraint.SetNoConstraint) layout = QHBoxLayout() - layout.setContentsMargins(MIN_GAP, 0, MIN_GAP, 0) + layout.setContentsMargins(GAP, 0, GAP, 0) layout.setSpacing(0) outer.addLayout(layout) - # Knolled: every control keeps its size and the gaps between them - # are equal; _size_now_playing sizes them (and the two ends). - self._row = layout - self._gaps: list[QSpacerItem] = [] - def gap(): - spacer = QSpacerItem(MIN_GAP, 0, QSizePolicy.Policy.Fixed, - QSizePolicy.Policy.Minimum) - self._gaps.append(spacer) - layout.addSpacerItem(spacer) + layout.addSpacing(GAP) # Rounded button boxes carry a user-tunable grayscale fill, decoupled # from the stripe color they used to share via palette(alternate-base). @@ -513,7 +501,7 @@ class TransportBar(QWidget): # the seek row hangs underneath the panel, in the header row. self._now_playing.setFixedHeight(CONTROL_HEIGHT) # Its width is set by _size_now_playing: all the room the controls - # and minimal gaps leave, up to NOW_PLAYING_MAX. + # and gaps leave. self._now_playing.setFixedWidth(NOW_PLAYING_MIN) layout.addWidget(self._now_playing, alignment=TOP) self._now_playing.installEventFilter(self) @@ -602,33 +590,17 @@ class TransportBar(QWidget): self._place_header() def _controls_width(self) -> int: - """Everything in the row of boxes but the now-playing panel, with - each gap at its floor.""" - return (2 * MIN_GAP - # A fixed width shows in the minimum, not the size hint. - + sum(max(w.sizeHint().width(), w.minimumWidth()) - for w in self._fixed_controls) - + len(self._fixed_controls) * MIN_GAP) + """Everything in the row of boxes but the now-playing panel: the + controls and a GAP before, between and after them.""" + # A fixed width shows in the minimum, not the size hint. + return (sum(max(w.sizeHint().width(), w.minimumWidth()) + for w in self._fixed_controls) + + (len(self._fixed_controls) + 2) * GAP) def _size_now_playing(self): - """The panel takes its share of the width; the rest is split into - equal gaps before, between and after the controls.""" - spaces = len(self._gaps) + 2 - fixed = sum(max(w.sizeHint().width(), w.minimumWidth()) - for w in self._fixed_controls) - panel = round(self.width() * NOW_PLAYING_SHARE) - panel = min(panel, self.width() - fixed - spaces * MIN_GAP) - panel = max(NOW_PLAYING_MIN, min(NOW_PLAYING_MAX, panel)) - self._now_playing.setFixedWidth(panel) - free = max(0, self.width() - fixed - panel) - gap = max(MIN_GAP, free // spaces) - for spacer in self._gaps: - spacer.changeSize(gap, 0, QSizePolicy.Policy.Fixed, - QSizePolicy.Policy.Minimum) - # Integer rounding's leftover pixels go to the right end. - end = max(MIN_GAP, free - gap * len(self._gaps)) - self._row.setContentsMargins(gap, 0, max(MIN_GAP, end - gap), 0) - self._row.invalidate() + """The panel fills whatever the controls and gaps leave.""" + self._now_playing.setFixedWidth( + max(NOW_PLAYING_MIN, self.width() - self._controls_width())) def minimumSizeHint(self): hint = super().minimumSizeHint() diff --git a/tests/test_round69.py b/tests/test_round69.py index f84ad52..0fc88fd 100644 --- a/tests/test_round69.py +++ b/tests/test_round69.py @@ -3,8 +3,8 @@ The Library button, the view's header (search box or playlist name) and the seek row share one row under the transport boxes: the Library button as wide as the sidebar, the header where the view starts, the seek row exactly under -the now-playing panel. The panel stops at NOW_PLAYING_MAX and the gaps between -the controls share what's left. +the now-playing panel. The row of boxes keeps one GAP before, between and +after its controls, and the panel fills the rest. """ from PyQt6.QtCore import QPoint @@ -112,18 +112,29 @@ def test_header_row_shares_the_seek_band(qapp, tmp_path): assert library.mapTo(bar, QPoint()).y() >= row.geometry().top() -def test_panel_scales_with_the_window(qapp, tmp_path): +def test_panel_fills_what_the_gaps_leave(qapp, tmp_path): window = _window(qapp, tmp_path, width=1920) bar = window._transport - assert abs(bar._now_playing.width() - 700) <= 1 + wide = bar._now_playing.width() + window.resize(1280, 500) + qapp.processEvents() + assert wide - bar._now_playing.width() == 640 window.resize(800, 500) qapp.processEvents() - assert NOW_PLAYING_MIN <= bar._now_playing.width() < 700 + assert bar._now_playing.width() >= NOW_PLAYING_MIN bpm = bar._bpm_button.parentWidget() assert bar._now_playing.geometry().right() < bpm.geometry().left() assert bpm.geometry().right() < bar.width() +def test_lists_have_bare_window_on_both_sides(qapp, tmp_path): + from lintunes.gui.sidebar import SIDEBAR_INSET + window = _window(qapp, tmp_path) + lists, view = window.sidebar.list_area, window._library_view + assert _gx(lists) - _gx(window) == SIDEBAR_INSET + assert _gx(window, window.width()) - _gx(view, view.width()) == SIDEBAR_INSET + + def test_views_seek_row_never_overlaps_the_header(qapp, tmp_path): window = _window(qapp, tmp_path, width=800) strip = window._library_view.header @@ -144,7 +155,7 @@ def test_long_name_shrinks_then_elides(qapp): def test_row_is_knolled_with_equal_gaps_and_ends(qapp, tmp_path): - from lintunes.gui.transport import MIN_GAP + from lintunes.gui.transport import GAP window = _window(qapp, tmp_path) bar = window._transport for width in (900, 1280, 1920): @@ -155,9 +166,6 @@ def test_row_is_knolled_with_equal_gaps_and_ends(qapp, tmp_path): bar._bpm_button.parentWidget()] edges = [(w.geometry().left(), w.geometry().right()) for w in row] gaps = [b[0] - a[1] - 1 for a, b in zip(edges, edges[1:])] - assert max(gaps) - min(gaps) <= 1 - assert gaps[0] >= MIN_GAP - # The ends get the same space as the gaps: the row floats. + # The same GAP everywhere, ends included, at every width. left_end, right_end = edges[0][0], bar.width() - 1 - edges[-1][1] - for end in (left_end, right_end): - assert abs(end - gaps[0]) <= 7 + assert gaps + [left_end, right_end] == [GAP] * 7