Search code examples
drag-and-droppyqtpyqt4pysideqtabwidget

Drag and drop tab from QTabWidget


I am trying to drag a tab from a tab widget and split the viewing area (- like eclipse does then you drag a tab heading into the edit area - and similar to the 'split vertically' functionality on a tab in PyCharm).

I tried using drag and drop but can't get the tab main area (eg text area) to register a drag coming from the QTabBar. I then tried just following the mouse movements and got further but the splitter code is not quite working and it's pretty ugly code.

NB I'm not using dockable widgets as this code will be used for the central widget of an app. I'd prefer to use drag and drop to do this - anyone have any ideas?

from PyQt4.QtGui import QWidget, QDrag, QTabBar, QTabWidget, QPainter, QPalette,\
QBrush, QColor, QPen, QVBoxLayout, QHBoxLayout, QTextEdit, QCheckBox
from PyQt4.QtCore import QByteArray, QMimeData, QPoint
from PyQt4 import QtCore, QtGui

class CentralTabWidget(QTabWidget):

    def __init__(self, parent=None):
        QTabWidget.__init__(self, parent)
        self.parent = parent

        tabBar = CentralTabBar(self)
        self.setTabBar(tabBar)
        self.addWidgets()

    def addWidgets(self):
        tab1 = QWidget()
        self.addTab(tab1, "Tab1")
        textArea = QTextEdit()
        textArea.setText("Text area 1")
        vBox = QVBoxLayout()
        vBox.addWidget(textArea)
        tab1.setLayout(vBox)
        tab2 = QWidget()
        self.addTab(tab2, "Tab2")
        textArea2 = QTextEdit()
        textArea2.setText("Text area 2")
        vBox2 = QVBoxLayout()
        vBox2.addWidget(textArea2)
        tab2.setLayout(vBox2)
        self.setAcceptDrops(True)
        self.verticalLineOverlay = Overlay(parent = self)
        self.verticalLineOverlay.hide()

    def dragEnterEvent(self, event):
        mimeData = event.mimeData()
        event.accept()

    def dragMoveEvent(self, event):
        print(">>dragMoveEvent()")

    def dropEvent(self, event):
        mimeData = event.mimeData()
        event.setDropAction(QtCore.Qt.MoveAction)
        event.accept()

    def resizeEvent(self, event):    
        self.verticalLineOverlay.resize(event.size())
        event.accept()

class CentralTabBar(QTabBar):   
    def __init__(self, parent=None):
        QTabBar.__init__(self, parent)
        self.parent = parent
        self.__drag_start_pos = QPoint()
        self.setAcceptDrops(True)

    def mousePressEvent(self, event):
        self.__mousePressPos = None
        self.__mouseMovePos = None
        if event.button() == QtCore.Qt.LeftButton:
            self.__mousePressPos = event.globalPos()
            self.__mouseMovePos = event.globalPos()
            if self.parent.parent.dndCheckBox.isChecked():
                self.startDrag()
        super(CentralTabBar, self).mousePressEvent(event)

    def mouseReleaseEvent(self, event):
        self.parent.verticalLineOverlay.setVisible(False)
        currPos = self.mapToGlobal(self.pos())
        ax, ay, aw, ah = self.geometry().getRect()
        if currPos.x() > ax + aw/4:
            self.parent.parent.createSplitter()
        super(CentralTabBar, self).mouseReleaseEvent(event)

    def mouseMoveEvent(self, event):
        if event.buttons() == QtCore.Qt.LeftButton:
            currPos = self.mapToGlobal(self.pos())
            globalPos = event.globalPos()
            diff = globalPos - self.__mouseMovePos
            newPos = self.mapFromGlobal(currPos + diff)
            xp1, yp1, xp2, yp2 = self.geometry().getCoords()
            ax, ay, aw, ah = self.geometry().getRect()
            parentx, parenty, parentw, parenth = self.parent.geometry().getCoords()
            if parenth > 10:
                if (newPos.y() > yp2) and (newPos.y() < (parenth)):
                    self.parent.verticalLineOverlay.setVisible(True)
        super(CentralTabBar, self).mouseMoveEvent(event)

    def startDrag(self):
        data = QByteArray()
        mimeData = QMimeData()
        mimeData.setData("application/x-icon-and-text", data)
        print("Using DnD")
        drag = QDrag(self)
        drag.setMimeData(mimeData)
        drag.exec_()

    def dragEnterEvent(self, event):
        event.accept()

    def dragMoveEvent(self, event):
        print("--dragMoveEvent()")

    def dropEvent(self, event):
        event.setDropAction(QtCore.Qt.MoveAction)
        event.accept()

