0

I had a solution in pyQt4 to undock-dock a tab from/to a QTabWidget by using QDockWidgets and the code below. After floating a tab, the re-docking was obtained by double clicking the titlebar of the QDockWidget. But it does not work any more in pyQt5 (the double-click does not seem to trigger a topLevelChanged event).

Why?

How to fix it and get back the proper re-docking behavior?

Where is this behavior change explained in the documentation?

Thanks for your help.

import sys
try:
      from PyQt5.QtCore import QEvent
      from PyQt5.QtWidgets import QApplication, QMainWindow, QDockWidget, QTabWidget, QLabel
except:
      from PyQt4.QtCore import QEvent
      from PyQt4.QtGui  import QApplication, QMainWindow, QDockWidget, QTabWidget, QLabel


class DockToTabWidget(QDockWidget):

    def __init__(self, title, parent=0):
        QDockWidget.__init__(self, title, parent)
        self._title = title
        self.topLevelChanged.connect(self.dockToTab)

    def dockToTab(self):
        if not self.isFloating():
            self.parent().addTab(self.widget(), self._title)
            self.close()
            del self


class TabWidgetWithUndocking(QTabWidget):

    def __init__(self):
        super(TabWidgetWithUndocking, self).__init__()
        self.tabBar().installEventFilter(self)

    def eventFilter(self, object, event):
        if object == self.tabBar():
            if event.type() == QEvent.MouseButtonDblClick:
                pos = event.pos()
                tabIndex = object.tabAt(pos)
                title = self.tabText(tabIndex)
                widget = self.widget(tabIndex)
                self.removeTab(tabIndex)
                dockWidget = DockToTabWidget(title, parent=self)
                dockWidget.setFeatures(QDockWidget.AllDockWidgetFeatures)
                dockWidget.setWidget(widget)
                dockWidget.setFloating(True)
                dockWidget.move(self.mapToGlobal(pos))
                dockWidget.show()
                return True
            return False

    def tabClose(self, index):
        self.removeTab(index)

qApp = QApplication([])
qApp.setQuitOnLastWindowClosed(True)
sys.excepthook = sys.__excepthook__
main = QMainWindow()
main.setWindowTitle('Main')
twu = TabWidgetWithUndocking()
for i in range(2):
    twu.addTab(QLabel('tab %i' % i), 'tab %i' % i)
main.setCentralWidget(twu)
main.show()
qApp.exec_()
user3650925
  • 173
  • 1
  • 10
  • I don't understand. You're referring to a double click on the QDockWidget (btw, it's with a "k", "doc**k**"), but your event filter is installed on the tab widget. Also, the parent of the dock widget should probably be the main window, not the tab widget. My impression is that it probably worked due to a combination of aspects that "allowed" it to work, but the implementation was conceptually wrong in origin. – musicamante Sep 16 '22 at 22:16
  • @musicamante. Thank you, I corrected the k. Note that I don't use a QMainWindow here, but mimick the usual undocking-docking behavior in a QMainWindow, in a QTabWidget. At each undocking: 1) I create a new QDockWidget 2) I port the content in the tab to this QDockWidget 3) I delete the tab. In the same way re-docking consists in adding a new tab with the QDockWidget content inside and deleting the QDockWidget. My question is about the double click on the titlebar of the QDockWidget triggering or not toplevelchanged. – user3650925 Sep 16 '22 at 22:42
  • 1
    @user3650925 You have a history of never selecting selecting an answer to your past questions... While it isn't a requirement to mark your questions as answered it is encouraged, and having a pattern of not doing so will discourage people from wanting to help you in the future. – Alexander Sep 16 '22 at 22:56
  • OK, I'll do it now. – user3650925 Sep 16 '22 at 23:14

1 Answers1

1

I don't remember the implementation of Qt4, but in Qt5 the double click always check that the dock widget was actually set on a valid QMainWindow parent before trying to toggle its top level state.

You're not adding the QDockWidget, nor you're using a QMainWindow, so the signal is never emitted. This also makes it problematic in some cases as it prevent proper handling of mouse events to allow dragging of the dock widget.

The only solution is to properly check for double clicks, and that can only happen by overriding event(), since it's internally managed by the dock widget in case a "title bar widget" is set, and mouseDoubleClickEvent will be never called.

The following edit of the original code should work fine also for PyQt4.

class DockToTabWidget(QDockWidget):
    attachRequested = pyqtSignal(QWidget, str)
    def __init__(self, title, widget):
        QDockWidget.__init__(self, title, widget.window())
        self.setFeatures(QDockWidget.AllDockWidgetFeatures)
        self.setWidget(widget)
        self.setFloating(True)

        floatButton = self.findChild(QAbstractButton, 'qt_dockwidget_floatbutton')
        floatButton.clicked.connect(self.attach)

    def attach(self):
        self.attachRequested.emit(self.widget(), self.windowTitle())
        self.deleteLater()

    def event(self, event):
        if (
            event.type() == event.MouseButtonDblClick 
            and event.button() == Qt.LeftButton
        ):
            opt = QStyleOptionDockWidget()
            self.initStyleOption(opt)
            if event.pos() in opt.rect:
                self.attach()
                return True
        return super().event(event)


class TabWidgetWithUndocking(QTabWidget):
    def __init__(self):
        super(TabWidgetWithUndocking, self).__init__()
        self.tabBar().installEventFilter(self)

    def eventFilter(self, obj, event):
        if obj == self.tabBar():
            if event.type() == QEvent.MouseButtonDblClick:
                pos = event.pos()
                tabIndex = obj.tabAt(pos)
                title = self.tabText(tabIndex)
                widget = self.widget(tabIndex)
                self.removeTab(tabIndex)
                dockWidget = DockToTabWidget(title, widget)
                dockWidget.attachRequested.connect(self.attachDock)
                dockWidget.move(self.mapToGlobal(pos))
                dockWidget.show()
                return True
            return False

    def attachDock(self, widget, title):
        self.setCurrentIndex(self.addTab(widget, title))

Note: object is a built-in type in Python: while not forbidden, you should not use it as a variable name.

musicamante
  • 41,230
  • 6
  • 33
  • 58
  • Wonderful, thank you very much! I am also interested by how you get and learn this type of information. Understanding all this with the standard Qt doc seems impossible to me. What is your source of information? – user3650925 Sep 17 '22 at 08:24
  • @user3650925 You're welcome! I'd say it's an average mix of documentation, experience and source code studying. For the code, you can use the [official repositories](https://code.qt.io/cgit/qt/qtbase.git/) or use a code browser [like this](https://codebrowser.dev/qt5/qtbase/src/) – musicamante Sep 17 '22 at 17:12