v0.33.0: title bars drawn by LinTunes, in the window's grey

On Wayland Qt paints the title bar through its adwaita decoration plugin,
which hardcodes pure white in light mode and ignores the palette. Every
window now goes frameless and gets an app-drawn bar in the palette's Window
color, so it follows the Background setting. Buttons follow GNOME's
button-layout; moving, snapping and edge resizing still go through the
compositor. Windows grow by the bar's height so their contents keep the
size they had.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-02 00:55:01 -07:00
co-authored by Claude Opus 5.5
parent c662ec0c2b
commit 4c0aaae463
6 changed files with 663 additions and 7 deletions
+190
View File
@@ -0,0 +1,190 @@
"""Round 72: LinTunes draws its own title bars.
On Wayland Qt's ``adwaita`` decoration plugin paints the title bar pure
white from hardcoded constants, so it can't follow the palette. Every window
goes frameless and gets ``gui/titlebar.TitleBar`` instead, in the palette's
Window color.
"""
import pytest
from PyQt6.QtCore import QPoint, QSize, Qt
from PyQt6.QtGui import QPalette
from PyQt6.QtWidgets import (
QApplication, QDialog, QLabel, QMainWindow, QMenu, QMessageBox,
QVBoxLayout, QWidget,
)
from lintunes.gui import titlebar
from lintunes.gui.art_window import ArtWindow
from lintunes.gui.titlebar import (
OPT_OUT, TitleBar, cursor_for, edges_at, parse_button_layout, title_bar,
)
@pytest.fixture
def installed(qapp, monkeypatch):
monkeypatch.setattr(titlebar, "_layout_cache",
parse_button_layout("close,minimize:appmenu"))
installer = titlebar.install(qapp)
yield installer
qapp.removeEventFilter(installer)
def _dialog(title="Preferences"):
dialog = QDialog()
dialog.setWindowTitle(title)
layout = QVBoxLayout(dialog)
layout.addWidget(QLabel("A label in a dialog"))
return dialog
def test_dialog_gets_frameless_bar_and_keeps_its_content_size(installed):
dialog = _dialog()
dialog.resize(400, 200)
dialog.show()
bar = title_bar(dialog)
assert isinstance(bar, TitleBar)
assert dialog.windowFlags() & Qt.WindowType.FramelessWindowHint
height = bar.bar_height()
assert dialog.contentsMargins().top() == height
# The bar used to sit outside the window; the contents keep their room.
assert dialog.size() == QSize(400, 200 + height)
assert bar.geometry().topLeft() == QPoint(0, 0)
assert bar.width() == dialog.width()
dialog.close()
def test_title_follows_window_title(installed):
dialog = _dialog("Get Info")
dialog.show()
bar = title_bar(dialog)
assert bar.title() == "Get Info"
dialog.setWindowTitle("Get Info — Reel One[*]")
dialog.setWindowModified(True)
assert bar.title() == "Get Info — Reel One*"
dialog.close()
def test_buttons_follow_gnome_layout(installed):
dialog = _dialog()
dialog.resize(400, 200)
dialog.show()
bar = title_bar(dialog)
kinds = [b.kind for b in bar._left]
assert kinds == ["close", "minimize"]
assert bar._right == []
assert bar._left[0].x() < bar._left[1].x() < bar.width() // 2
dialog.close()
def test_main_window_message_box_and_art_window_get_one(installed):
window = QMainWindow()
window.menuBar().addMenu("File")
window.setCentralWidget(QLabel("hello"))
window.show()
assert title_bar(window) is not None
# The menu bar sits below the bar, not under it.
assert window.menuBar().y() >= title_bar(window).height()
box = QMessageBox(QMessageBox.Icon.Question, "Delete?", "Really?",
QMessageBox.StandardButton.Yes, window)
box.show()
assert title_bar(box) is not None
box.close()
art = ArtWindow()
art.show()
assert title_bar(art) is not None
assert art.contentsRect().top() == title_bar(art).height()
art.close()
window.close()
def test_popups_and_opted_out_windows_are_left_alone(installed):
menu = QMenu()
menu.addAction("x")
menu.popup(QPoint(0, 0))
assert title_bar(menu) is None
menu.close()
plain = QWidget()
plain.setProperty(OPT_OUT, True)
plain.show()
assert title_bar(plain) is None
assert not plain.windowFlags() & Qt.WindowType.FramelessWindowHint
plain.close()
def test_fixed_size_dialog_has_no_maximize_and_stays_fixed(installed,
monkeypatch):
monkeypatch.setattr(titlebar, "_layout_cache",
parse_button_layout(":minimize,maximize,close"))
dialog = _dialog()
dialog.setFixedSize(300, 120)
dialog.show()
bar = title_bar(dialog)
assert [b.kind for b in bar._right] == ["minimize", "maximize", "close"]
maximize = bar._right[1]
assert not maximize.isVisibleTo(dialog)
assert dialog.minimumSize() == dialog.maximumSize() \
== QSize(300, 120 + bar.bar_height())
assert not titlebar.is_resizable(dialog)
# No resize from the top strip either.
assert not bar._top_edges(QPoint(1, 1))
dialog.close()
def test_parse_button_layout():
assert parse_button_layout("'close,minimize:appmenu'") == (
["close", "minimize"], [])
assert parse_button_layout(":minimize,maximize,close") == (
[], ["minimize", "maximize", "close"])
assert parse_button_layout("appmenu:close") == ([], ["close"])
def test_edges_at_sides_and_corners():
size = QSize(400, 300)
E = Qt.Edge
assert not edges_at(QPoint(200, 150), size)
assert edges_at(QPoint(0, 150), size) == E.LeftEdge
assert edges_at(QPoint(399, 150), size) == E.RightEdge
assert edges_at(QPoint(200, 299), size) == E.BottomEdge
assert edges_at(QPoint(200, 0), size) == E.TopEdge
assert edges_at(QPoint(0, 0), size) == E.LeftEdge | E.TopEdge
# Along an edge, near a corner, the corner is caught.
assert edges_at(QPoint(5, 299), size) == E.LeftEdge | E.BottomEdge
assert edges_at(QPoint(399, 290), size) == E.RightEdge | E.BottomEdge
assert cursor_for(E.LeftEdge | E.TopEdge) == Qt.CursorShape.SizeFDiagCursor
assert cursor_for(E.RightEdge | E.TopEdge) == Qt.CursorShape.SizeBDiagCursor
assert cursor_for(E.BottomEdge) == Qt.CursorShape.SizeVerCursor
def test_bar_paints_in_the_background_preference(installed, qapp, tmp_path):
from lintunes import theme
from lintunes.preferences import Preferences
saved = (QPalette(qapp.palette()), qapp.font(), qapp.styleSheet())
prefs = Preferences(tmp_path)
prefs.set("color_background", 224)
try:
theme.apply_theme(qapp, prefs)
dialog = _dialog()
dialog.resize(300, 150)
dialog.show()
image = title_bar(dialog).grab().toImage()
assert image.pixelColor(image.width() // 2, 3).name() == "#e0e0e0"
dialog.close()
finally:
qapp.setPalette(saved[0])
qapp.setFont(saved[1])
qapp.setStyleSheet(saved[2])
def test_maximized_window_hides_its_edges(installed):
dialog = _dialog()
dialog.resize(300, 150)
dialog.show()
bar = title_bar(dialog)
assert all(g.isVisibleTo(dialog) for g in bar._grips)
dialog.setWindowState(Qt.WindowState.WindowMaximized)
QApplication.processEvents()
assert not any(g.isVisibleTo(dialog) for g in bar._grips)
dialog.close()