class DetachableTabBar(QTabBar): ''' The TabBar class re-implements some of the functionality of the QTabBar widget ''' detach_tab_signal = Signal(int, QPoint, bool) # tab at pos, mouse cursor position, by double click move_tab_signal = Signal(int, int) empty_signal = Signal() def __init__(self, parent=None): QTabBar.__init__(self, parent) self.setAcceptDrops(True) self.setElideMode(Qt.ElideRight) self.setSelectionBehaviorOnRemove(QTabBar.SelectLeftTab) self.drag_start_pos = QPoint() self.drag_droped_pos = QPoint() self.mouse_cursor = QCursor() self.drag_initiated = False def mouseDoubleClickEvent(self, event): ''' Send the detach_tab_signal when a tab is double clicked ''' event.accept() self.detach_tab_signal.emit(self.tabAt(event.pos()), self.mouse_cursor.pos(), True) def mousePressEvent(self, event): ''' Set the starting position for a drag event when the mouse button is pressed ''' self.drag_droped_pos = QPoint(0, 0) self.drag_initiated = False if event.button() == Qt.LeftButton: self.drag_start_pos = event.pos() QTabBar.mousePressEvent(self, event) def mouseMoveEvent(self, event): ''' Determine if the current movement is a drag. If it is, convert it into a QDrag. If the drag ends inside the tab bar, emit an move_tab_signal. If the drag ends outside the tab bar, emit an detach_tab_signal. ''' # Determine if the current movement is detected as a drag if not self.drag_start_pos.isNull() and ((event.pos() - self.drag_start_pos).manhattanLength() > QApplication.startDragDistance()): self.drag_initiated = True # If the current movement is a drag initiated by the left button if ((event.buttons() & Qt.LeftButton)) and self.drag_initiated: # Stop the move event finishMoveEvent = QMouseEvent(QEvent.MouseMove, event.pos(), Qt.NoButton, Qt.NoButton, Qt.NoModifier) QTabBar.mouseMoveEvent(self, finishMoveEvent) # Convert the move event into a drag drag = QDrag(self) mime_data = QMimeData() mime_data.setData('action', b'application/tab-detach') drag.setMimeData(mime_data) # Create the appearance of dragging the tab content # tab_index = self.tabAt(self.drag_start_pos) pixmap = self.parentWidget().grab() targetPixmap = QPixmap(pixmap.size()) targetPixmap.fill(Qt.transparent) painter = QPainter(targetPixmap) painter.setOpacity(0.85) painter.drawPixmap(0, 0, pixmap) painter.end() drag.setPixmap(targetPixmap) # Initiate the drag dropAction = drag.exec_(Qt.MoveAction | Qt.CopyAction) # If the drag completed outside of the tab bar, detach the tab and move # the content to the current cursor position if dropAction == Qt.IgnoreAction: event.accept() self.detach_tab_signal.emit(self.tabAt(self.drag_start_pos), self.mouse_cursor.pos(), False) elif dropAction == Qt.MoveAction: # else if the drag completed inside the tab bar, move the selected tab to the new position if not self.drag_droped_pos.isNull(): self.move_tab_signal.emit(self.tabAt(self.drag_start_pos), self.tabAt(self.drag_droped_pos)) else: # else if the drag completed inside the tab bar new TabBar, move the selected tab to the new TabBar self.detach_tab_signal.emit(self.tabAt(self.drag_start_pos), self.mouse_cursor.pos(), False) event.accept() else: QTabBar.mouseMoveEvent(self, event) def dragEnterEvent(self, event): ''' Determine if the drag has entered a tab position from another tab position ''' self.drag_droped_pos = QPoint(0, 0) mime_data = event.mimeData() formats = mime_data.formats() if 'action' in formats and mime_data.data('action') == 'application/tab-detach': event.acceptProposedAction() QTabBar.dragMoveEvent(self, event) def dropEvent(self, event): ''' Get the position of the end of the drag ''' self.drag_droped_pos = event.pos() mime_data = event.mimeData() formats = mime_data.formats() if 'action' in formats and mime_data.data('action') == 'application/tab-detach': event.acceptProposedAction() QTabBar.dropEvent(self, event) def prepared_for_drop(self): return not self.drag_droped_pos.isNull() def tabRemoved(self, index): QTabBar.tabRemoved(self, index) if self.count() == 0: self.empty_signal.emit()