Widgets: It can be the child of a slot and it can be attached/detached between different slots
Right now the splitter is a really usable widget and it allows the user to create dynamic layouts by adding/removing slots using 3 basic options (split horizontally, split vertically, resizing the slot), it's a really flexible widget and is UI friendly.
That said, SublimeText splitter has really cool features you could borrow to make the current widget even more flexible:
Create layouts, one layout is a certain state of the splitter. Example1, Example2.
Moving slots around by dragging them with the mouse or by using shortcuts. Example3
Once you're using the concept of layout, the editor can save/restore the state of a layout at startup.
I think these features boost up coding productivity as you can easily switch between layouts when you're coding multiple components and you've got a general view of all parts of a task. For instance, if you're coding a standalone widget 1 window is good enough, if you're coding a widget with 1 single dependency 1x1, code from different packages layout mxn.
Some basic notation first:
Right now the splitter is a really usable widget and it allows the user to create dynamic layouts by adding/removing slots using 3 basic options (split horizontally, split vertically, resizing the slot), it's a really flexible widget and is UI friendly.
That said, SublimeText splitter has really cool features you could borrow to make the current widget even more flexible:
I think these features boost up coding productivity as you can easily switch between layouts when you're coding multiple components and you've got a general view of all parts of a task. For instance, if you're coding a standalone widget 1 window is good enough, if you're coding a widget with 1 single dependency 1x1, code from different packages layout mxn.
Here's some code that could help to improve the current splitter, code based on https://stackoverflow.com/questions/47267195/in-pyqt4-is-it-possible-to-detach-tabs-from-a-qtabwidget:
DetachableTab.py
from PyQt5.Qt import * # noqa class TabBar(QTabBar): tab_detached = pyqtSignal(int, QPoint) tab_moved = pyqtSignal(int, int) tab_droped = pyqtSignal(str, int, QPoint) def __init__(self, parent=None): super().__init__(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): event.accept() self.tab_detached.emit(self.tabAt( event.pos()), self.mouse_cursor.pos()) def mousePressEvent(self, event): if event.button() == Qt.LeftButton: self.drag_start_pos = event.pos() self.drag_droped_pos.setX(0) self.drag_droped_pos.setY(0) self.drag_initiated = False QTabBar.mousePressEvent(self, event) def mouseMoveEvent(self, event): if not self.drag_start_pos.isNull() and ((event.pos() - self.drag_start_pos).manhattanLength() < QApplication.startDragDistance()): self.drag_initiated = True if (((event.buttons() & Qt.LeftButton)) and self.drag_initiated): finishMoveEvent = QMouseEvent(QEvent.MouseMove, event.pos( ), Qt.NoButton, Qt.NoButton, Qt.NoModifier) QTabBar.mouseMoveEvent(self, finishMoveEvent) drag = QDrag(self) md = QMimeData() md.setData('action', QByteArray().append('application/tab-detach')) drag.setMimeData(md) pixmap = self.parentWidget().currentWidget().grab() target_pixmap = QPixmap(pixmap.size()) target_pixmap.fill(Qt.transparent) painter = QPainter(target_pixmap) painter.setOpacity(0.85) painter.drawPixmap(0, 0, pixmap) painter.end() drag.setPixmap(target_pixmap) drop_action = drag.exec_( Qt.MoveAction | Qt.CopyAction) if self.drag_droped_pos.x() != 0 and self.drag_droped_pos.y() != 0: drop_action = Qt.MoveAction if drop_action == Qt.IgnoreAction: event.accept() self.tab_detached.emit(self.tabAt( self.drag_start_pos), self.mouse_cursor.pos()) elif drop_action == Qt.MoveAction: if not self.drag_droped_pos.isNull(): event.accept() self.tab_moved.emit(self.tabAt( self.drag_start_pos), self.tabAt(self.drag_droped_pos)) else: QTabBar.mouseMoveEvent(self, event) def dragEnterEvent(self, event): md = event.mimeData() md_str = str(md.data('action'), encoding='utf-8') formats = md.formats() if 'action' in formats and md_str == 'application/tab-detach': event.acceptProposedAction() QTabBar.dragMoveEvent(self, event) def dropEvent(self, event): self.drag_droped_pos = event.pos() QTabBar.dropEvent(self, event) def detached_tab_drop(self, name, drop_pos): tab_drop_pos = self.mapFromGlobal(drop_pos) index = self.tabAt(tab_drop_pos) self.tab_droped.emit(name, index, drop_pos) class WindowDropFilter(QObject): signal_droped = pyqtSignal(QPoint) def __init__(self): QObject.__init__(self) self.last_event = None def eventFilter(self, obj, event): if self.last_event == QEvent.Move and event.type() == 173: mouse_cursor = QCursor() drop_pos = mouse_cursor.pos() self.signal_droped.emit(drop_pos) self.last_event = event.type() return True else: self.last_event = event.type() return False class DetachedTab(QMainWindow): signal_closed = pyqtSignal(QWidget, str, QIcon) signal_droped = pyqtSignal(str, QPoint) def __init__(self, name, content_widget): super().__init__() self.setObjectName(name) self.setWindowTitle(name) self.content_widget = content_widget self.setCentralWidget(self.content_widget) self.content_widget.show() self.window_drop_filter = WindowDropFilter() self.installEventFilter(self.window_drop_filter) self.window_drop_filter.signal_droped.connect(self.on_signal_droped) def on_signal_droped(self, drop_pos): self.signal_droped.emit(self.objectName(), drop_pos) def closeEvent(self, event): self.signal_closed.emit( self.content_widget, self.objectName(), self.windowIcon()) class DetachableTabWidget(QTabWidget): def __init__(self, parent=None): super().__init__(parent) self.tab_bar = TabBar(self) self.tab_bar.tab_detached.connect(self.detachTab) self.tab_bar.tab_moved.connect(self.moveTab) self.tab_bar.tab_droped.connect(self.detached_tab_drop) self.setTabBar(self.tab_bar) self.detached_tabs = {} qApp.aboutToQuit.connect(self.close_detached_tabs) def setMovable(self, movable): pass def moveTab(self, fromIndex, toIndex): widget = self.widget(fromIndex) icon = self.tabIcon(fromIndex) text = self.tabText(fromIndex) self.removeTab(fromIndex) self.insertTab(toIndex, widget, icon, text) self.setCurrentIndex(toIndex) def detachTab(self, index, point): name = self.tabText(index) icon = self.tabIcon(index) if icon.isNull(): icon = self.window().windowIcon() content_widget = self.widget(index) try: content_widget_rect = content_widget.frameGeometry() except AttributeError: return detached_tab = DetachedTab(name, content_widget) detached_tab.setWindowModality(Qt.NonModal) detached_tab.setWindowIcon(icon) detached_tab.setGeometry(content_widget_rect) detached_tab.signal_closed.connect(self.attachTab) detached_tab.signal_droped.connect(self.tab_bar.detached_tab_drop) detached_tab.move(point) detached_tab.show() self.detached_tabs[name] = detached_tab def attachTab(self, content_widget, name, icon, insert_at=None): content_widget.setParent(self) del self.detached_tabs[name] if not icon.isNull(): try: tab_icon_pixmap = icon.pixmap(icon.availableSizes()[0]) tab_icon_image = tab_icon_pixmap.toImage() except IndexError: tab_icon_image = None else: tab_icon_image = None if not icon.isNull(): try: window_icon_pixmap = self.window().windowIcon().pixmap( icon.availableSizes()[0]) window_icon_image = window_icon_pixmap.toImage() except IndexError: window_icon_image = None else: window_icon_image = None if tab_icon_image == window_icon_image: if insert_at == None: index = self.addTab(content_widget, name) else: index = self.insertTab(insert_at, content_widget, name) else: if insert_at == None: index = self.addTab(content_widget, icon, name) else: index = self.insertTab(insert_at, content_widget, icon, name) if index > -1: self.setCurrentIndex(index) def remove_tab_by_name(self, name): attached = False for index in xrange(self.count()): if str(name) == str(self.tabText(index)): self.removeTab(index) attached = True break if not attached: for key in self.detached_tabs: if str(name) == str(key): self.detached_tabs[key].signal_closed.disconnect() self.detached_tabs[key].close() del self.detached_tabs[key] break def detached_tab_drop(self, name, index, drop_pos): if index > -1: content_widget = self.detached_tabs[name].content_widget icon = self.detached_tabs[name].windowIcon() self.detached_tabs[name].signal_closed.disconnect() self.detached_tabs[name].close() self.attachTab(content_widget, name, icon, index) else: tab_drop_pos = self.mapFromGlobal(drop_pos) if self.rect().contains(tab_drop_pos): if tab_drop_pos.y() < self.tab_bar.height() or self.count() == 0: self.detached_tabs[name].close() def close_detached_tabs(self): listOfDetachedTabs = [] for key in self.detached_tabs: listOfDetachedTabs.append(self.detached_tabs[key]) for detached_tab in listOfDetachedTabs: detached_tab.close() if __name__ == '__main__': import sys app = QApplication(sys.argv) mainWindow = QMainWindow() tabWidget = DetachableTabWidget() tab1 = QLabel('Test Widget 1') tabWidget.addTab(tab1, 'Tab1') tab2 = QLabel('Test Widget 2') tabWidget.addTab(tab2, 'Tab2') tab3 = QLabel('Test Widget 3') tabWidget.addTab(tab3, 'Tab3') tabWidget.show() mainWindow.setCentralWidget(tabWidget) mainWindow.show() try: exitStatus = app.exec_() sys.exit(exitStatus) except: pass