Search code examples
qtpyqt5qt5pysidepyside6

Qt QWidget.move(x,y) fails to update widget on Wayland screen


I have a class QMovableResizableWidget. It sub-classes another class QResizableWidget, which is a subclass of QCustomWidget, a subclass of QWidget.

QMovableResizableWidget is given no parent widget, on initialization. I've been trying to get QMovableResizableWidget().move(x,y) to work. setGeometry works as intended but the QMovableResizableWidget refuses to move from its original position.

However after I move, the (x,y) position of the widget changes to the position I have moved, but not the widget itself (painted on the screen). [I've tried repainting, but this fails too.] Also, on calling move, the WA_Moved widget attribute is set to True, as expected.

I've searched extensively for this problem but can't find any pointers. Is there something I'm missing?

Below is a bare bones implementation of QMovableResizableWidget and snippets of it's ancestor classes:

import sys
from PySide6 import QtWidgets, QtCore, QtGui

Qt = QtCore.Qt
QMouseEvent = QtGui.QMouseEvent
QWidget = QtWidgets.QWidget
QEvent = QtCore.QEvent
CursorShape = Qt.CursorShape
QApplication = QtWidgets.QApplication
WidgetAttributes = Qt.WidgetAttribute
WindowTypes = Qt.WindowType
QPoint = QtCore.QPoint


class QCustomWidget(QWidget):
    def __init__(self, p, f):
        # type: (QWidget | None, WindowTypes) -> None
        super().__init__(p, f)
        self.setMouseTracking(True)
        
        

class QResizableWidget(QCustomWidget):
    def __init__(self,p, f):
        # type: (QWidget| None, WindowTypes) -> None
        super().__init__(p, f)
        pass
        

class QMovableResizableWidget(QCustomWidget):
    def __init__(self,p, f):
        # type: (QWidget | None, WindowTypes) -> None
        super().__init__(p, f)

        self.__moveInitLoc = None
        self.__isDragging = False
        self.setAttribute(Qt.WidgetAttribute.WA_MouseTracking, True)
        
    def event(self, ev):
        # type: (QEvent | QMouseEvent) -> bool
        if isinstance(ev, QMouseEvent):
            if (
                ev.type() == QEvent.Type.MouseButtonPress
                and ev.button() == Qt.MouseButton.LeftButton
            ):
                # print("Movable widget clicked")
                self.__isDragging = True
                self.__moveInitLoc = ev.globalPos()

            elif (
                ev.type() == QEvent.Type.MouseMove
                and self.__isDragging
            ):
                # dragging
                # print("ms move")
                if self.__moveInitLoc is not None:
                    self.setCursor(CursorShape.DragMoveCursor)
                    
                    diffLoc = ev.globalPos() - self.__moveInitLoc
                    newLoc = self.mapToGlobal(self.pos()) + diffLoc
                    self.move(newLoc) # x,y location updated in object. But object doesn't move
                    return True

            elif ev.type() == QEvent.Type.MouseButtonRelease:
                # Check if we were dragging
                # print("ms Released")
                self.__isDragging = False
                if self.__moveInitLoc is not None:
                    self.__moveInitLoc = None
                    self.unsetCursor()

        return super().event(ev)
    

if __name__ == "__main__":
    app = QApplication(sys.argv)
    app.setStyle("Fusion")

    window = QMovableResizableWidget(None, WindowTypes.Window | WindowTypes.FramelessWindowHint)

    window.resize(300, 300)
    window.show()
    sys.exit(app.exec())

Solution

  • Your main issue is within this line:

    newLoc = self.mapToGlobal(self.pos()) + diffLoc
    

    You're practically translating the widget position by its own global position: mapToGlobal maps a point in local coordinates to a point in global coordinates based on the global position of the widget.

    If you call self.mapToGlobal(self.pos()) of a widget that is shown at 10, 10 (to its parent, or to the whole desktop for top level widget), the result is 20, 20, or 10, 10 plus the screen coordinate of that widget.

    Also, once the widget has been moved, any further mapping will be done based on the new position.

    Here is a more appropriate implementation:

        mousePos = self.mapToParent(ev.pos())
        diffLoc = mousePos - self.__moveInitLoc
        newLoc = self.pos() + diffLoc
        self.move(newLoc)
        self.__moveInitLoc = mousePos
    

    Note that the first and last line (and their order related to the move() call) are extremely important, because they keep track of the mapped mouse position related to the current geometry of the widget and before it was moved.

    Also note that using mapToParent() is important because it considers the possibility of the widget being a top level one: mapToGlobal() only assumes that the position is in global coordinates, and that would be obviously wrong for children of another widget.

    Update about Wayland

    One of the limitations of Wayland is that it doesn't support setting a top level window geometry (position and size) from the client side. The solution, then, is to use the OS capabilities to actually move the window: get the QWindow (windowHandle()) of the current top level widget (window()), and simply call startSystemMove():

        def event(self, ev):
            # type: (QEvent | QMouseEvent) -> bool
            if (
                ev.type() == QEvent.Type.MouseButtonPress
                and ev.button() == Qt.MouseButton.LeftButton
            ):
                self.window().windowHandle().startSystemMove()
                event.accept()
                return True
            return super().event(ev)