class Overlay(QWidget):    
    def __init__(self, parent=None):        
        super(Overlay, self).__init__(parent)
        print("--__init__() parent type:{0}".format(type(parent))) 
        self.parent = parent
        palette = QPalette(self.palette())
        palette.setColor(palette.Background, QtCore.Qt.transparent)
        self.setPalette(palette)

    def paintEvent(self, event):  
        self.painter = QPainter()
        self.painter.setPen(QPen(QtCore.Qt.NoPen))
        self.painter.begin(self)
        self.painter.setRenderHint(QPainter.Antialiasing)
        self.painter.fillRect(self.parent.currentWidget().geometry(), QBrush(QColor(255, 255, 255, 10)))
        parentx, parenty, parentw, parenth = self.parent.geometry().getCoords()
        self.painter.drawRect( parentx, parenty, (parentw/2)-10, parenth)   
        self.painter.drawRect( (parentw/2)+10, parenty, parentw, parenth)        
        self.painter.setPen(QPen(QtCore.Qt.NoPen)) 

    def setUpPainter(self):
        self.painter = QPainter()
        self.painter.setPen(QPen(QtCore.Qt.NoPen))
        self.painter.begin(self)
        self.painter.setRenderHint(QPainter.Antialiasing)
        self.painter.fillRect(self.parent.currentWidget().geometry(), QBrush(QColor(255, 255, 255, 10)))
        parentx, parenty, parentw, parenth = self.parent.geometry().getCoords()

class Window(QtGui.QWidget):
    def __init__(self):
        QtGui.QWidget.__init__(self)
        self.layout = QHBoxLayout()
        self.dndCheckBox = QCheckBox("Use DnD")
        self.dndCheckBox.setChecked(True)
        self.ctw = CentralTabWidget(self)
        self.layout.addWidget(self.ctw)
        self.layout.addWidget(self.dndCheckBox)
        self.setLayout(self.layout)

    def createSplitter(self):
        #not sure why widgets are not redrawn inside the splitter
        splitter1 = QtGui.QSplitter(QtCore.Qt.Horizontal)
        self.removeWidgets(self.layout)
        splitter1.addWidget(self.ctw)
        self.layout.addWidget(splitter1)
        self.layout.addWidget(self.dndCheckBox)

    def removeWidgets(self, layout):
        for cnt in reversed(range(layout.count())):
            widget = layout.takeAt(cnt).widget()
            if widget is not None: 
                widget.deleteLater()

if __name__ == '__main__':
    import sys
    app = QtGui.QApplication(sys.argv)
    window = Window()
    window.resize(350, 300)
    window.show()
    sys.exit(app.exec_())

Using a QMainWindow inside a QMainMaindow I tried:

from PyQt4 import QtCore, QtGui
from PyQt4.QtGui import QMainWindow, QTextEdit, QDockWidget

_DOCK_OPTS = QtGui.QMainWindow.AllowNestedDocks
_DOCK_OPTS |= QtGui.QMainWindow.AllowTabbedDocks

class Window(QMainWindow):
    def __init__(self):
        QtGui.QMainWindow.__init__(self)
        secondQMainWindow = QMainWindow()
        self.central = secondQMainWindow
        self.setDockOptions(_DOCK_OPTS)
        dw1 = QDockWidget("One")
        textArea = QTextEdit()
        textArea.setText("Text area 1")
        dw1.setWidget(textArea)

        dw2 = QDockWidget("Two")
        textArea2 = QTextEdit()
        textArea2.setText("Text area 2")
        dw2.setWidget(textArea2)
        self.addDockWidget(QtCore.Qt.LeftDockWidgetArea, dw1)
        self.addDockWidget(QtCore.Qt.RightDockWidgetArea, dw2)
        self.tabifyDockWidget(dw1, dw2)
        dw3 = QDockWidget("Three")
        textArea3 = QTextEdit()
        textArea3.setText("Text area 3")
        dw3.setWidget(textArea3)
        self.addDockWidget(QtCore.Qt.LeftDockWidgetArea, dw3)

if __name__ == '__main__':
    import sys
    app = QtGui.QApplication(sys.argv)
    window = Window()
    window.show()
    app.exec_()

But I don't think I have the addition of widgets to the parent vs child central widget correct.


Solution

  • Here is a trick I've used in the past to do what you are looking for. It may be quite limited if you want to have a lot of control on the parameters but it does the job.

    1. Put a 2nd QMainWindow as the centralWidget of you QMainWindow.
    2. Don't set any centralWidget for this 2nd QMainWindow.
    3. Set flags : QMainWindow.AllowNestedDocks and QMainWindow.AllowTabbedDocks

    4. Add tabs by adding QDockWidgets. These will be automatically threated as QTabWidgets.

    5. Use QMainWindow.tabifyDockWidget(firstWidget,secondWidget) to stack the widgets programmatically (it can be used multiple times for more than 2 widgets)

    Example :

    window=QtGui.QMainWindow()
        window.centralContent=QtGui.QMainWindow()
    
    window.setCentralWidget(window.centralContent)
    
    window.centralContent.firstTabWidget=QtGui.QWidget()
    window.centralContent.firstTabDock=QtGui.QDockWidget("first")
    window.centralContent.firstTabDock.setWidget(window.centralContent.firstTabWidget)
    window.centralContent.addDockWidget(window.centralContent.firstTabDock)
     window.centralContent.secondTabWidget=QtGui.QWidget()
    window.centralContent.secondTabDock=QtGui.QDockWidget("second")
    window.centralContent.secondTabDock.setWidget(window.centralContent.secondTabWidget)
    window.centralContent.addDockWidget(window.centralContent.secondTabDock)
    
    window.centralContent.tabifyDockWidget( window.centralContent.firstTabDock, window.centralContent.secondTabDock)