126 lines
5.3 KiB
Python
126 lines
5.3 KiB
Python
"""Borderless desktop window controls, with native move and resize gestures."""
|
||
|
||
from PySide6.QtCore import QEvent, Qt
|
||
from PySide6.QtWidgets import QHBoxLayout, QMenuBar, QPushButton, QWidget
|
||
|
||
|
||
class _ResizeHandle(QWidget):
|
||
def __init__(self, window, edges, cursor):
|
||
super().__init__(window)
|
||
self.edges = edges
|
||
self.setCursor(cursor)
|
||
self.setStyleSheet("background: transparent; border: none;")
|
||
|
||
def mousePressEvent(self, event):
|
||
if event.button() == Qt.LeftButton:
|
||
handle = self.window().windowHandle()
|
||
if handle and handle.startSystemResize(self.edges):
|
||
event.accept()
|
||
return
|
||
super().mousePressEvent(event)
|
||
|
||
|
||
class DesktopWindowBar(QMenuBar):
|
||
"""Keep account menus and window actions without the Windows frame."""
|
||
|
||
RESIZE_MARGIN = 5
|
||
|
||
def __init__(self, window):
|
||
super().__init__(window)
|
||
self.host = window
|
||
window.setWindowFlag(Qt.FramelessWindowHint, True)
|
||
window.setContentsMargins(0, 0, 0, 0)
|
||
self.setNativeMenuBar(False)
|
||
self.setFixedHeight(32)
|
||
self.setStyleSheet("""
|
||
QMenuBar { background: #F4F8FF; border: none; padding: 0 8px;
|
||
color: #52658E; font-size: 12px; }
|
||
QMenuBar::item { background: transparent; padding: 6px 10px; }
|
||
QMenuBar::item:selected { background: #E8EFFC; border-radius: 6px; }
|
||
QPushButton { background: transparent; border: none; border-radius: 0;
|
||
padding: 0; margin: 0; color: #52658E; }
|
||
QPushButton:hover { background: #E3EBFA; }
|
||
QPushButton#WindowClose:hover { background: #E95060; color: white; }
|
||
""")
|
||
actions = QWidget(self)
|
||
layout = QHBoxLayout(actions)
|
||
layout.setContentsMargins(0, 0, 0, 0)
|
||
layout.setSpacing(0)
|
||
self.minimize_button = self._button("−", "最小化", window.showMinimized, actions)
|
||
self.maximize_button = self._button("□", "最大化", self.toggle_maximized, actions)
|
||
self.close_button = self._button("×", "关闭", window.close, actions)
|
||
self.close_button.setObjectName("WindowClose")
|
||
for button in (self.minimize_button, self.maximize_button, self.close_button):
|
||
layout.addWidget(button)
|
||
self.setCornerWidget(actions, Qt.TopRightCorner)
|
||
|
||
left, right, top, bottom = Qt.LeftEdge, Qt.RightEdge, Qt.TopEdge, Qt.BottomEdge
|
||
self._resize_handles = {
|
||
edges: _ResizeHandle(window, edges, cursor)
|
||
for edges, cursor in (
|
||
(left, Qt.SizeHorCursor), (right, Qt.SizeHorCursor),
|
||
(top, Qt.SizeVerCursor), (bottom, Qt.SizeVerCursor),
|
||
(top | left, Qt.SizeFDiagCursor), (bottom | right, Qt.SizeFDiagCursor),
|
||
(top | right, Qt.SizeBDiagCursor), (bottom | left, Qt.SizeBDiagCursor),
|
||
)
|
||
}
|
||
window.installEventFilter(self)
|
||
|
||
@staticmethod
|
||
def _button(text, label, callback, parent):
|
||
button = QPushButton(text, parent)
|
||
button.setFixedSize(42, 32)
|
||
button.setToolTip(label)
|
||
button.setAccessibleName(label)
|
||
button.clicked.connect(callback)
|
||
return button
|
||
|
||
def toggle_maximized(self):
|
||
if self.host.isMaximized():
|
||
self.host.showNormal()
|
||
else:
|
||
self.host.showMaximized()
|
||
|
||
def mousePressEvent(self, event):
|
||
# Menu items keep their usual click behavior; empty space moves the window.
|
||
if event.button() == Qt.LeftButton and self.actionAt(event.position().toPoint()) is None:
|
||
handle = self.host.windowHandle()
|
||
if handle and handle.startSystemMove():
|
||
event.accept()
|
||
return
|
||
super().mousePressEvent(event)
|
||
|
||
def mouseDoubleClickEvent(self, event):
|
||
if event.button() == Qt.LeftButton and self.actionAt(event.position().toPoint()) is None:
|
||
self.toggle_maximized()
|
||
event.accept()
|
||
return
|
||
super().mouseDoubleClickEvent(event)
|
||
|
||
def eventFilter(self, watched, event):
|
||
if watched is self.host and event.type() in (
|
||
QEvent.Resize, QEvent.Show, QEvent.WindowStateChange,
|
||
):
|
||
self._update_handles()
|
||
label = "还原" if self.host.isMaximized() else "最大化"
|
||
self.maximize_button.setText("❐" if self.host.isMaximized() else "□")
|
||
self.maximize_button.setToolTip(label)
|
||
self.maximize_button.setAccessibleName(label)
|
||
return super().eventFilter(watched, event)
|
||
|
||
def _update_handles(self):
|
||
margin = self.RESIZE_MARGIN
|
||
width, height = self.host.width(), self.host.height()
|
||
for edges, handle in self._resize_handles.items():
|
||
horizontal = bool(edges & (Qt.LeftEdge | Qt.RightEdge))
|
||
vertical = bool(edges & (Qt.TopEdge | Qt.BottomEdge))
|
||
x = width - margin if edges & Qt.RightEdge else (0 if horizontal else margin)
|
||
y = height - margin if edges & Qt.BottomEdge else (0 if vertical else margin)
|
||
handle.setGeometry(
|
||
x, y,
|
||
margin if horizontal else max(0, width - 2 * margin),
|
||
margin if vertical else max(0, height - 2 * margin),
|
||
)
|
||
handle.setVisible(not (self.host.isMaximized() or self.host.isFullScreen()))
|
||
handle.raise_()
|