Search code examples
python-3.xpyqt5qtabbar

How to track the local position of a movable QTab?


I am currently attempting to set boundaries so that tabs cannot move beyond on each end of my QTabBar. By default the movable tab just disappears into void whenever it is dragged outside of the QTabBar's area - An example of the QTab being dragged outside of the QTabBar's Area.

This is fine for the most part, but visually I would like the tab to stop moving entirely once it reaches the edge of the QTabBar. I've attempted to accomplish this goal via Qt's event system, and currently only have support for the left boundary. Whenever the user clicks on a QTab, I record his mouse's initial position and the current tabs unmoved position and size. I then use these values to determine at which x-position the current tab would reach the edges of the QTabBar if the user were to move his mouse left. If this position is ever reached the event is filtered preventing any further movement of the tab.

    def eventFilter(self, source, event):
    if event.type() == QEvent.Type.MouseButtonPress:
        self.startingpos = event.x()
        self.tabOrigin = self.curTab.x()
        self.tabRoom = self.startingpos - self.tabOrigin

    if event.type() == QEvent.Type.MouseMove:
        if self.curIndex != self.tabs.currentIndex():
            self.curIndex = self.tabs.currentIndex()
            self.curTab = self.tabs.tabBar().tabRect(self.curIndex)

        if event.x() < self.tabRoom:
            return True
    return False

This tactic is effective unless the user quickly moves his mouse left. This causes the moving tab to get stuck at the last recorded position of the mouse before it went past the specified boundary, before reaching the QTabBar's edge - An example of the QTab getting stuck before reaching the left side of the QTabBar.

I'm not exactly sure how to get around this issue. I understand that the moving tab is not actually the same one as the tab that is originally clicked. Is there anyway to access the position of the moving tab and manually set its position? If not, any advice about how else to go about solving this problem would be greatly appreciated.


Solution

  • Mouse events are not linear, they "skip" pixels if the user moves the mouse too fast.

    The problem is that the "moving tab" is completely implemented internally, so there's no access to that tab (which, in fact, is temporarily a "fake" widget drawn over the remaining tabs).

    A possible solution is to detect the current pressed tab, get the horizontal mouse position relative to that tab, check whether the movement goes beyond the limits (0 on the left, the right of the last tab for the right margin), synthesize a new mouse event based on that, and post that event.

        def eventFilter(self, source, event):
            if (event.type() == event.MouseButtonPress and 
                event.button() == QtCore.Qt.LeftButton):
                    tabRect = source.tabRect(source.tabAt(event.pos()))
                    self.leftDistance = event.x() - tabRect.left()
                    self.rightMargin = tabRect.right() - event.x()
                    self.rightLimit = source.tabRect(self.tabs.count() - 1).right()
            elif (event.type() == event.MouseMove and 
                event.buttons() == QtCore.Qt.LeftButton):
                    if event.x() - self.leftDistance < 0:
                        pos = QtCore.QPoint(self.leftDistance, event.y())
                        newEvent = QtGui.QMouseEvent(
                            event.type(), pos, 
                            event.button(), event.buttons(), 
                            event.modifiers())
                        QtWidgets.QApplication.postEvent(source, newEvent)
                        return True
                    elif event.x() + self.rightMargin > self.rightLimit:
                        pos = QtCore.QPoint(
                            self.rightLimit - self.rightMargin, event.y())
                        newEvent = QtGui.QMouseEvent(
                            event.type(), pos, 
                            event.button(), event.buttons(), 
                            event.modifiers())
                        QtWidgets.QApplication.postEvent(source, newEvent)
                        return True
            return super().eventFilter(source, event)
    

    Note that eventFilter() should always return the base implementation, otherwise you could face unexpected behavior in some situations.