Esempio n. 1
0
class HeaderWithButton(QHeaderView):
    """Class that reimplements the QHeaderView section paint event to draw a button
    that is used to display and change the type of that column or row.
    """
    def __init__(self, orientation, parent=None):
        super(HeaderWithButton, self).__init__(orientation, parent)
        self.setHighlightSections(True)
        self.setSectionsClickable(True)
        self.setDefaultAlignment(Qt.AlignLeft)
        self.sectionResized.connect(self._section_resize)
        self.sectionMoved.connect(self._section_move)
        self._font = QFont('Font Awesome 5 Free Solid')

        self._display_all = True
        self._display_sections = []

        self._margin = Margin(left=0, right=0, top=0, bottom=0)

        self._menu = self._create_menu()

        self._button = QToolButton(parent=self)
        self._button.setMenu(self._menu)
        self._button.setPopupMode(QToolButton.InstantPopup)
        self._button.setFont(self._font)
        self._button.hide()

        self._render_button = QToolButton(parent=self)
        self._render_button.setFont(self._font)
        self._render_button.hide()

        self._button_logical_index = None
        self.setMinimumSectionSize(self.minimumSectionSize() +
                                   self.widget_width())

    @property
    def display_all(self):
        return self._display_all

    @display_all.setter
    def display_all(self, display_all):
        self._display_all = display_all
        self.viewport().update()

    @property
    def sections_with_buttons(self):
        return self._display_sections

    @sections_with_buttons.setter
    def sections_with_buttons(self, sections):
        self._display_sections = set(sections)
        self.viewport().update()

    def _create_menu(self):
        menu = QMenu(self)
        for at in _ALLOWED_TYPES:
            action = QAction(parent=menu)
            action.setText(at)
            menu.addAction(action)
        menu.triggered.connect(self._menu_pressed)
        return menu

    def _menu_pressed(self, action):
        type_str = action.text()
        if type_str == "integer sequence datetime":
            dialog = NewIntegerSequenceDateTimeConvertSpecDialog()
            if dialog.exec_():
                convert_spec = dialog.get_spec()
            else:
                return
        else:
            convert_spec = value_to_convert_spec(type_str)

        logical_index = self._button_logical_index
        self.model().set_type(logical_index, convert_spec, self.orientation())

    def widget_width(self):
        """Width of widget

        Returns:
            [int] -- Width of widget
        """
        if self.orientation() == Qt.Horizontal:
            return self.height()
        return self.sectionSize(0)

    def widget_height(self):
        """Height of widget

        Returns:
            [int] -- Height of widget
        """
        if self.orientation() == Qt.Horizontal:
            return self.height()
        return self.sectionSize(0)

    def mouseMoveEvent(self, mouse_event):
        """Moves the button to the correct section so that interacting with the button works.
        """
        log_index = self.logicalIndexAt(mouse_event.x(), mouse_event.y())
        if not self._display_all and log_index not in self._display_sections:
            self._button_logical_index = None
            self._button.hide()
            super().mouseMoveEvent(mouse_event)
            return

        if self._button_logical_index != log_index:
            self._button_logical_index = log_index
            self._set_button_geometry(self._button, log_index)
            self._button.show()
        super().mouseMoveEvent(mouse_event)

    def mousePressEvent(self, mouse_event):
        """Move the button to the pressed location and show or hide it if button should not be shown.
        """
        log_index = self.logicalIndexAt(mouse_event.x(), mouse_event.y())
        if not self._display_all and log_index not in self._display_sections:
            self._button_logical_index = None
            self._button.hide()
            super().mousePressEvent(mouse_event)
            return

        if self._button_logical_index != log_index:
            self._button_logical_index = log_index
            self._set_button_geometry(self._button, log_index)
            self._button.show()
        super().mousePressEvent(mouse_event)

    def leaveEvent(self, event):
        """Hide button
        """
        self._button_logical_index = None
        self._button.hide()
        super().leaveEvent(event)

    def _set_button_geometry(self, button, index):
        """Sets a buttons geometry depending on the index.

        Arguments:
            button {QWidget} -- QWidget that geometry should be set
            index {int} -- logical_index to set position and geometry to.
        """
        margin = self._margin
        if self.orientation() == Qt.Horizontal:
            button.setGeometry(
                self.sectionViewportPosition(index) + margin.left,
                margin.top,
                self.widget_width() - self._margin.left - self._margin.right,
                self.widget_height() - margin.top - margin.bottom,
            )
        else:
            button.setGeometry(
                margin.left,
                self.sectionViewportPosition(index) + margin.top,
                self.widget_width() - self._margin.left - self._margin.right,
                self.widget_height() - margin.top - margin.bottom,
            )

    def _section_resize(self, i):
        """When a section is resized.

        Arguments:
            i {int} -- logical index to section being resized
        """
        self._button.hide()
        if i == self._button_logical_index:
            self._set_button_geometry(self._button, self._button_logical_index)

    def paintSection(self, painter, rect, logical_index):
        """Paints a section of the QHeader view.

        Works by drawing a pixmap of the button to the left of the orignial paint rectangle.
        Then shifts the original rect to the right so these two doesn't paint over eachother.
        """
        if not self._display_all and logical_index not in self._display_sections:
            super().paintSection(painter, rect, logical_index)
            return

        # get the type of the section.
        type_spec = self.model().get_type(logical_index, self.orientation())
        if type_spec is None:
            type_spec = "string"
        else:
            type_spec = type_spec.DISPLAY_NAME
        font_str = _TYPE_TO_FONT_AWESOME_ICON[type_spec]

        # set data for both interaction button and render button.
        self._button.setText(font_str)
        self._render_button.setText(font_str)
        self._set_button_geometry(self._render_button, logical_index)

        # get pixmap from render button and draw into header section.
        rw = self._render_button.grab()
        if self.orientation() == Qt.Horizontal:
            painter.drawPixmap(self.sectionViewportPosition(logical_index), 0,
                               rw)
        else:
            painter.drawPixmap(0, self.sectionViewportPosition(logical_index),
                               rw)

        # shift rect that super class should paint in to the right so it doesn't
        # paint over the button
        rect.adjust(self.widget_width(), 0, 0, 0)
        super().paintSection(painter, rect, logical_index)

    def sectionSizeFromContents(self, logical_index):
        """Add the button width to the section so it displays right.

        Arguments:
            logical_index {int} -- logical index of section

        Returns:
            [QSize] -- Size of section
        """
        org_size = super().sectionSizeFromContents(logical_index)
        org_size.setWidth(org_size.width() + self.widget_width())
        return org_size

    def _section_move(self, logical, old_visual_index, new_visual_index):
        """Section beeing moved.

        Arguments:
            logical {int} -- logical index of section beeing moved.
            old_visual_index {int} -- old visual index of section
            new_visual_index {int} -- new visual index of section
        """
        self._button.hide()
        if self._button_logical_index is not None:
            self._set_button_geometry(self._button, self._button_logical_index)

    def fix_widget_positions(self):
        """Update position of interaction button
        """
        if self._button_logical_index is not None:
            self._set_button_geometry(self._button, self._button_logical_index)

    def set_margins(self, margins):
        self._margin = margins
