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:
@@ -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()
|
||||
Reference in New Issue
Block a user