class ResizableFramelessContainer(QWidget): """A resizable frameless container ResizableFramelessContainer can be moved and resized by mouse. Call `attach_widget` to attach an inner widget and `detach` to detach the inner widget. NOTE: this is mainly designed for picture in picture mode currently. """ def __init__(self, ): super().__init__(parent=None) self._widget = None self._timer = QTimer(self) self._old_pos = None self._widget = None self._size_grip = QSizeGrip(self) self._timer.timeout.connect(self.__on_timeout) # setup window layout self.setWindowFlags(Qt.WindowStaysOnTopHint | Qt.FramelessWindowHint) self._size_grip.setFixedSize(20, 20) self._layout = QVBoxLayout(self) self._layout.setContentsMargins(0, 0, 0, 0) self._layout.setSpacing(0) self._layout.addWidget(self._size_grip) self._layout.setAlignment(self._size_grip, Qt.AlignBottom | Qt.AlignRight) self.setMouseTracking(True) def attach_widget(self, widget): """set inner widget""" self._widget = widget self._widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self._layout.insertWidget(0, self._widget) def detach(self): self._layout.removeWidget(self._widget) self._widget = None def paintEvent(self, e): painter = QPainter(self) if self._size_grip.isVisible(): painter.save() painter.setPen(QColor('white')) option = QTextOption() option.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) rect = QRect(self._size_grip.pos(), self._size_grip.size()) painter.drawText(QRectF(rect), '●', option) painter.restore() def mousePressEvent(self, e): self._old_pos = e.globalPos() def mouseMoveEvent(self, e): # NOTE: e.button() == Qt.LeftButton don't work on Windows # on Windows, even I drag with LeftButton, the e.button() return 0, # which means no button if self._old_pos is not None: delta = e.globalPos() - self._old_pos self.move(self.x() + delta.x(), self.y() + delta.y()) self._old_pos = e.globalPos() def mouseReleaseEvent(self, e): self._old_pos = None def enterEvent(self, e): super().enterEvent(e) if not self._size_grip.isVisible(): self.resize(self.width(), self.height() + 20) self._size_grip.show() self._timer.stop() def leaveEvent(self, e): super().leaveEvent(e) self._timer.start(2000) def resizeEvent(self, e): super().resizeEvent(e) def __on_timeout(self): if self._size_grip.isVisible(): self._size_grip.hide() self.resize(self.width(), self.height() - 20)
class PSticky(QDialog): closed = pyqtSignal() __title_bar_height__ = 32 __bottom_grip__size__ = 16 __window_margin__ = 1 def __init__(self, parent: QWidget, inner: bool = False): if not inner: super().__init__() self.inner = inner self.__parent__ = parent if self.__parent__ is not None: self.__central_widget__ = None self.__follow__ = False self.HAnchor = Parapluie.HCenter self.VAnchor = Parapluie.VCenter self.__update_distance__ = False self.__const_hDistance__ = False self.__const_vDistance__ = False self.leftDistance = 0 self.rightDistance = 0 self.topDistance = 0 self.bottomDistance = 0 PFunction.applyStyle(self, PResource.stylesheet()) self.setWindowFlags(Qt.FramelessWindowHint) self.setObjectName(Parapluie.Object_StickyWindow) self.title = QLabel("Parapluie") self.title.setStyleSheet("font-size: 14px; font-weight: bold;") self.title.setContentsMargins(8, 0, 8, 0) self.title.setMinimumHeight(self.__title_bar_height__) self.bottom_sizeGrip = QSizeGrip(self) self.bottom_sizeGrip.setObjectName(Parapluie.Object_StickyWindow_ResizeBottom) self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) self.titleBar = QHBoxLayout() self.titleBar.setContentsMargins(0, 0, 0, 0) self.titleBar.addWidget(self.title, alignment=Qt.AlignLeft | Qt.AlignCenter) self.titleBar.addStretch() self.moveWidget = PMoveWidget(self) self.moveWidget.setObjectName(Parapluie.Object_StickyWindow_FunctionButton) self.moveWidget.setLayout(self.titleBar) self.moveWidget.setMaximumHeight(self.__title_bar_height__) self.mainLayout = QVBoxLayout() self.mainLayout.setContentsMargins(self.__window_margin__, self.__window_margin__, self.__window_margin__, self.__window_margin__) self.mainLayout.setSpacing(0) self.mainLayout.addWidget(self.moveWidget, alignment=Qt.AlignTop) self.mainLayout.addWidget(self.bottom_sizeGrip, alignment=Qt.AlignRight) self.setLayout(self.mainLayout) if isinstance(parent, Sticky.PWindow): self.updateDistance() parent.resized.connect(self.followParentRelative) parent.closed.connect(self.close) parent.minimized.connect(self.minimizedFollowParent) def updateDistance(self): if self.__parent__ is not None: if not self.__const_hDistance__: self.leftDistance = self.x() - self.__parent__.x() self.rightDistance = (self.x() + self.width()) - self.__parent__.x() - self.__parent__.width() if not self.__const_vDistance__: self.topDistance = self.y() - self.__parent__.y() self.bottomDistance = (self.y() + self.height()) - self.__parent__.y() - self.__parent__.height() def moveEvent(self, a0: QtGui.QMoveEvent): if self.__parent__ is not None: super(PSticky, self).moveEvent(a0) if self.__update_distance__: self.updateDistance() def resizeEvent(self, a0: QtGui.QResizeEvent): if self.__parent__ is not None: super(PSticky, self).resizeEvent(a0) if self.__update_distance__: self.updateDistance() def closeEvent(self, a0: QtGui.QCloseEvent): if self.__parent__ is not None: self.completeDestroy(0) self.closed.emit() def setWindowTitle(self, *args): if self.__parent__ is not None: super(PSticky, self).setWindowTitle(*args) self.title.setText(*args) def setCentralWidget(self, widget: QWidget): if self.__parent__ is not None: self.mainLayout.insertWidget(1, widget) self.__central_widget__ = widget self.__central_widget__.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) def addWindowAction(self, button): if self.__parent__ is not None: self.titleBar.addWidget(button, alignment=Qt.AlignLeft | Qt.AlignTop) def setResizable(self, resize=False): if self.__parent__ is not None: self.bottom_sizeGrip.setVisible(resize and not (self.__const_vDistance__ or self.__const_hDistance__)) def isResizable(self): return self.bottom_sizeGrip.isVisible() def setMovable(self, movable: bool): if self.__parent__ is not None: self.moveWidget.setMovable(movable and not (self.__const_vDistance__ or self.__const_hDistance__)) def isMovable(self) -> bool: return self.moveWidget.isMovable() def completeDestroy(self, code: int = 0): self.done(code) self.__parent__ = None def setAnchor(self, hAnchor: int, vAnchor: int, hDistance: int = None, vDistance: int = None): if self.__parent__ is not None: self.HAnchor = hAnchor self.VAnchor = vAnchor if hDistance is not None: self.leftDistance = hDistance self.rightDistance = hDistance self.__const_hDistance__ = True else: self.__const_hDistance__ = False if vDistance is not None: self.topDistance = vDistance self.bottomDistance = vDistance self.__const_vDistance__ = True else: self.__const_vDistance__ = False self.setMovable(not (self.__const_vDistance__ or self.__const_hDistance__)) self.setResizable(not (self.__const_vDistance__ or self.__const_hDistance__)) self.followParentRelative() def enableFollowParentRelative(self) -> bool: if self.__parent__ is not None: self.__follow__ = isinstance(self.__parent__, Sticky.PWindow) if self.__follow__: self.setWindowFlags(self.windowFlags() | Qt.WindowStaysOnTopHint) return self.__follow__ return False def minimizedFollowParent(self): if self.__parent__ is not None: minimize = self.__parent__.isMinimized() if minimize: self.hide() else: self.show() self.followParentRelative() def followParentRelative(self): if self.__follow__ and self.__parent__ is not None: if self.inner: pPoint = self.__parent__.mapToGlobal(QPoint(self.__parent__.x(), self.__parent__.y())) else: pPoint = QPoint(0, 0) if self.HAnchor == Parapluie.HCenter: x = pPoint.x() + self.__parent__.width() / 2 - (self.width() / 2) elif self.HAnchor == Parapluie.Left: x = pPoint.x() + self.leftDistance elif self.HAnchor == Parapluie.Right: x = pPoint.x() + self.__parent__.width() + self.rightDistance - self.width() else: x = 0 if self.VAnchor == Parapluie.VCenter: y = pPoint.y() + self.__parent__.height() / 2 - (self.height() / 2) elif self.VAnchor == Parapluie.Top: y = pPoint.y() + self.topDistance elif self.VAnchor == Parapluie.Bottom: y = pPoint.y() + self.__parent__.height() + self.bottomDistance - self.height() else: y = 0 if x > self.__parent__.width(): x = self.__parent__.width() elif x < 0: x = 0 if y > self.__parent__.height(): y = self.__parent__.height() elif y < 0: y = 0 self.__update_distance__ = False self.move(x, y) self.__update_distance__ = True