class TabBarPlus(QTabBar):
    """Tab bar that has a plus button floating to the right of the tabs."""

    plus_clicked = Signal()

    def __init__(self, parent):
        """
        Args:
            parent (MultiSpineDBEditor)
        """
        super().__init__(parent)
        self._parent = parent
        self._plus_button = QToolButton(self)
        self._plus_button.setIcon(QIcon(CharIconEngine("\uf067")))
        self._plus_button.clicked.connect(
            lambda _=False: self.plus_clicked.emit())
        self._move_plus_button()
        self.setShape(QTabBar.RoundedNorth)
        self.setTabsClosable(True)
        self.setMovable(True)
        self.setElideMode(Qt.ElideLeft)
        self.drag_index = None
        self._tab_hot_spot_x = None
        self._hot_spot_y = None

    def resizeEvent(self, event):
        """Sets the dimension of the plus button. Also, makes the tab bar as wide as the parent."""
        super().resizeEvent(event)
        self.setFixedWidth(self.parent().width())
        self.setMinimumHeight(self.height())
        self._move_plus_button()
        extent = max(0, self.height() - 2)
        self._plus_button.setFixedSize(extent, extent)
        self.setExpanding(False)

    def tabLayoutChange(self):
        super().tabLayoutChange()
        self._move_plus_button()

    def _move_plus_button(self):
        """Places the plus button at the right of the last tab."""
        left = sum([self.tabRect(i).width() for i in range(self.count())])
        top = self.geometry().top() + 1
        self._plus_button.move(left, top)

    def mousePressEvent(self, event):
        """Registers the position of the press, in case we need to detach the tab."""
        super().mousePressEvent(event)
        tab_rect = self.tabRect(self.tabAt(event.pos()))
        self._tab_hot_spot_x = event.pos().x() - tab_rect.x()
        self._hot_spot_y = event.pos().y() - tab_rect.y()

    def mouseMoveEvent(self, event):
        """Detaches a tab either if the user moves beyond the limits of the tab bar, or if it's the only one."""
        self._plus_button.hide()
        if self.count() == 1:
            self._send_release_event(event.pos())
            hot_spot = QPoint(event.pos().x(), self._hot_spot_y)
            self._parent.start_drag(hot_spot)
            return
        if self.count() > 1 and not self.geometry().contains(event.pos()):
            self._send_release_event(event.pos())
            hot_spot_x = event.pos().x()
            hot_spot = QPoint(event.pos().x(), self._hot_spot_y)
            index = self.tabAt(hot_spot)
            if index == -1:
                index = self.count() - 1
            self._parent.detach(index, hot_spot,
                                hot_spot_x - self._tab_hot_spot_x)
            return
        super().mouseMoveEvent(event)

    def _send_release_event(self, pos):
        """Sends a mouse release event at given position in local coordinates. Called just before detaching a tab.

        Args:
            pos (QPoint)
        """
        self.drag_index = None
        release_event = QMouseEvent(QEvent.MouseButtonRelease, pos,
                                    Qt.LeftButton, Qt.LeftButton,
                                    Qt.NoModifier)
        QApplication.sendEvent(self, release_event)

    def mouseReleaseEvent(self, event):
        super().mouseReleaseEvent(event)
        self._plus_button.show()
        self.update()
        self.releaseMouse()
        if self.drag_index is not None:
            # Pass it to parent
            event.ignore()

    def start_dragging(self, index):
        """Stars dragging the given index. This happens when a detached tab is reattached to this bar.

        Args:
            index (int)
        """
        self.drag_index = index
        press_pos = self.tabRect(self.drag_index).center()
        press_event = QMouseEvent(QEvent.MouseButtonPress, press_pos,
                                  Qt.LeftButton, Qt.LeftButton, Qt.NoModifier)
        QApplication.sendEvent(self, press_event)
        QApplication.processEvents()
        move_pos = self.mapFromGlobal(QCursor.pos())
        if self.geometry().contains(move_pos):
            move_event = QMouseEvent(QEvent.MouseMove, move_pos, Qt.LeftButton,
                                     Qt.LeftButton, Qt.NoModifier)
            QApplication.sendEvent(self, move_event)
        self.grabMouse()

    def index_under_mouse(self):
        """Returns the index under the mouse cursor, or None if the cursor isn't over the tab bar.
        Used to check for drop targets.

        Returns:
            int or NoneType
        """
        pos = self.mapFromGlobal(QCursor.pos())
        if not self.geometry().contains(pos):
            return None
        index = self.tabAt(pos)
        if index == -1:
            index = self.count()
        return index

    def _show_plus_button_context_menu(self, global_pos):
        toolbox = self._parent.db_mngr.parent()
        if toolbox is None:
            return
        ds_urls = {
            ds.name: ds.project_item.sql_alchemy_url()
            for ds in toolbox.project_item_model.items("Data Stores")
        }
        if not ds_urls:
            return
        menu = QMenu(self)
        for name, url in ds_urls.items():
            action = menu.addAction(name,
                                    lambda name=name, url=url: self._parent.
                                    add_new_tab({url: name}))
            action.setEnabled(bool(url))
        menu.popup(global_pos)
        menu.aboutToHide.connect(menu.deleteLater)

    def contextMenuEvent(self, event):
        index = self.tabAt(event.pos())
        if self._plus_button.underMouse():
            self._show_plus_button_context_menu(event.globalPos())
            return
        if self.tabButton(index, QTabBar.RightSide).underMouse():
            return
        db_editor = self._parent.tab_widget.widget(index)
        if db_editor is None:
            return
        menu = QMenu(self)
        others = self._parent.others()
        if others:
            move_tab_menu = menu.addMenu("Move tab to another window")
            move_tab_to_new_window = move_tab_menu.addAction(
                "New window",
                lambda _=False, index=index: self._parent.move_tab(
                    index, None))
            for other in others:
                move_tab_menu.addAction(
                    other.name(),
                    lambda _=False, index=index, other=other: self._parent.
                    move_tab(index, other))
        else:
            move_tab_to_new_window = menu.addAction(
                "Move tab to new window",
                lambda _=False, index=index: self._parent.move_tab(
                    index, None))
        move_tab_to_new_window.setEnabled(self.count() > 1)
        menu.addSeparator()
        menu.addAction(db_editor.url_toolbar.reload_action)
        db_url_codenames = db_editor.db_url_codenames
        menu.addAction(
            QIcon(CharIconEngine("\uf24d")),
            "Duplicate",
            lambda _=False, index=index + 1, db_url_codenames=db_url_codenames:
            self._parent.insert_new_tab(index, db_url_codenames),
        )
        menu.popup(event.globalPos())
        menu.aboutToHide.connect(menu.deleteLater)
        event.accept()