Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extending splitter widget to behave like SublimeText3 #20

Open
brupelo opened this issue Jun 4, 2018 · 0 comments
Open

Extending splitter widget to behave like SublimeText3 #20

brupelo opened this issue Jun 4, 2018 · 0 comments

Comments

@brupelo
Copy link

brupelo commented Jun 4, 2018

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant