Files
pax 314bcf90fe feat(gui): SVG for the remaining text glyph icons
grid star, multi-site tick, loop once, layout grips, folder and stepper buttons
2026-09-30 18:34:35 -05:00

161 lines
5.9 KiB
Python

"""Drag overlay for rearranging panels (model in panel_layout).
Panels never leave the main window. A drag only picks a zone on another
panel and main_window re-parents the widgets, so nothing depends on
global window coordinates (which Wayland clients never get).
"""
from __future__ import annotations
from PySide6.QtCore import QEvent, QRect, Qt, Signal
from PySide6.QtGui import QColor, QPainter, QPalette
from PySide6.QtWidgets import QApplication, QWidget
from .icons import render
from .panel_layout import PANEL_MIME, PANEL_TITLES, Rect, grip_rect, hit_zone, zone_rects
class LayoutOverlay(QWidget):
"""Over the main splitter: edit-mode grips, and the drop target.
Parented to the splitter's parent, not the splitter itself: QSplitter
adopts every child widget as a pane.
"""
drag_requested = Signal(str)
finished = Signal()
def __init__(self, splitter: QWidget, panels: dict[str, QWidget], edit: bool) -> None:
super().__init__(splitter.parentWidget())
self._splitter = splitter
self._panels = panels
self._edit = edit
self._hover: tuple[str, str] | None = None
self._press: tuple[str, object] | None = None
self.dragging: str | None = None
self.dropped: tuple[str, str] | None = None
self.setAcceptDrops(True)
self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
splitter.installEventFilter(self)
self.setGeometry(splitter.geometry())
self.raise_()
self.show()
if edit:
self.setFocus()
def eventFilter(self, obj, event) -> bool:
if obj is self._splitter and event.type() in (QEvent.Type.Resize, QEvent.Type.Move):
self.setGeometry(self._splitter.geometry())
return False
def panel_rects(self) -> dict[str, Rect]:
out = {}
for name, w in self._panels.items():
if w.isVisible():
tl = w.mapTo(self.parentWidget(), w.rect().topLeft()) - self.pos()
out[name] = (tl.x(), tl.y(), w.width(), w.height())
return out
# edit-mode grips
def _grip_at(self, pos) -> str | None:
for name, rect in self.panel_rects().items():
if QRect(*grip_rect(rect)).contains(pos):
return name
return None
def mousePressEvent(self, event) -> None:
name = self._grip_at(event.position().toPoint()) if self._edit else None
self._press = (name, event.position().toPoint()) if name else None
def mouseMoveEvent(self, event) -> None:
if self._press and (
event.position().toPoint() - self._press[1]
).manhattanLength() >= QApplication.startDragDistance():
name = self._press[0]
self._press = None
self.drag_requested.emit(name)
def mouseReleaseEvent(self, event) -> None:
self._press = None
def keyPressEvent(self, event) -> None:
if event.key() == Qt.Key.Key_Escape and self._edit:
self.finished.emit()
else:
super().keyPressEvent(event)
# drop target
def _hit(self, event) -> tuple[str, str] | None:
p = event.position().toPoint()
return hit_zone(self.panel_rects(), p.x(), p.y(), self.dragging)
def dragEnterEvent(self, event) -> None:
if event.mimeData().hasFormat(PANEL_MIME):
event.acceptProposedAction()
def dragMoveEvent(self, event) -> None:
hit = self._hit(event)
if hit != self._hover:
self._hover = hit
self.update()
if hit:
event.acceptProposedAction()
else:
event.ignore()
def dragLeaveEvent(self, event) -> None:
self._hover = None
self.update()
def dropEvent(self, event) -> None:
self.dropped = self._hit(event)
self._hover = None
self.update()
if self.dropped:
event.acceptProposedAction()
def paintEvent(self, event) -> None:
painter = QPainter(self)
pal = self.palette()
accent = pal.color(QPalette.ColorRole.Highlight)
rects = self.panel_rects()
if self._edit:
dim = QColor(pal.color(QPalette.ColorRole.Window))
dim.setAlpha(150)
painter.fillRect(self.rect(), dim)
if self.dragging:
for name, rect in rects.items():
if name == self.dragging:
continue
for zone, r in zone_rects(*rect).items():
fill = QColor(accent)
fill.setAlpha(140 if self._hover == (name, zone) else 40)
painter.fillRect(QRect(*r), fill)
painter.setPen(accent)
for rect in rects.values():
painter.drawRect(QRect(*rect).adjusted(0, 0, -1, -1))
if self._edit:
for name, rect in rects.items():
g = QRect(*grip_rect(rect))
painter.fillRect(g, pal.color(QPalette.ColorRole.Button))
painter.drawRect(g.adjusted(0, 0, -1, -1))
ink = pal.color(QPalette.ColorRole.ButtonText)
painter.setPen(ink)
# SVG grip plus the title, centred together (was a "≡" glyph)
title = PANEL_TITLES[name]
tw = painter.fontMetrics().horizontalAdvance(title)
x = g.center().x() - (14 + 6 + tw) // 2
painter.drawPixmap(x, g.center().y() - 7, render("grip", ink, 14, self.devicePixelRatioF()))
painter.drawText(QRect(x + 20, g.top(), tw + 2, g.height()),
Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignLeft, title)
painter.setPen(accent)
painter.setPen(pal.color(QPalette.ColorRole.WindowText))
painter.drawText(
self.rect().adjusted(0, 0, 0, -12),
Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignBottom,
"Drag panels to rearrange. Esc or Ctrl+E to finish.",
)
painter.end()