hydrargyrum / eye

Edit Your Editor: a scriptable Qt-based text editor - mirror of https://gitlab.com/hydrargyrum/eye
Do What The F*ck You Want To Public License
15 stars 1 forks source link

Extending splitter widget to behave like SublimeText3 #20

Open brupelo opened 6 years ago

brupelo commented 6 years ago

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