fix: opaque faces for floating windows and theme-aware pop-out icons

A transparent top-level renders black on the container's non-composited
X11, so floating docks get a dynamic floating property feeding the
QSS [floating="true"] opaque face, and PopoutWindow sets
WA_StyledBackground. Pop-out titlebar glyphs are now painted in the
palette's WindowText color and re-tint on PaletteChange, so they stay
visible after a theme flip.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-08 18:40:23 +02:00
co-authored by Claude Fable 5
parent c697049ce6
commit 5ea4024f86
2 changed files with 60 additions and 12 deletions
+11 -1
View File
@@ -2897,7 +2897,17 @@ class MainWindow(QMainWindow):
@Slot(bool)
def _on_dock_top_level_changed(self, floating: bool) -> None:
dock = self.sender()
if floating and isinstance(dock, QDockWidget):
if not isinstance(dock, QDockWidget):
return
# Floating = top-level: a transparent top-level renders black on the
# container's non-composited X11, so the QDockWidget[floating="true"]
# QSS rule gives it an opaque face; docked it goes transparent again
# so the main-window gradient stays continuous.
dock.setProperty("floating", floating)
dock.style().unpolish(dock)
dock.style().polish(dock)
dock.update()
if floating:
# Pop-outs open enlarged instead of keeping the cramped docked
# size. Deferred: the window is mid-reparent while the signal
# fires.
+49 -11
View File
@@ -1,23 +1,24 @@
from PySide6.QtCore import QPoint, QRect, QSize, Qt
from PySide6.QtGui import QCursor, QGuiApplication, QIcon, QPainter, QPen, QPixmap
from PySide6.QtCore import QEvent, QPoint, QRect, QSize, Qt
from PySide6.QtGui import QColor, QCursor, QGuiApplication, QIcon, QPainter, QPalette, QPen, QPixmap
from PySide6.QtWidgets import QDockWidget, QHBoxLayout, QLabel, QToolButton, QVBoxLayout, QWidget
from aare.gui.styles import FRAME_L1_COLOR, FRAME_L1_WIDTH, TEXT, qcolor
from aare.gui.styles import FRAME_L1_COLOR, FRAME_L1_WIDTH, qcolor
# Title-bar buttons: icon fills the button, both the same size.
TITLEBAR_BUTTON_PX = 22
TITLEBAR_ICON_PX = 18
def _titlebar_icon(kind: str, size: int = TITLEBAR_ICON_PX) -> QIcon:
def _titlebar_icon(kind: str, color: QColor, size: int = TITLEBAR_ICON_PX) -> QIcon:
"""Hand-painted borderless glyphs — the style's standard title-bar
pixmaps draw boxed icons, and text glyphs are missing from the
container's fonts."""
container's fonts. Color comes from the caller's palette so the glyphs
follow the theme (they are pixmaps, QSS color cannot reach them)."""
pixmap = QPixmap(size, size)
pixmap.fill(Qt.GlobalColor.transparent)
painter = QPainter(pixmap)
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
pen = QPen(qcolor(TEXT), 2)
pen = QPen(color, 2)
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
pen.setJoinStyle(Qt.PenJoinStyle.RoundJoin)
painter.setPen(pen)
@@ -33,9 +34,9 @@ def _titlebar_icon(kind: str, size: int = TITLEBAR_ICON_PX) -> QIcon:
return QIcon(pixmap)
def _titlebar_button(parent: QWidget, kind: str, tooltip: str) -> QToolButton:
def _titlebar_button(parent: QWidget, tooltip: str) -> QToolButton:
# Icon is set by DockTitleBar._tint_icons (initially and on theme change).
button = QToolButton(parent)
button.setIcon(_titlebar_icon(kind))
button.setIconSize(QSize(TITLEBAR_ICON_PX, TITLEBAR_ICON_PX))
button.setFixedSize(TITLEBAR_BUTTON_PX, TITLEBAR_BUTTON_PX)
button.setAutoRaise(True)
@@ -66,15 +67,30 @@ class DockTitleBar(QWidget):
layout.addStretch(1)
self.popout_button = _titlebar_button(
self, "popout", "Open in a separate window (the panel stays here too)"
self, "Open in a separate window (the panel stays here too)"
)
self.popout_button.clicked.connect(on_popout)
layout.addWidget(self.popout_button)
close_button = _titlebar_button(self, "close", "Close panel (reopen via the View menu)")
close_button = _titlebar_button(self, "Close panel (reopen via the View menu)")
close_button.clicked.connect(dock.close)
layout.addWidget(close_button)
self._icon_buttons = {"popout": self.popout_button, "close": close_button}
self._tint_icons()
def _tint_icons(self) -> None:
color = self.palette().color(QPalette.ColorRole.WindowText)
for kind, button in self._icon_buttons.items():
button.setIcon(_titlebar_icon(kind, color))
def changeEvent(self, event):
# A theme switch lands here as a palette/style change; the glyphs are
# pixmaps, so they must be repainted in the new text color.
if event.type() in (QEvent.Type.PaletteChange, QEvent.Type.StyleChange):
self._tint_icons()
super().changeEvent(event)
class PopoutWindow(QWidget):
"""Additional top-level window for a panel mirror.
@@ -98,6 +114,10 @@ class PopoutWindow(QWidget):
def __init__(self, title: str, content: QWidget, parent=None):
super().__init__(parent, Qt.WindowType.Window)
# QWidget SUBCLASSES skip QSS background painting unless this is set;
# an unpainted top-level renders black on the container's
# non-composited X11 (the PopoutWindow QSS rule supplies the fill).
self.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True)
self.setWindowTitle(title)
self.setMinimumSize(300, 160)
layout = QVBoxLayout(self)
@@ -176,7 +196,7 @@ class PopoutWindow(QWidget):
super().mousePressEvent(event)
def mouseMoveEvent(self, event):
if self._manual_edges and self._press_global is not None and self._press_geom is not None:
if self._manual_edges and self._press_global is not None:
delta = event.globalPosition().toPoint() - self._press_global
geom = QRect(self._press_geom)
if self._manual_edges & Qt.Edge.LeftEdge:
@@ -201,3 +221,21 @@ class PopoutWindow(QWidget):
self._press_global = None
self._press_geom = None
super().mouseReleaseEvent(event)
if __name__ == "__main__":
# ponytail: smallest check that fails if the edge maths breaks
import os
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
from PySide6.QtWidgets import QApplication, QLabel
app = QApplication([])
w = PopoutWindow("t", QLabel("x"))
w.resize(400, 300)
assert w._edges_at(QPoint(5, 150)) == Qt.Edge.LeftEdge
assert w._edges_at(QPoint(398, 298)) == (Qt.Edge.RightEdge | Qt.Edge.BottomEdge)
assert w._edges_at(QPoint(200, 150)) == Qt.Edge(0)
assert w._cursor_for(Qt.Edge.LeftEdge | Qt.Edge.TopEdge) == Qt.CursorShape.SizeFDiagCursor
assert w._cursor_for(Qt.Edge.RightEdge | Qt.Edge.TopEdge) == Qt.CursorShape.SizeBDiagCursor
print("gude")