eye
eye copied to clipboard
Extending splitter widget to behave like SublimeText3
Some basic notation first:
- Splitter: It contains 1 or more slots
- Slot: It contains 0 or more widgets
- 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.
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