Exemple #1
0
class WorkspaceStatus(BaseWorkspace):
    def setupWorkspace(self):
        self.pile = QStackedWidget(self)
        self.wLayout.addWidget(self.pile)
        self.dlgs = weakref.WeakValueDictionary()

        self.pile.setSizePolicy(
            QSizePolicy.Expanding, QSizePolicy.Expanding)
        self.push(DefaultStatusWidget(), 'default')

    def push(self, widget, name):
        idx = self.pile.addWidget(widget)
        self.pile.setCurrentIndex(idx)
        self.dlgs[name] = widget
        return idx, widget

    def clear(self, name):
        if name in self.dlgs:
            w = self.dlgs[name]
            idx = self.pile.indexOf(w)
            if idx:
                self.pile.removeWidget(w)

    def pushProgress(self, name):
        return self.push(DefaultProgressDialog(), name)

    async def pushRunDialog(self, dialog, name):
        dialog.setEnabled(True)
        idx, w = self.push(dialog, name)
        await runDialogAsync(dialog)
Exemple #2
0
class E5SideBar(QWidget):
    """
    Class implementing a sidebar with a widget area, that is hidden or shown,
    if the current tab is clicked again.
    """
    Version = 1

    North = 0
    East = 1
    South = 2
    West = 3

    def __init__(self, orientation=None, delay=200, parent=None):
        """
        Constructor
        
        @param orientation orientation of the sidebar widget (North, East,
            South, West)
        @param delay value for the expand/shrink delay in milliseconds
            (integer)
        @param parent parent widget (QWidget)
        """
        super(E5SideBar, self).__init__(parent)

        self.__tabBar = QTabBar()
        self.__tabBar.setDrawBase(True)
        self.__tabBar.setShape(QTabBar.RoundedNorth)
        self.__tabBar.setUsesScrollButtons(True)
        self.__tabBar.setDrawBase(False)
        self.__stackedWidget = QStackedWidget(self)
        self.__stackedWidget.setContentsMargins(0, 0, 0, 0)
        self.__autoHideButton = QToolButton()
        self.__autoHideButton.setCheckable(True)
        self.__autoHideButton.setIcon(
            UI.PixmapCache.getIcon("autoHideOff.png"))
        self.__autoHideButton.setChecked(True)
        self.__autoHideButton.setToolTip(
            self.tr("Deselect to activate automatic collapsing"))
        self.barLayout = QBoxLayout(QBoxLayout.LeftToRight)
        self.barLayout.setContentsMargins(0, 0, 0, 0)
        self.layout = QBoxLayout(QBoxLayout.TopToBottom)
        self.layout.setContentsMargins(0, 0, 0, 0)
        self.layout.setSpacing(0)
        self.barLayout.addWidget(self.__autoHideButton)
        self.barLayout.addWidget(self.__tabBar)
        self.layout.addLayout(self.barLayout)
        self.layout.addWidget(self.__stackedWidget)
        self.setLayout(self.layout)

        # initialize the delay timer
        self.__actionMethod = None
        self.__delayTimer = QTimer(self)
        self.__delayTimer.setSingleShot(True)
        self.__delayTimer.setInterval(delay)
        self.__delayTimer.timeout.connect(self.__delayedAction)

        self.__minimized = False
        self.__minSize = 0
        self.__maxSize = 0
        self.__bigSize = QSize()

        self.splitter = None
        self.splitterSizes = []

        self.__hasFocus = False
        # flag storing if this widget or any child has the focus
        self.__autoHide = False

        self.__tabBar.installEventFilter(self)

        self.__orientation = E5SideBar.North
        if orientation is None:
            orientation = E5SideBar.North
        self.setOrientation(orientation)

        self.__tabBar.currentChanged[int].connect(
            self.__stackedWidget.setCurrentIndex)
        e5App().focusChanged.connect(self.__appFocusChanged)
        self.__autoHideButton.toggled[bool].connect(self.__autoHideToggled)

    def setSplitter(self, splitter):
        """
        Public method to set the splitter managing the sidebar.
        
        @param splitter reference to the splitter (QSplitter)
        """
        self.splitter = splitter
        self.splitter.splitterMoved.connect(self.__splitterMoved)
        self.splitter.setChildrenCollapsible(False)
        index = self.splitter.indexOf(self)
        self.splitter.setCollapsible(index, False)

    def __splitterMoved(self, pos, index):
        """
        Private slot to react on splitter moves.
        
        @param pos new position of the splitter handle (integer)
        @param index index of the splitter handle (integer)
        """
        if self.splitter:
            self.splitterSizes = self.splitter.sizes()

    def __delayedAction(self):
        """
        Private slot to handle the firing of the delay timer.
        """
        if self.__actionMethod is not None:
            self.__actionMethod()

    def setDelay(self, delay):
        """
        Public method to set the delay value for the expand/shrink delay in
        milliseconds.
        
        @param delay value for the expand/shrink delay in milliseconds
            (integer)
        """
        self.__delayTimer.setInterval(delay)

    def delay(self):
        """
        Public method to get the delay value for the expand/shrink delay in
        milliseconds.
        
        @return value for the expand/shrink delay in milliseconds (integer)
        """
        return self.__delayTimer.interval()

    def __cancelDelayTimer(self):
        """
        Private method to cancel the current delay timer.
        """
        self.__delayTimer.stop()
        self.__actionMethod = None

    def shrink(self):
        """
        Public method to record a shrink request.
        """
        self.__delayTimer.stop()
        self.__actionMethod = self.__shrinkIt
        self.__delayTimer.start()

    def __shrinkIt(self):
        """
        Private method to shrink the sidebar.
        """
        self.__minimized = True
        self.__bigSize = self.size()
        if self.__orientation in [E5SideBar.North, E5SideBar.South]:
            self.__minSize = self.minimumSizeHint().height()
            self.__maxSize = self.maximumHeight()
        else:
            self.__minSize = self.minimumSizeHint().width()
            self.__maxSize = self.maximumWidth()
        if self.splitter:
            self.splitterSizes = self.splitter.sizes()

        self.__stackedWidget.hide()

        if self.__orientation in [E5SideBar.North, E5SideBar.South]:
            self.setFixedHeight(self.__tabBar.minimumSizeHint().height())
        else:
            self.setFixedWidth(self.__tabBar.minimumSizeHint().width())

        self.__actionMethod = None

    def expand(self):
        """
        Public method to record a expand request.
        """
        self.__delayTimer.stop()
        self.__actionMethod = self.__expandIt
        self.__delayTimer.start()

    def __expandIt(self):
        """
        Private method to expand the sidebar.
        """
        self.__minimized = False
        self.__stackedWidget.show()
        self.resize(self.__bigSize)
        if self.__orientation in [E5SideBar.North, E5SideBar.South]:
            minSize = max(self.__minSize, self.minimumSizeHint().height())
            self.setMinimumHeight(minSize)
            self.setMaximumHeight(self.__maxSize)
        else:
            minSize = max(self.__minSize, self.minimumSizeHint().width())
            self.setMinimumWidth(minSize)
            self.setMaximumWidth(self.__maxSize)
        if self.splitter:
            self.splitter.setSizes(self.splitterSizes)

        self.__actionMethod = None

    def isMinimized(self):
        """
        Public method to check the minimized state.
        
        @return flag indicating the minimized state (boolean)
        """
        return self.__minimized

    def isAutoHiding(self):
        """
        Public method to check, if the auto hide function is active.
        
        @return flag indicating the state of auto hiding (boolean)
        """
        return self.__autoHide

    def eventFilter(self, obj, evt):
        """
        Public method to handle some events for the tabbar.
        
        @param obj reference to the object (QObject)
        @param evt reference to the event object (QEvent)
        @return flag indicating, if the event was handled (boolean)
        """
        if obj == self.__tabBar:
            if evt.type() == QEvent.MouseButtonPress:
                pos = evt.pos()
                for i in range(self.__tabBar.count()):
                    if self.__tabBar.tabRect(i).contains(pos):
                        break

                if i == self.__tabBar.currentIndex():
                    if self.isMinimized():
                        self.expand()
                    else:
                        self.shrink()
                    return True
                elif self.isMinimized():
                    self.expand()
            elif evt.type() == QEvent.Wheel:
                if qVersion() >= "5.0.0":
                    delta = evt.angleDelta().y()
                else:
                    delta = evt.delta()
                if delta > 0:
                    self.prevTab()
                else:
                    self.nextTab()
                return True

        return QWidget.eventFilter(self, obj, evt)

    def addTab(self, widget, iconOrLabel, label=None):
        """
        Public method to add a tab to the sidebar.
        
        @param widget reference to the widget to add (QWidget)
        @param iconOrLabel reference to the icon or the label text of the tab
            (QIcon, string)
        @param label the labeltext of the tab (string) (only to be
            used, if the second parameter is a QIcon)
        """
        if label:
            index = self.__tabBar.addTab(iconOrLabel, label)
            self.__tabBar.setTabToolTip(index, label)
        else:
            index = self.__tabBar.addTab(iconOrLabel)
            self.__tabBar.setTabToolTip(index, iconOrLabel)
        self.__stackedWidget.addWidget(widget)
        if self.__orientation in [E5SideBar.North, E5SideBar.South]:
            self.__minSize = self.minimumSizeHint().height()
        else:
            self.__minSize = self.minimumSizeHint().width()

    def insertTab(self, index, widget, iconOrLabel, label=None):
        """
        Public method to insert a tab into the sidebar.
        
        @param index the index to insert the tab at (integer)
        @param widget reference to the widget to insert (QWidget)
        @param iconOrLabel reference to the icon or the labeltext of the tab
            (QIcon, string)
        @param label the labeltext of the tab (string) (only to be
            used, if the second parameter is a QIcon)
        """
        if label:
            index = self.__tabBar.insertTab(index, iconOrLabel, label)
            self.__tabBar.setTabToolTip(index, label)
        else:
            index = self.__tabBar.insertTab(index, iconOrLabel)
            self.__tabBar.setTabToolTip(index, iconOrLabel)
        self.__stackedWidget.insertWidget(index, widget)
        if self.__orientation in [E5SideBar.North, E5SideBar.South]:
            self.__minSize = self.minimumSizeHint().height()
        else:
            self.__minSize = self.minimumSizeHint().width()

    def removeTab(self, index):
        """
        Public method to remove a tab.
        
        @param index the index of the tab to remove (integer)
        """
        self.__stackedWidget.removeWidget(self.__stackedWidget.widget(index))
        self.__tabBar.removeTab(index)
        if self.__orientation in [E5SideBar.North, E5SideBar.South]:
            self.__minSize = self.minimumSizeHint().height()
        else:
            self.__minSize = self.minimumSizeHint().width()

    def clear(self):
        """
        Public method to remove all tabs.
        """
        while self.count() > 0:
            self.removeTab(0)

    def prevTab(self):
        """
        Public slot used to show the previous tab.
        """
        ind = self.currentIndex() - 1
        if ind == -1:
            ind = self.count() - 1

        self.setCurrentIndex(ind)
        self.currentWidget().setFocus()

    def nextTab(self):
        """
        Public slot used to show the next tab.
        """
        ind = self.currentIndex() + 1
        if ind == self.count():
            ind = 0

        self.setCurrentIndex(ind)
        self.currentWidget().setFocus()

    def count(self):
        """
        Public method to get the number of tabs.
        
        @return number of tabs in the sidebar (integer)
        """
        return self.__tabBar.count()

    def currentIndex(self):
        """
        Public method to get the index of the current tab.
        
        @return index of the current tab (integer)
        """
        return self.__stackedWidget.currentIndex()

    def setCurrentIndex(self, index):
        """
        Public slot to set the current index.
        
        @param index the index to set as the current index (integer)
        """
        self.__tabBar.setCurrentIndex(index)
        self.__stackedWidget.setCurrentIndex(index)
        if self.isMinimized():
            self.expand()

    def currentWidget(self):
        """
        Public method to get a reference to the current widget.
        
        @return reference to the current widget (QWidget)
        """
        return self.__stackedWidget.currentWidget()

    def setCurrentWidget(self, widget):
        """
        Public slot to set the current widget.
        
        @param widget reference to the widget to become the current widget
            (QWidget)
        """
        self.__stackedWidget.setCurrentWidget(widget)
        self.__tabBar.setCurrentIndex(self.__stackedWidget.currentIndex())
        if self.isMinimized():
            self.expand()

    def indexOf(self, widget):
        """
        Public method to get the index of the given widget.
        
        @param widget reference to the widget to get the index of (QWidget)
        @return index of the given widget (integer)
        """
        return self.__stackedWidget.indexOf(widget)

    def isTabEnabled(self, index):
        """
        Public method to check, if a tab is enabled.
        
        @param index index of the tab to check (integer)
        @return flag indicating the enabled state (boolean)
        """
        return self.__tabBar.isTabEnabled(index)

    def setTabEnabled(self, index, enabled):
        """
        Public method to set the enabled state of a tab.
        
        @param index index of the tab to set (integer)
        @param enabled enabled state to set (boolean)
        """
        self.__tabBar.setTabEnabled(index, enabled)

    def orientation(self):
        """
        Public method to get the orientation of the sidebar.
        
        @return orientation of the sidebar (North, East, South, West)
        """
        return self.__orientation

    def setOrientation(self, orient):
        """
        Public method to set the orientation of the sidebar.

        @param orient orientation of the sidebar (North, East, South, West)
        """
        if orient == E5SideBar.North:
            self.__tabBar.setShape(QTabBar.RoundedNorth)
            self.__tabBar.setSizePolicy(QSizePolicy.Expanding,
                                        QSizePolicy.Preferred)
            self.barLayout.setDirection(QBoxLayout.LeftToRight)
            self.layout.setDirection(QBoxLayout.TopToBottom)
            self.layout.setAlignment(self.barLayout, Qt.AlignLeft)
        elif orient == E5SideBar.East:
            self.__tabBar.setShape(QTabBar.RoundedEast)
            self.__tabBar.setSizePolicy(QSizePolicy.Preferred,
                                        QSizePolicy.Expanding)
            self.barLayout.setDirection(QBoxLayout.TopToBottom)
            self.layout.setDirection(QBoxLayout.RightToLeft)
            self.layout.setAlignment(self.barLayout, Qt.AlignTop)
        elif orient == E5SideBar.South:
            self.__tabBar.setShape(QTabBar.RoundedSouth)
            self.__tabBar.setSizePolicy(QSizePolicy.Expanding,
                                        QSizePolicy.Preferred)
            self.barLayout.setDirection(QBoxLayout.LeftToRight)
            self.layout.setDirection(QBoxLayout.BottomToTop)
            self.layout.setAlignment(self.barLayout, Qt.AlignLeft)
        elif orient == E5SideBar.West:
            self.__tabBar.setShape(QTabBar.RoundedWest)
            self.__tabBar.setSizePolicy(QSizePolicy.Preferred,
                                        QSizePolicy.Expanding)
            self.barLayout.setDirection(QBoxLayout.TopToBottom)
            self.layout.setDirection(QBoxLayout.LeftToRight)
            self.layout.setAlignment(self.barLayout, Qt.AlignTop)
        self.__orientation = orient

    def tabIcon(self, index):
        """
        Public method to get the icon of a tab.
        
        @param index index of the tab (integer)
        @return icon of the tab (QIcon)
        """
        return self.__tabBar.tabIcon(index)

    def setTabIcon(self, index, icon):
        """
        Public method to set the icon of a tab.
        
        @param index index of the tab (integer)
        @param icon icon to be set (QIcon)
        """
        self.__tabBar.setTabIcon(index, icon)

    def tabText(self, index):
        """
        Public method to get the text of a tab.
        
        @param index index of the tab (integer)
        @return text of the tab (string)
        """
        return self.__tabBar.tabText(index)

    def setTabText(self, index, text):
        """
        Public method to set the text of a tab.
        
        @param index index of the tab (integer)
        @param text text to set (string)
        """
        self.__tabBar.setTabText(index, text)

    def tabToolTip(self, index):
        """
        Public method to get the tooltip text of a tab.
        
        @param index index of the tab (integer)
        @return tooltip text of the tab (string)
        """
        return self.__tabBar.tabToolTip(index)

    def setTabToolTip(self, index, tip):
        """
        Public method to set the tooltip text of a tab.
        
        @param index index of the tab (integer)
        @param tip tooltip text to set (string)
        """
        self.__tabBar.setTabToolTip(index, tip)

    def tabWhatsThis(self, index):
        """
        Public method to get the WhatsThis text of a tab.
        
        @param index index of the tab (integer)
        @return WhatsThis text of the tab (string)
        """
        return self.__tabBar.tabWhatsThis(index)

    def setTabWhatsThis(self, index, text):
        """
        Public method to set the WhatsThis text of a tab.
        
        @param index index of the tab (integer)
        @param text WhatsThis text to set (string)
        """
        self.__tabBar.setTabWhatsThis(index, text)

    def widget(self, index):
        """
        Public method to get a reference to the widget associated with a tab.
        
        @param index index of the tab (integer)
        @return reference to the widget (QWidget)
        """
        return self.__stackedWidget.widget(index)

    def saveState(self):
        """
        Public method to save the state of the sidebar.
        
        @return saved state as a byte array (QByteArray)
        """
        if len(self.splitterSizes) == 0:
            if self.splitter:
                self.splitterSizes = self.splitter.sizes()
            self.__bigSize = self.size()
            if self.__orientation in [E5SideBar.North, E5SideBar.South]:
                self.__minSize = self.minimumSizeHint().height()
                self.__maxSize = self.maximumHeight()
            else:
                self.__minSize = self.minimumSizeHint().width()
                self.__maxSize = self.maximumWidth()

        data = QByteArray()
        stream = QDataStream(data, QIODevice.WriteOnly)
        stream.setVersion(QDataStream.Qt_4_6)

        stream.writeUInt16(self.Version)
        stream.writeBool(self.__minimized)
        stream << self.__bigSize
        stream.writeUInt16(self.__minSize)
        stream.writeUInt16(self.__maxSize)
        stream.writeUInt16(len(self.splitterSizes))
        for size in self.splitterSizes:
            stream.writeUInt16(size)
        stream.writeBool(self.__autoHide)

        return data

    def restoreState(self, state):
        """
        Public method to restore the state of the sidebar.
        
        @param state byte array containing the saved state (QByteArray)
        @return flag indicating success (boolean)
        """
        if state.isEmpty():
            return False

        if self.__orientation in [E5SideBar.North, E5SideBar.South]:
            minSize = self.layout.minimumSize().height()
            maxSize = self.maximumHeight()
        else:
            minSize = self.layout.minimumSize().width()
            maxSize = self.maximumWidth()

        data = QByteArray(state)
        stream = QDataStream(data, QIODevice.ReadOnly)
        stream.setVersion(QDataStream.Qt_4_6)
        stream.readUInt16()  # version
        minimized = stream.readBool()

        if minimized and not self.__minimized:
            self.shrink()

        stream >> self.__bigSize
        self.__minSize = max(stream.readUInt16(), minSize)
        self.__maxSize = max(stream.readUInt16(), maxSize)
        count = stream.readUInt16()
        self.splitterSizes = []
        for i in range(count):
            self.splitterSizes.append(stream.readUInt16())

        self.__autoHide = stream.readBool()
        self.__autoHideButton.setChecked(not self.__autoHide)

        if not minimized:
            self.expand()

        return True

    #######################################################################
    ## methods below implement the autohide functionality
    #######################################################################

    def __autoHideToggled(self, checked):
        """
        Private slot to handle the toggling of the autohide button.
        
        @param checked flag indicating the checked state of the button
            (boolean)
        """
        self.__autoHide = not checked
        if self.__autoHide:
            self.__autoHideButton.setIcon(
                UI.PixmapCache.getIcon("autoHideOn.png"))
        else:
            self.__autoHideButton.setIcon(
                UI.PixmapCache.getIcon("autoHideOff.png"))

    def __appFocusChanged(self, old, now):
        """
        Private slot to handle a change of the focus.
        
        @param old reference to the widget, that lost focus (QWidget or None)
        @param now reference to the widget having the focus (QWidget or None)
        """
        self.__hasFocus = self.isAncestorOf(now)
        if self.__autoHide and not self.__hasFocus and not self.isMinimized():
            self.shrink()
        elif self.__autoHide and self.__hasFocus and self.isMinimized():
            self.expand()

    def enterEvent(self, event):
        """
        Protected method to handle the mouse entering this widget.
        
        @param event reference to the event (QEvent)
        """
        if self.__autoHide and self.isMinimized():
            self.expand()
        else:
            self.__cancelDelayTimer()

    def leaveEvent(self, event):
        """
        Protected method to handle the mouse leaving this widget.
        
        @param event reference to the event (QEvent)
        """
        if self.__autoHide and not self.__hasFocus and not self.isMinimized():
            self.shrink()
        else:
            self.__cancelDelayTimer()

    def shutdown(self):
        """
        Public method to shut down the object.
        
        This method does some preparations so the object can be deleted
        properly. It disconnects from the focusChanged signal in order to
        avoid trouble later on.
        """
        e5App().focusChanged.disconnect(self.__appFocusChanged)
Exemple #3
0
class TabBarWindow(TabWindow):
    """Implementation which uses a separate QTabBar and QStackedWidget.
    The Tab bar is placed next to the menu bar to save real estate."""
    def __init__(self, app, **kwargs):
        super().__init__(app, **kwargs)

    def _setupUi(self):
        self.setWindowTitle(self.app.NAME)
        self.resize(640, 480)
        self.tabBar = QTabBar()
        self.verticalLayout = QVBoxLayout()
        self.verticalLayout.setContentsMargins(0, 0, 0, 0)
        self._setupActions()
        self._setupMenu()

        self.centralWidget = QWidget(self)
        self.setCentralWidget(self.centralWidget)
        self.stackedWidget = QStackedWidget()
        self.centralWidget.setLayout(self.verticalLayout)
        self.horizontalLayout = QHBoxLayout()
        self.horizontalLayout.addWidget(self.menubar, 0, Qt.AlignTop)
        self.horizontalLayout.addWidget(self.tabBar, 0, Qt.AlignTop)
        self.verticalLayout.addLayout(self.horizontalLayout)
        self.verticalLayout.addWidget(self.stackedWidget)

        self.tabBar.currentChanged.connect(self.showWidget)
        self.tabBar.tabCloseRequested.connect(self.onTabCloseRequested)

        self.stackedWidget.currentChanged.connect(self.updateMenuBar)
        self.stackedWidget.widgetRemoved.connect(self.onRemovedWidget)

        self.tabBar.setTabsClosable(True)
        self.restoreGeometry()

    def addTab(self, page, title, switch=True):
        stack_index = self.stackedWidget.insertWidget(-1, page)
        tab_index = self.tabBar.addTab(title)

        if isinstance(page, DirectoriesDialog):
            self.tabBar.setTabButton(tab_index, QTabBar.RightSide, None)
        if switch:  # switch to the added tab immediately upon creation
            self.setTabIndex(tab_index)
            self.stackedWidget.setCurrentWidget(page)
        return stack_index

    @pyqtSlot(int)
    def showWidget(self, index):
        if index >= 0 and index <= self.stackedWidget.count() - 1:
            self.stackedWidget.setCurrentIndex(index)
            # if not self.tabBar.isTabVisible(index):
            self.setTabVisible(index, True)

    def indexOfWidget(self, widget):
        # Warning: this may return -1 if widget is not a child of stackedwidget
        return self.stackedWidget.indexOf(widget)

    def setCurrentIndex(self, tab_index):
        # The signal will handle switching the stackwidget's widget
        self.setTabIndex(tab_index)
        # self.stackedWidget.setCurrentWidget(self.stackedWidget.widget(tab_index))

    @pyqtSlot(int)
    def setTabIndex(self, index):
        if index is None:
            return
        self.tabBar.setCurrentIndex(index)

    def setTabVisible(self, index, value):
        return self.tabBar.setTabVisible(index, value)

    @pyqtSlot(int)
    def onRemovedWidget(self, index):
        self.removeTab(index)

    @pyqtSlot(int)
    def removeTab(self, index):
        # No need to remove the widget here:
        # self.stackedWidget.removeWidget(self.stackedWidget.widget(index))
        return self.tabBar.removeTab(index)

    @pyqtSlot(int)
    def removeWidget(self, widget):
        return self.stackedWidget.removeWidget(widget)

    def isTabVisible(self, index):
        return self.tabBar.isTabVisible(index)

    def getCurrentIndex(self):
        return self.stackedWidget.currentIndex()

    def getWidgetAtIndex(self, index):
        return self.stackedWidget.widget(index)

    def getCount(self):
        return self.stackedWidget.count()

    @pyqtSlot()
    def toggleTabBar(self):
        value = self.sender().isChecked()
        self.actionToggleTabs.setChecked(value)
        self.tabBar.setVisible(value)

    @pyqtSlot(int)
    def onTabCloseRequested(self, index):
        current_widget = self.getWidgetAtIndex(index)
        if isinstance(current_widget, DirectoriesDialog):
            # On MacOS, the tab has a close button even though we explicitely
            # set it to None in order to hide it. This should prevent
            # the "Directories" tab from closing by mistake.
            return
        current_widget.close()
        self.stackedWidget.removeWidget(current_widget)
Exemple #4
0
class FontWindow(BaseWindow):
    def __init__(self, font, parent=None):
        super().__init__(parent)
        self._font = None

        self._infoWindow = None
        self._featuresWindow = None
        self._groupsWindow = None
        self._kerningWindow = None
        self._metricsWindow = None

        self.toolBar = ToolBar(self)
        self.toolBar.setTools(t() for t in QApplication.instance().drawingTools())

        self.glyphCellView = GlyphCellView(self)
        self.glyphCellView.glyphActivated.connect(self.openGlyphTab)
        self.glyphCellView.glyphsDropped.connect(self._orderChanged)
        self.glyphCellView.selectionChanged.connect(self._selectionChanged)
        self.glyphCellView.setAcceptDrops(True)
        self.glyphCellView.setCellRepresentationName("TruFont.GlyphCell")
        self.glyphCellView.setFrameShape(self.glyphCellView.NoFrame)
        self.glyphCellView.setFocus()

        self.tabWidget = TabWidget(self)
        self.tabWidget.setAutoHide(True)
        self.tabWidget.setHeroFirstTab(True)
        self.tabWidget.addTab(self.tr("Font"))

        self.stackWidget = QStackedWidget(self)
        self.stackWidget.addWidget(self.glyphCellView)
        self.tabWidget.currentTabChanged.connect(self._tabChanged)
        self.tabWidget.tabRemoved.connect(
            lambda index: self.stackWidget.removeWidget(self.stackWidget.widget(index))
        )
        self.stackWidget.currentChanged.connect(self._widgetChanged)

        self.propertiesView = PropertiesView(font, self)
        self.propertiesView.hide()

        self.statusBar = StatusBar(self)
        self.statusBar.setMinimumSize(32)
        self.statusBar.setMaximumSize(128)
        self.statusBar.sizeChanged.connect(self._sizeChanged)

        self.setFont_(font)

        app = QApplication.instance()
        app.dispatcher.addObserver(
            self, "_drawingToolRegistered", "drawingToolRegistered"
        )
        app.dispatcher.addObserver(
            self, "_drawingToolUnregistered", "drawingToolUnregistered"
        )
        app.dispatcher.addObserver(
            self, "_glyphViewGlyphsChanged", "glyphViewGlyphsChanged"
        )

        layout = QHBoxLayout(self)
        layout.addWidget(self.toolBar)
        vLayout = QVBoxLayout()
        vLayout.addWidget(self.tabWidget)
        pageWidget = PageWidget()
        pageWidget.addWidget(self.stackWidget)
        pageWidget.addWidget(self.statusBar)
        vLayout.addWidget(pageWidget)
        layout.addLayout(vLayout)
        layout.addWidget(self.propertiesView)
        layout.setContentsMargins(0, 2, 0, 0)
        layout.setSpacing(2)

        elements = [
            ("Ctrl+D", self.deselect),
            (platformSpecific.closeKeySequence(), self.closeGlyphTab),
            # XXX: does this really not warrant widget focus?
            (QKeySequence.Delete, self.delete),
            ("Shift+" + QKeySequence(QKeySequence.Delete).toString(), self.delete),
            ("Z", lambda: self.zoom(1)),
            ("X", lambda: self.zoom(-1)),
        ]
        e = platformSpecific.altDeleteSequence()
        if e is not None:
            elements.append((e, self.delete))
        e = platformSpecific.altRedoSequence()
        if e is not None:
            elements.append((e, self.redo))
        for keys, callback in elements:
            shortcut = QShortcut(QKeySequence(keys), self)
            shortcut.activated.connect(callback)

        self.installEventFilter(PreviewEventFilter(self))

        self.readSettings()
        self.propertiesView.activeLayerModified.connect(self._activeLayerModified)
        self.statusBar.sizeChanged.connect(self.writeSettings)

    def readSettings(self):
        geometry = settings.fontWindowGeometry()
        if geometry:
            self.restoreGeometry(geometry)
        cellSize = settings.glyphCellSize()
        self.statusBar.setSize(cellSize)
        hidden = settings.propertiesHidden()
        if not hidden:
            self.properties()

    def writeSettings(self):
        settings.setFontWindowGeometry(self.saveGeometry())
        settings.setGlyphCellSize(self.glyphCellView.cellSize()[0])
        settings.setPropertiesHidden(self.propertiesView.isHidden())

    def menuBar(self):
        return self.layout().menuBar()

    def setMenuBar(self, menuBar):
        self.layout().setMenuBar(menuBar)

    def setupMenu(self, menuBar):
        app = QApplication.instance()

        fileMenu = menuBar.fetchMenu(Entries.File)
        fileMenu.fetchAction(Entries.File_New)
        fileMenu.fetchAction(Entries.File_Open)
        fileMenu.fetchMenu(Entries.File_Open_Recent)
        if not platformSpecific.mergeOpenAndImport():
            fileMenu.fetchAction(Entries.File_Import)
        fileMenu.addSeparator()
        fileMenu.fetchAction(Entries.File_Save, self.saveFile)
        fileMenu.fetchAction(Entries.File_Save_As, self.saveFileAs)
        fileMenu.fetchAction(Entries.File_Save_All)
        fileMenu.fetchAction(Entries.File_Reload, self.reloadFile)
        fileMenu.addSeparator()
        fileMenu.fetchAction(Entries.File_Export, self.exportFile)
        fileMenu.fetchAction(Entries.File_Exit)

        editMenu = menuBar.fetchMenu(Entries.Edit)
        self._undoAction = editMenu.fetchAction(Entries.Edit_Undo, self.undo)
        self._redoAction = editMenu.fetchAction(Entries.Edit_Redo, self.redo)
        editMenu.addSeparator()
        cut = editMenu.fetchAction(Entries.Edit_Cut, self.cut)
        copy = editMenu.fetchAction(Entries.Edit_Copy, self.copy)
        copyComponent = editMenu.fetchAction(
            Entries.Edit_Copy_As_Component, self.copyAsComponent
        )
        paste = editMenu.fetchAction(Entries.Edit_Paste, self.paste)
        self._clipboardActions = (cut, copy, copyComponent, paste)
        editMenu.fetchAction(Entries.Edit_Select_All, self.selectAll)
        # editMenu.fetchAction(Entries.Edit_Deselect, self.deselect)
        editMenu.fetchAction(Entries.Edit_Find, self.findGlyph)
        editMenu.addSeparator()
        editMenu.fetchAction(Entries.Edit_Settings)

        viewMenu = menuBar.fetchMenu(Entries.View)
        viewMenu.fetchAction(Entries.View_Zoom_In, lambda: self.zoom(1))
        viewMenu.fetchAction(Entries.View_Zoom_Out, lambda: self.zoom(-1))
        viewMenu.fetchAction(Entries.View_Reset_Zoom, self.resetZoom)
        viewMenu.addSeparator()
        viewMenu.fetchAction(Entries.View_Next_Tab, lambda: self.tabOffset(1))
        viewMenu.fetchAction(Entries.View_Previous_Tab, lambda: self.tabOffset(-1))
        viewMenu.fetchAction(Entries.View_Next_Glyph, lambda: self.glyphOffset(1))
        viewMenu.fetchAction(Entries.View_Previous_Glyph, lambda: self.glyphOffset(-1))
        viewMenu.fetchAction(Entries.View_Layer_Up, lambda: self.layerOffset(-1))
        viewMenu.fetchAction(Entries.View_Layer_Down, lambda: self.layerOffset(1))
        viewMenu.addSeparator()
        viewMenu.fetchAction(Entries.View_Show_Points)
        viewMenu.fetchAction(Entries.View_Show_Metrics)
        viewMenu.fetchAction(Entries.View_Show_Images)
        viewMenu.fetchAction(Entries.View_Show_Guidelines)

        fontMenu = menuBar.fetchMenu(Entries.Font)
        fontMenu.fetchAction(Entries.Font_Font_Info, self.fontInfo)
        fontMenu.fetchAction(Entries.Font_Font_Features, self.fontFeatures)
        fontMenu.addSeparator()
        fontMenu.fetchAction(Entries.Font_Add_Glyphs, self.addGlyphs)
        fontMenu.fetchAction(Entries.Font_Sort, self.sortGlyphs)

        # glyphMenu = menuBar.fetchMenu(self.tr("&Glyph"))
        # self._layerAction = glyphMenu.fetchAction(
        #     self.tr("&Layer Actions…"), self.layerActions, "L")

        menuBar.fetchMenu(Entries.Scripts)

        windowMenu = menuBar.fetchMenu(Entries.Window)
        windowMenu.fetchAction(Entries.Window_Groups, self.groups)
        windowMenu.fetchAction(Entries.Window_Kerning, self.kerning)
        windowMenu.fetchAction(Entries.Window_Metrics, self.metrics)
        windowMenu.fetchAction(Entries.Window_Scripting)
        windowMenu.fetchAction(Entries.Window_Properties, self.properties)
        windowMenu.addSeparator()
        action = windowMenu.fetchAction(Entries.Window_Output)
        action.setEnabled(app.outputWindow is not None)

        helpMenu = menuBar.fetchMenu(Entries.Help)
        helpMenu.fetchAction(Entries.Help_Documentation)
        helpMenu.fetchAction(Entries.Help_Report_An_Issue)
        helpMenu.addSeparator()
        helpMenu.fetchAction(Entries.Help_About)

        self._updateGlyphActions()

    # --------------
    # Custom methods
    # --------------

    def font_(self):
        return self._font

    def setFont_(self, font):
        if self._font is not None:
            self._font.removeObserver(self, "Font.Changed")
            self._font.removeObserver(self, "Font.GlyphOrderChanged")
            self._font.removeObserver(self, "Font.SortDescriptorChanged")
        self._font = font
        self.setWindowTitle(self.fontTitle())
        if font is None:
            return
        self._updateGlyphsFromGlyphOrder()
        font.addObserver(self, "_fontChanged", "Font.Changed")
        font.addObserver(self, "_glyphOrderChanged", "Font.GlyphOrderChanged")
        font.addObserver(self, "_sortDescriptorChanged", "Font.SortDescriptorChanged")

    def fontTitle(self):
        if self._font is None:
            return None
        path = self._font.path or self._font.binaryPath
        if path is not None:
            return os.path.basename(path.rstrip(os.sep))
        return self.tr("Untitled")

    def isGlyphTab(self):
        return bool(self.stackWidget.currentIndex())

    def openGlyphTab(self, glyph):
        # if a tab with this glyph exists already, switch to it
        for index in range(self.stackWidget.count()):
            if not index:
                continue
            view = self.stackWidget.widget(index)
            if list(view.glyphs()) == [glyph]:
                self.tabWidget.setCurrentTab(index)
                return
        # spawn
        widget = GlyphCanvasView(self)
        widget.setInputNames([glyph.name])
        widget.activeGlyphChanged.connect(self._selectionChanged)
        widget.glyphNamesChanged.connect(self._namesChanged)
        widget.pointSizeModified.connect(self.statusBar.setSize)
        widget.toolModified.connect(self.toolBar.setCurrentTool)
        # add
        self.tabWidget.addTab(_textForGlyphs([glyph]))
        self.stackWidget.addWidget(widget)
        # activate
        self.tabWidget.setCurrentTab(-1)

    def closeGlyphTab(self):
        index = self.stackWidget.currentIndex()
        if index:
            self.tabWidget.removeTab(index)

    def maybeSaveBeforeExit(self):
        if self._font.dirty:
            ret = CloseMessageBox.getCloseDocument(self, self.fontTitle())
            if ret == QMessageBox.Save:
                self.saveFile()
                return True
            elif ret == QMessageBox.Discard:
                return True
            return False
        return True

    # -------------
    # Notifications
    # -------------

    # app

    def _drawingToolRegistered(self, notification):
        toolClass = notification.data["tool"]
        index = self.stackWidget.currentIndex()
        parent = self.stackWidget.currentWidget() if index else None
        self.toolBar.addTool(toolClass(parent=parent))

    def _drawingToolUnregistered(self, notification):
        toolClass = notification.data["tool"]
        for tool in self.toolBar.tools():
            if isinstance(tool, toolClass):
                self.toolBar.removeTool(tool)
                return
        raise ValueError(f"couldn't find tool to unregister: {toolClass}")

    def _glyphViewGlyphsChanged(self, notification):
        self._updateGlyphActions()

    # widgets

    def _activeLayerModified(self):
        if self.isGlyphTab():
            widget = self.stackWidget.currentWidget()
            index = self.sender().currentIndex().row()
            layers = self._font.layers
            layer = layers[layers.layerOrder[index]]
            currentGlyph = widget.activeGlyph()
            # XXX: adjust TLayer.get and use it
            if currentGlyph.name in layer:
                glyph = layer[currentGlyph.name]
            else:
                glyph = layer.newGlyph(currentGlyph.name)
            widget.setActiveGlyph(glyph)

    def _namesChanged(self):
        sender = self.sender()
        index = self.stackWidget.indexOf(sender)
        self.tabWidget.setTabName(index, _textForGlyphs(sender.glyphs()))

    def _sizeChanged(self):
        size = self.statusBar.size()
        if self.isGlyphTab():
            widget = self.stackWidget.currentWidget()
            widget.setPointSize(size)
        else:
            self.glyphCellView.setCellSize(size)

    def _tabChanged(self, index):
        self.statusBar.setShouldPropagateSize(not index)
        # we need to hide, then setParent, then show
        self.stackWidget.currentWidget().hide()
        newWidget = self.stackWidget.widget(index)
        if index:
            for tool in self.toolBar.tools():
                tool.setParent(newWidget)
        self.stackWidget.setCurrentIndex(index)
        newWidget.setFocus(Qt.OtherFocusReason)

    def _toolChanged(self, tool):
        widget = self.stackWidget.currentWidget()
        ok = widget.setCurrentTool(tool)
        # the glyph view NAKed the change (in mouseDown)
        # set back the current tool in the toolbar
        if not ok:
            self.toolBar.setCurrentTool(widget.currentTool())

    def _widgetChanged(self, index):
        # update current glyph
        self._updateCurrentGlyph()
        # update undo/redo
        self._updateGlyphActions()
        # update slider
        if self.isGlyphTab():
            lo, hi, unit = 0, 900000, " pt"
            widget = self.stackWidget.currentWidget()
            size = widget.pointSize()
        else:
            lo, hi, unit = 32, 128, None
            size = self.glyphCellView.cellSize()[0]
        self.statusBar.setMinimumSize(lo)
        self.statusBar.setMaximumSize(hi)
        self.statusBar.setSize(size)
        self.statusBar.setUnit(unit)
        self.statusBar.setTextVisible(not self.isGlyphTab())
        # update and connect setCurrentTool
        try:
            self.toolBar.currentToolChanged.disconnect()
        except TypeError:
            pass
        if not index:
            return
        widget = self.stackWidget.currentWidget()
        widget.setCurrentTool(self.toolBar.currentTool())
        self.toolBar.currentToolChanged.connect(self._toolChanged)

    def _orderChanged(self):
        # TODO: reimplement when we start showing glyph subsets
        glyphs = self.glyphCellView.glyphs()
        self._font.glyphOrder = [glyph.name for glyph in glyphs]

    def _selectionChanged(self):
        if self.isGlyphTab():
            activeGlyph = self.stackWidget.currentWidget().activeGlyph()
        else:
            activeGlyph = self.glyphCellView.lastSelectedGlyph()
            # selection text
            # TODO: this should probably be internal to the label
            selection = self.glyphCellView.selection()
            if selection is not None:
                count = len(selection)
                if count == 1:
                    glyph = self.glyphCellView.glyphsForIndexes(selection)[0]
                    text = "%s " % glyph.name
                else:
                    text = ""
                if count:
                    text = self.tr(f"{text}(%n selected)", n=count)
            else:
                text = ""
            self.statusBar.setText(text)
        # currentGlyph
        app = QApplication.instance()
        app.setCurrentGlyph(activeGlyph)
        # actions
        self._updateGlyphActions()

    # defcon

    def _fontChanged(self, notification):
        font = notification.object
        self.setWindowModified(font.dirty)

    def _glyphOrderChanged(self, notification):
        self._updateGlyphsFromGlyphOrder()

    def _updateGlyphsFromGlyphOrder(self):
        font = self._font
        glyphOrder = font.glyphOrder
        if glyphOrder:
            glyphCount = 0
            glyphs = []
            for glyphName in glyphOrder:
                if glyphName in font:
                    glyph = font[glyphName]
                    glyphCount += 1
                else:
                    glyph = font.get(glyphName, asTemplate=True)
                glyphs.append(glyph)
            if glyphCount < len(font):
                # if some glyphs in the font are not present in the glyph
                # order, loop again to add them at the end
                for glyph in font:
                    if glyph not in glyphs:
                        glyphs.append(glyph)
                font.disableNotifications(observer=self)
                font.glyphOrder = [glyph.name for glyph in glyphs]
                font.enableNotifications(observer=self)
        else:
            glyphs = list(font)
            font.disableNotifications(observer=self)
            font.glyphOrder = [glyph.name for glyph in glyphs]
            font.enableNotifications(observer=self)
        self.glyphCellView.setGlyphs(glyphs)

    def _sortDescriptorChanged(self, notification):
        font = notification.object
        descriptors = notification.data["newValue"]
        if descriptors is None:
            return
        if descriptors[0]["type"] == "glyphSet":
            glyphNames = descriptors[0]["glyphs"]
        else:
            glyphNames = font.unicodeData.sortGlyphNames(font.keys(), descriptors)
        font.glyphOrder = glyphNames

    # ------------
    # Menu methods
    # ------------

    # File

    def saveFile(self, path=None, ufoFormatVersion=3):
        if path is None and self._font.path is None:
            self.saveFileAs()
        else:
            if path is None:
                path = self._font.path
            self._font.save(path, ufoFormatVersion)

    def saveFileAs(self):
        fileFormats = OrderedDict(
            [
                (self.tr("UFO Font version 3 {}").format("(*.ufo)"), 3),
                (self.tr("UFO Font version 2 {}").format("(*.ufo)"), 2),
            ]
        )
        state = settings.saveFileDialogState()
        path = self._font.path or self._font.binaryPath
        if path:
            directory = os.path.dirname(path)
        else:
            directory = (
                None
                if state
                else QStandardPaths.standardLocations(QStandardPaths.DocumentsLocation)[
                    0
                ]
            )
        # TODO: switch to directory dlg on platforms that need it
        dialog = QFileDialog(
            self, self.tr("Save File"), directory, ";;".join(fileFormats.keys())
        )
        if state:
            dialog.restoreState(state)
        dialog.setAcceptMode(QFileDialog.AcceptSave)
        if directory:
            dialog.setDirectory(directory)
        ok = dialog.exec_()
        settings.setSaveFileDialogState(dialog.saveState())
        if ok:
            nameFilter = dialog.selectedNameFilter()
            path = dialog.selectedFiles()[0]
            if not os.path.basename(path).endswith(".ufo"):
                path += ".ufo"
            self.saveFile(path, fileFormats[nameFilter])
            app = QApplication.instance()
            app.setCurrentFile(self._font.path)
            self.setWindowTitle(self.fontTitle())
        # return ok

    def reloadFile(self):
        font = self._font
        path = font.path or font.binaryPath
        if not font.dirty or path is None:
            return
        if not ReloadMessageBox.getReloadDocument(self, self.fontTitle()):
            return
        if font.path is not None:
            font.reloadInfo()
            font.reloadKerning()
            font.reloadGroups()
            font.reloadFeatures()
            font.reloadLib()
            font.reloadGlyphs(font.keys())
            font.dirty = False
        else:
            # TODO: we should do this in-place
            font_ = font.__class__().new()
            font_.extract(font.binaryPath)
            self.setFont_(font_)

    def exportFile(self):
        params, ok = ExportDialog.getExportParameters(self, self._font)
        if not ok:
            return
        baseName = params["baseName"]
        directory = params["exportDirectory"]
        compression = set(map(str.lower, params["compression"]))
        for format in map(str.lower, params["formats"]):
            fileName = f"{baseName}.{format}"
            path = os.path.join(directory, fileName)
            try:
                self._font.export(path, format, compression=compression)
            except Exception as e:
                msg = (
                    self.tr("This font’s feature file contains an error.")
                    if isinstance(e, FeatureLibError)
                    else None
                )
                errorReports.showCriticalException(e, message=msg)

    # Edit

    def undo(self):
        widget = self.stackWidget.currentWidget()
        if self.isGlyphTab():
            glyph = widget.activeGlyph()
        else:
            glyph = widget.lastSelectedGlyph()
        glyph.undo()

    def redo(self):
        widget = self.stackWidget.currentWidget()
        if self.isGlyphTab():
            glyph = widget.activeGlyph()
        else:
            glyph = widget.lastSelectedGlyph()
        glyph.redo()

    def cut(self):
        self.copy()
        widget = self.stackWidget.currentWidget()
        if self.isGlyphTab():
            glyph = widget.activeGlyph()
            deleteUISelection(glyph)
        else:
            glyphs = widget.glyphs()
            for index in widget.selection():
                glyph = glyphs[index]
                glyph.clear()

    def copy(self):
        font = self._font
        widget = self.stackWidget.currentWidget()
        clipboard = QApplication.clipboard()
        mimeData = QMimeData()
        if self.isGlyphTab():
            glyph = widget.activeGlyph()
            copyGlyph = glyph.getRepresentation("TruFont.FilterSelection")
            packGlyphs = (copyGlyph,)
        else:
            glyphs = self.glyphCellView.glyphs()
            packGlyphs = (
                glyphs[index] for index in sorted(self.glyphCellView.selection())
            )

        svgGlyphs = []
        pickled = []
        for i, glyph in enumerate(packGlyphs):
            pickled.append(glyph.serialize(blacklist=("name", "unicodes")))

            pen = SVGPathPen(font)
            glyph.draw(pen)
            col = i % 5
            row = i // 5
            g = '<g transform="matrix(1,0,0,-1,{:f},{:f})"><path d="{}"/></g>'.format(
                font.info.unitsPerEm * col,
                font.info.unitsPerEm * row,
                pen.getCommands(),
            )
            svgGlyphs.append(g)

        mimeData.setData("application/x-trufont-glyph-data", pickle.dumps(pickled))

        svg = """\
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
 "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg">
%s
</svg>
""" % "\n".join(
            svgGlyphs
        )
        mimeData.setData("image/svg+xml", svg.encode("utf-8"))

        clipboard.setMimeData(mimeData)

    def copyAsComponent(self):
        if self.isGlyphTab():
            pass
        else:
            glyphs = self.glyphCellView.glyphs()
            pickled = []
            for index in self.glyphCellView.selection():
                glyph = glyphs[index]
                componentGlyph = glyph.__class__()
                componentGlyph.width = glyph.width
                component = componentGlyph.instantiateComponent()
                component.baseGlyph = glyph.name
                pickled.append(componentGlyph.serialize())
            clipboard = QApplication.clipboard()
            mimeData = QMimeData()
            mimeData.setData("application/x-trufont-glyph-data", pickle.dumps(pickled))
            clipboard.setMimeData(mimeData)

    def paste(self):
        isGlyphTab = self.isGlyphTab()
        widget = self.stackWidget.currentWidget()
        if isGlyphTab:
            glyphs = (widget.activeGlyph(),)
        else:
            selection = self.glyphCellView.selection()
            glyphs = widget.glyphsForIndexes(selection)
        clipboard = QApplication.clipboard()
        mimeData = clipboard.mimeData()
        if mimeData.hasFormat("application/x-trufont-glyph-data"):
            data = pickle.loads(mimeData.data("application/x-trufont-glyph-data"))
            if len(data) == len(glyphs):
                for pickled, glyph in zip(data, glyphs):
                    if isGlyphTab:
                        pasteGlyph = glyph.__class__()
                        pasteGlyph.deserialize(pickled)
                        # TODO: if we serialize selected state, we don't need
                        # to do this
                        pasteGlyph.selected = True
                        if (
                            len(pasteGlyph)
                            or len(pasteGlyph.components)
                            or len(pasteGlyph.anchors)
                        ):
                            glyph.beginUndoGroup()
                            glyph.holdNotifications()
                            count = len(glyph)
                            pen = glyph.getPointPen()
                            # contours, components
                            pasteGlyph.drawPoints(pen)
                            for contour in glyph[count:]:
                                contour.selected = True
                            # anchors
                            for anchor in pasteGlyph.anchors:
                                glyph.appendAnchor(dict(anchor))
                            # guidelines
                            for guideline in pasteGlyph.guidelines:
                                glyph.appendGuideline(dict(guideline))
                            glyph.releaseHeldNotifications()
                            glyph.endUndoGroup()
                    else:
                        glyph.deserialize(pickled)
            return
        if mimeData.hasFormat("image/svg+xml"):
            if len(glyphs) == 1:
                glyph = glyphs[0]
                try:
                    svgPath = SVGPath.fromstring(mimeData.data("image/svg+xml"))
                except Exception:
                    pass
                else:
                    glyph.beginUndoGroup()
                    if not isGlyphTab:
                        glyph.clear()
                    svgPath.draw(glyph.getPen())
                    glyph.endUndoGroup()
                    return
        if mimeData.hasText():
            if len(glyphs) == 1:
                glyph = glyphs[0]
                otherGlyph = glyph.__class__()
                text = mimeData.text()
                try:
                    readGlyphFromString(text, otherGlyph, otherGlyph.getPointPen())
                except Exception:
                    try:
                        svgPath = SVGPath.fromstring(text)
                        svgPath.draw(otherGlyph.getPen())
                    except Exception:
                        return
                glyph.beginUndoGroup()
                if not isGlyphTab:
                    glyph.clear()
                otherGlyph.drawPoints(glyph.getPointPen())
                glyph.endUndoGroup()

    def selectAll(self):
        widget = self.stackWidget.currentWidget()
        if self.isGlyphTab():
            glyph = widget.activeGlyph()
            if glyph.selected:
                for anchor in glyph.anchors:
                    anchor.selected = True
                for component in glyph.components:
                    component.selected = True
            else:
                glyph.selected = True
        else:
            widget.selectAll()

    def deselect(self):
        widget = self.stackWidget.currentWidget()
        if self.isGlyphTab():
            glyph = widget.activeGlyph()
            for anchor in glyph.anchors:
                anchor.selected = False
            for component in glyph.components:
                component.selected = False
            glyph.selected = False
        else:
            widget.setSelection(set())

    def delete(self):
        modifiers = QApplication.keyboardModifiers()
        widget = self.stackWidget.currentWidget()
        if self.isGlyphTab():
            glyph = widget.activeGlyph()
            # TODO: fuse more the two methods, they're similar and delete is
            # Cut except not putting in the clipboard
            if modifiers & Qt.AltModifier:
                deleteUISelection(glyph)
            else:
                preserveShape = not modifiers & Qt.ShiftModifier
                removeUIGlyphElements(glyph, preserveShape)
        else:
            erase = modifiers & Qt.ShiftModifier
            if self._proceedWithDeletion(erase):
                glyphs = widget.glyphsForIndexes(widget.selection())
                for glyph in glyphs:
                    font = glyph.font
                    for layer in font.layers:
                        if glyph.name in layer:
                            defaultLayer = layer[glyph.name] == glyph
                            if defaultLayer and not erase:
                                # TODO: clear in glyph.template setter?
                                glyph.clear()
                                glyph.template = True
                            else:
                                del layer[glyph.name]

    def findGlyph(self):
        widget = self.stackWidget.currentWidget()
        if self.isGlyphTab():
            glyph = widget.activeGlyph()
            newGlyph, ok = FindDialog.getNewGlyph(self, glyph)
            if ok and newGlyph is not None:
                widget.setActiveGlyph(newGlyph)
        else:
            pass  # XXX

    # View

    def zoom(self, step):
        if self.isGlyphTab():
            widget = self.stackWidget.currentWidget()
            newScale = widget.scale() * pow(1.2, step)
            widget.zoom(newScale)
            self.statusBar.setSize(widget.pointSize())
        else:
            value = self.statusBar.size()
            newValue = value + 10 * step
            self.statusBar.setSize(newValue)

    def resetZoom(self):
        widget = self.stackWidget.currentWidget()
        if self.isGlyphTab():
            widget.fitScaleBBox()
        else:
            settings.removeGlyphCellSize()
            cellSize = settings.glyphCellSize()
            self.statusBar.setSize(cellSize)

    def tabOffset(self, value):
        tab = self.tabWidget.currentTab()
        newTab = (tab + value) % len(self.tabWidget.tabs())
        self.tabWidget.setCurrentTab(newTab)

    def glyphOffset(self, value):
        widget = self.stackWidget.currentWidget()
        if self.isGlyphTab():
            currentGlyph = widget.activeGlyph()
            font = currentGlyph.font
            glyphOrder = font.glyphOrder
            # should be enforced in fontView already
            if not (glyphOrder and len(glyphOrder)):
                return
            index = glyphOrder.index(currentGlyph.name)
            newIndex = (index + value) % len(glyphOrder)
            glyph = font[glyphOrder[newIndex]]
            widget.setActiveGlyph(glyph)
        else:
            lastSelectedCell = widget.lastSelectedCell()
            if lastSelectedCell is None:
                return
            newIndex = lastSelectedCell + value
            if newIndex < 0 or newIndex >= len(widget.glyphs()):
                return
            widget.setSelection({newIndex})

    def layerOffset(self, value):
        widget = self.stackWidget.currentWidget()
        if self.isGlyphTab():
            currentGlyph = widget.activeGlyph()
            layerSet, layer = currentGlyph.layerSet, currentGlyph.layer
            if None in (layerSet, layer):
                return
            index = layerSet.layerOrder.index(layer.name)
            newIndex = (index + value) % len(layerSet)
            layer_ = layerSet[layerSet.layerOrder[newIndex]]
            if layer_ == layer:
                return
            # XXX: fix get
            # glyph = layer_.get(currentGlyph.name)
            if currentGlyph.name in layer_:
                glyph = layer_[currentGlyph.name]
            else:
                glyph = layer_.newGlyph(currentGlyph.name)
            widget.setActiveGlyph(glyph)

    # Font

    def fontInfo(self):
        # If a window is already opened, bring it to the front, else spawn one.
        # TODO: see about using widget.setAttribute(Qt.WA_DeleteOnClose)
        # otherwise it seems we're just leaking memory after each close...
        # (both raise_ and show allocate memory instead of using the hidden
        # widget it seems)
        if self._infoWindow is not None and self._infoWindow.isVisible():
            self._infoWindow.raise_()
        else:
            self._infoWindow = FontInfoWindow(self._font, self)
            self._infoWindow.show()

    def fontFeatures(self):
        # TODO: see up here
        if self._featuresWindow is not None and self._featuresWindow.isVisible():
            self._featuresWindow.raise_()
        else:
            self._featuresWindow = FontFeaturesWindow(self._font, self)
            self._featuresWindow.show()

    def addGlyphs(self):
        glyphs = self.glyphCellView.glyphs()
        newGlyphNames, params, ok = AddGlyphsDialog.getNewGlyphNames(self, glyphs)
        if ok:
            sortFont = params.pop("sortFont")
            for name in newGlyphNames:
                glyph = self._font.get(name, **params)
                if glyph is not None:
                    glyphs.append(glyph)
            self.glyphCellView.setGlyphs(glyphs)
            if sortFont:
                # TODO: when the user add chars from a glyphSet and no others,
                # should we try to sort according to that glyphSet?
                # The above would probably warrant some rearchitecturing.
                # kick-in the sort mechanism
                self._font.sortDescriptor = self._font.sortDescriptor

    def sortGlyphs(self):
        sortDescriptor, ok = SortDialog.getDescriptor(self, self._font.sortDescriptor)
        if ok:
            self._font.sortDescriptor = sortDescriptor

    # Window

    def groups(self):
        # TODO: see up here
        if self._groupsWindow is not None and self._groupsWindow.isVisible():
            self._groupsWindow.raise_()
        else:
            self._groupsWindow = GroupsWindow(self._font, self)
            self._groupsWindow.show()

    def kerning(self):
        # TODO: see up here
        if self._kerningWindow is not None and self._kerningWindow.isVisible():
            self._kerningWindow.raise_()
        else:
            self._kerningWindow = KerningWindow(self._font, self)
            self._kerningWindow.show()

    def metrics(self):
        # TODO: see up here
        if self._metricsWindow is not None and self._metricsWindow.isVisible():
            self._metricsWindow.raise_()
        else:
            self._metricsWindow = MetricsWindow(self._font)
            # XXX: need proper, fast windowForFont API!
            self._metricsWindow._fontWindow = self
            self.destroyed.connect(self._metricsWindow.close)
            self._metricsWindow.show()
        # TODO: default string kicks-in on the window before this. Figure out
        # how to make a clean interface
        selection = self.glyphCellView.selection()
        if selection:
            glyphs = self.glyphCellView.glyphsForIndexes(selection)
            self._metricsWindow.setGlyphs(glyphs)

    def properties(self):
        shouldBeVisible = self.propertiesView.isHidden()
        self.propertiesView.setVisible(shouldBeVisible)
        self.writeSettings()

    # update methods

    def _setGlyphPreview(self, value):
        index = self.stackWidget.currentIndex()
        if index:
            widget = self.stackWidget.currentWidget()
            widget.setPreviewEnabled(value)

    def _updateCurrentGlyph(self):
        # TODO: refactor this pattern...
        widget = self.stackWidget.currentWidget()
        if self.isGlyphTab():
            glyph = widget.activeGlyph()
        else:
            glyph = widget.lastSelectedGlyph()
        if glyph is not None:
            app = QApplication.instance()
            app.setCurrentGlyph(glyph)

    def _updateGlyphActions(self):
        if not hasattr(self, "_undoAction"):
            return
        widget = self.stackWidget.currentWidget()
        if self.isGlyphTab():
            currentGlyph = widget.activeGlyph()
        else:
            currentGlyph = widget.lastSelectedGlyph()
        # disconnect eventual signal of previous glyph
        objects = ((self._undoAction, self.undo), (self._redoAction, self.redo))
        for action, slot in objects:
            try:
                action.disconnect()
            except TypeError:
                pass
            action.triggered.connect(slot)
        # now update status
        if currentGlyph is None:
            self._undoAction.setEnabled(False)
            self._redoAction.setEnabled(False)
        else:
            undoManager = currentGlyph.undoManager
            self._undoAction.setEnabled(currentGlyph.canUndo())
            undoManager.canUndoChanged.connect(self._undoAction.setEnabled)
            self._redoAction.setEnabled(currentGlyph.canRedo())
            undoManager.canRedoChanged.connect(self._redoAction.setEnabled)
        # and other actions
        for action in self._clipboardActions:
            action.setEnabled(currentGlyph is not None)

    # helper

    def _proceedWithDeletion(self, erase=False):
        if not self.glyphCellView.selection():
            return
        tr = self.tr("Delete") if erase else self.tr("Clear")
        text = self.tr("Do you want to %s selected glyphs?") % tr.lower()
        closeDialog = QMessageBox(
            QMessageBox.Question,
            "",
            self.tr("%s glyphs") % tr,
            QMessageBox.Yes | QMessageBox.No,
            self,
        )
        closeDialog.setInformativeText(text)
        closeDialog.setModal(True)
        ret = closeDialog.exec_()
        if ret == QMessageBox.Yes:
            return True
        return False

    # ----------
    # Qt methods
    # ----------

    def setWindowTitle(self, title):
        if platformSpecific.appNameInTitle():
            title += " – TruFont"
        super().setWindowTitle(f"[*]{title}")

    def sizeHint(self):
        return QSize(1270, 800)

    def moveEvent(self, event):
        self.writeSettings()

    resizeEvent = moveEvent

    def showEvent(self, event):
        app = QApplication.instance()
        data = dict(font=self._font, window=self)
        app.postNotification("fontWindowWillOpen", data)
        super().showEvent(event)
        app.postNotification("fontWindowOpened", data)

    def closeEvent(self, event):
        ok = self.maybeSaveBeforeExit()
        if ok:
            app = QApplication.instance()
            data = dict(font=self._font, window=self)
            app.postNotification("fontWindowWillClose", data)
            self._font.removeObserver(self, "Font.Changed")
            app = QApplication.instance()
            app.dispatcher.removeObserver(self, "drawingToolRegistered")
            app.dispatcher.removeObserver(self, "drawingToolUnregistered")
            app.dispatcher.removeObserver(self, "glyphViewGlyphsChanged")
            event.accept()
        else:
            event.ignore()

    def event(self, event):
        if event.type() == QEvent.WindowActivate:
            app = QApplication.instance()
            app.setCurrentFontWindow(self)
            self._updateCurrentGlyph()
        return super().event(event)

    def paintEvent(self, event):
        painter = QPainter(self)
        painter.fillRect(event.rect(), QColor(212, 212, 212))
Exemple #5
0
class SubTabWidget(QWidget):
    _tabChanged = pyqtSignal(int, name = "tabChanged")

    def __init__(self, subtitleData, videoWidget, parent = None):
        super(SubTabWidget, self).__init__(parent)
        self._subtitleData = subtitleData
        self.__initTabWidget(videoWidget)

    def __initTabWidget(self, videoWidget):
        settings = SubSettings()

        mainLayout = QVBoxLayout(self)
        mainLayout.setContentsMargins(0, 0, 0, 0)
        mainLayout.setSpacing(0)

        #TabBar
        self.tabBar = QTabBar(self)

        # Splitter (bookmarks + pages)
        self.splitter = QSplitter(self)
        self.splitter.setObjectName("sidebar_splitter")

        self._toolbox = ToolBox(self._subtitleData, self)
        self._toolbox.setObjectName("sidebar")
        self._toolbox.setMinimumWidth(100)

        self._toolbox.addTool(Details(self._subtitleData, self))
        self._toolbox.addTool(Synchronizer(videoWidget, self._subtitleData, self))
        self._toolbox.addTool(History(self))

        self.rightWidget = QWidget()
        rightLayout = QGridLayout()
        rightLayout.setContentsMargins(0, 0, 0, 0)
        self.rightWidget.setLayout(rightLayout)

        self._mainTab = FileList(_("Subtitles"), self._subtitleData, self)

        self.pages = QStackedWidget(self)
        rightLayout.addWidget(self.pages, 0, 0)

        self.tabBar.addTab(self._mainTab.name)
        self.pages.addWidget(self._mainTab)

        self.splitter.addWidget(self._toolbox)
        self.splitter.addWidget(self.rightWidget)
        self.__drawSplitterHandle(1)

        # Setting widgets
        mainLayout.addWidget(self.tabBar)
        mainLayout.addWidget(self.splitter)

        # Widgets settings
        self.tabBar.setMovable(True)
        self.tabBar.setTabsClosable(True)
        self.tabBar.setExpanding(False)

        # Don't resize left panel if it's not needed
        leftWidgetIndex = self.splitter.indexOf(self._toolbox)
        rightWidgetIndex = self.splitter.indexOf(self.rightWidget)

        self.splitter.setStretchFactor(leftWidgetIndex, 0)
        self.splitter.setStretchFactor(rightWidgetIndex, 1)
        self.splitter.setCollapsible(leftWidgetIndex, False)
        self.splitter.setSizes([250])

        # Some signals
        self.tabBar.currentChanged.connect(self.showTab)
        self.tabBar.tabCloseRequested.connect(self.closeTab)
        self.tabBar.tabMoved.connect(self.moveTab)
        self._mainTab.requestOpen.connect(self.openTab)
        self._mainTab.requestRemove.connect(self.removeFile)

        self.tabChanged.connect(lambda i: self._toolbox.setContentFor(self.tab(i)))

        self.setLayout(mainLayout)

    def __addTab(self, filePath):
        """Returns existing tab index. Creates a new one if it isn't opened and returns its index
        otherwise."""
        for i in range(self.tabBar.count()):
            widget = self.pages.widget(i)
            if not widget.isStatic and filePath == widget.filePath:
                return i
        tab = SubtitleEditor(filePath, self._subtitleData, self)
        newIndex = self.tabBar.addTab(self._createTabName(tab.name, tab.history.isClean()))
        tab.history.cleanChanged.connect(
            lambda clean: self._cleanStateForFileChanged(filePath, clean))
        self.pages.addWidget(tab)
        return newIndex

    def __drawSplitterHandle(self, index):
        splitterHandle = self.splitter.handle(index)

        splitterLayout = QVBoxLayout(splitterHandle)
        splitterLayout.setSpacing(0)
        splitterLayout.setContentsMargins(0, 0, 0, 0)

        line = QFrame(splitterHandle)
        line.setFrameShape(QFrame.HLine)
        line.setFrameShadow(QFrame.Sunken)
        splitterLayout.addWidget(line)
        splitterHandle.setLayout(splitterLayout)

    def _createTabName(self, name, cleanState):
        if cleanState is True:
            return name
        else:
            return "%s +" % name

    def _cleanStateForFileChanged(self, filePath, cleanState):
        page = self.tabByPath(filePath)
        if page is not None:
            for i in range(self.tabBar.count()):
                if self.tabBar.tabText(i)[:len(page.name)] == page.name:
                    self.tabBar.setTabText(i, self._createTabName(page.name, cleanState))
                    return

    def saveWidgetState(self, settings):
        settings.setState(self.splitter, self.splitter.saveState())
        settings.setHidden(self._toolbox, self._toolbox.isHidden())

    def restoreWidgetState(self, settings):
        self.showPanel(not settings.getHidden(self._toolbox))

        splitterState = settings.getState(self.splitter)
        if not splitterState.isEmpty():
            self.splitter.restoreState(settings.getState(self.splitter))

    @pyqtSlot(str, bool)
    def openTab(self, filePath, background=False):
        if self._subtitleData.fileExists(filePath):
            tabIndex = self.__addTab(filePath)
            if background is False:
                self.showTab(tabIndex)
        else:
            log.error(_("SubtitleEditor not created for %s!" % filePath))

    @pyqtSlot(str)
    def removeFile(self, filePath):
        tab = self.tabByPath(filePath)
        command = RemoveFile(filePath)
        if tab is not None:
            index = self.pages.indexOf(tab)
            if self.closeTab(index):
                self._subtitleData.execute(command)
        else:
            self._subtitleData.execute(command)


    @pyqtSlot(int)
    def closeTab(self, index):
        tab = self.tab(index)
        if tab.canClose():
            widgetToRemove = self.pages.widget(index)
            self.tabBar.removeTab(index)
            self.pages.removeWidget(widgetToRemove)
            widgetToRemove.deleteLater()
            return True
        return False


    def count(self):
        return self.tabBar.count()

    def currentIndex(self):
        return self.tabBar.currentIndex()

    def currentPage(self):
        return self.pages.currentWidget()

    @pyqtSlot(int, int)
    def moveTab(self, fromIndex, toIndex):
        fromWidget = self.pages.widget(fromIndex)
        toWidget = self.pages.widget(toIndex)
        if fromWidget.isStatic or toWidget.isStatic:
            self.tabBar.blockSignals(True) # signals would cause infinite recursion
            self.tabBar.moveTab(toIndex, fromIndex)
            self.tabBar.blockSignals(False)
            return
        else:
            self.pages.removeWidget(fromWidget)
            self.pages.removeWidget(toWidget)

            if fromIndex < toIndex:
                self.pages.insertWidget(fromIndex, toWidget)
                self.pages.insertWidget(toIndex, fromWidget)
            else:
                self.pages.insertWidget(toIndex, fromWidget)
                self.pages.insertWidget(fromIndex, toWidget)

            # Hack
            # Qt changes tabs during mouse drag and dropping. The next line is added
            # to prevent it.
            self.showTab(self.tabBar.currentIndex())

    @pyqtSlot(int)
    def showTab(self, index):
        showWidget = self.pages.widget(index)
        if showWidget:
            self.pages.setCurrentWidget(showWidget)
            self.tabBar.blockSignals(True)
            self.tabBar.setCurrentIndex(index)
            self.tabBar.blockSignals(False)

            # Try to update current tab.
            showWidget.updateTab()

            self._tabChanged.emit(index)

    def showPanel(self, val):
        if val is True:
            self._toolbox.show()
        else:
            self._toolbox.hide()

    def togglePanel(self):
        if self._toolbox.isHidden():
            self._toolbox.show()
        else:
            self._toolbox.hide()

    def tab(self, index):
        return self.pages.widget(index)

    def tabByPath(self, path):
        for i in range(self.pages.count()):
            page = self.tab(i)
            if not page.isStatic and page.filePath == path:
                return page
        return None

    @property
    def fileList(self):
        return self._mainTab
Exemple #6
0
class E5SideBar(QWidget):
    """
    Class implementing a sidebar with a widget area, that is hidden or shown,
    if the current tab is clicked again.
    """
    Version = 1
    
    North = 0
    East = 1
    South = 2
    West = 3
    
    def __init__(self, orientation=None, delay=200, parent=None):
        """
        Constructor
        
        @param orientation orientation of the sidebar widget (North, East,
            South, West)
        @param delay value for the expand/shrink delay in milliseconds
            (integer)
        @param parent parent widget (QWidget)
        """
        super(E5SideBar, self).__init__(parent)
        
        self.__tabBar = QTabBar()
        self.__tabBar.setDrawBase(True)
        self.__tabBar.setShape(QTabBar.RoundedNorth)
        self.__tabBar.setUsesScrollButtons(True)
        self.__tabBar.setDrawBase(False)
        self.__stackedWidget = QStackedWidget(self)
        self.__stackedWidget.setContentsMargins(0, 0, 0, 0)
        self.__autoHideButton = QToolButton()
        self.__autoHideButton.setCheckable(True)
        self.__autoHideButton.setIcon(
            UI.PixmapCache.getIcon("autoHideOff.png"))
        self.__autoHideButton.setChecked(True)
        self.__autoHideButton.setToolTip(
            self.tr("Deselect to activate automatic collapsing"))
        self.barLayout = QBoxLayout(QBoxLayout.LeftToRight)
        self.barLayout.setContentsMargins(0, 0, 0, 0)
        self.layout = QBoxLayout(QBoxLayout.TopToBottom)
        self.layout.setContentsMargins(0, 0, 0, 0)
        self.layout.setSpacing(0)
        self.barLayout.addWidget(self.__autoHideButton)
        self.barLayout.addWidget(self.__tabBar)
        self.layout.addLayout(self.barLayout)
        self.layout.addWidget(self.__stackedWidget)
        self.setLayout(self.layout)
        
        # initialize the delay timer
        self.__actionMethod = None
        self.__delayTimer = QTimer(self)
        self.__delayTimer.setSingleShot(True)
        self.__delayTimer.setInterval(delay)
        self.__delayTimer.timeout.connect(self.__delayedAction)
        
        self.__minimized = False
        self.__minSize = 0
        self.__maxSize = 0
        self.__bigSize = QSize()
        
        self.splitter = None
        self.splitterSizes = []
        
        self.__hasFocus = False
        # flag storing if this widget or any child has the focus
        self.__autoHide = False
        
        self.__tabBar.installEventFilter(self)
        
        self.__orientation = E5SideBar.North
        if orientation is None:
            orientation = E5SideBar.North
        self.setOrientation(orientation)
        
        self.__tabBar.currentChanged[int].connect(
            self.__stackedWidget.setCurrentIndex)
        e5App().focusChanged[QWidget, QWidget].connect(self.__appFocusChanged)
        self.__autoHideButton.toggled[bool].connect(self.__autoHideToggled)
    
    def setSplitter(self, splitter):
        """
        Public method to set the splitter managing the sidebar.
        
        @param splitter reference to the splitter (QSplitter)
        """
        self.splitter = splitter
        self.splitter.splitterMoved.connect(self.__splitterMoved)
        self.splitter.setChildrenCollapsible(False)
        index = self.splitter.indexOf(self)
        self.splitter.setCollapsible(index, False)
    
    def __splitterMoved(self, pos, index):
        """
        Private slot to react on splitter moves.
        
        @param pos new position of the splitter handle (integer)
        @param index index of the splitter handle (integer)
        """
        if self.splitter:
            self.splitterSizes = self.splitter.sizes()
    
    def __delayedAction(self):
        """
        Private slot to handle the firing of the delay timer.
        """
        if self.__actionMethod is not None:
            self.__actionMethod()
    
    def setDelay(self, delay):
        """
        Public method to set the delay value for the expand/shrink delay in
        milliseconds.
        
        @param delay value for the expand/shrink delay in milliseconds
            (integer)
        """
        self.__delayTimer.setInterval(delay)
    
    def delay(self):
        """
        Public method to get the delay value for the expand/shrink delay in
        milliseconds.
        
        @return value for the expand/shrink delay in milliseconds (integer)
        """
        return self.__delayTimer.interval()
    
    def __cancelDelayTimer(self):
        """
        Private method to cancel the current delay timer.
        """
        self.__delayTimer.stop()
        self.__actionMethod = None
    
    def shrink(self):
        """
        Public method to record a shrink request.
        """
        self.__delayTimer.stop()
        self.__actionMethod = self.__shrinkIt
        self.__delayTimer.start()
   
    def __shrinkIt(self):
        """
        Private method to shrink the sidebar.
        """
        self.__minimized = True
        self.__bigSize = self.size()
        if self.__orientation in [E5SideBar.North, E5SideBar.South]:
            self.__minSize = self.minimumSizeHint().height()
            self.__maxSize = self.maximumHeight()
        else:
            self.__minSize = self.minimumSizeHint().width()
            self.__maxSize = self.maximumWidth()
        if self.splitter:
            self.splitterSizes = self.splitter.sizes()
        
        self.__stackedWidget.hide()
        
        if self.__orientation in [E5SideBar.North, E5SideBar.South]:
            self.setFixedHeight(self.__tabBar.minimumSizeHint().height())
        else:
            self.setFixedWidth(self.__tabBar.minimumSizeHint().width())
        
        self.__actionMethod = None
    
    def expand(self):
        """
        Public method to record a expand request.
        """
        self.__delayTimer.stop()
        self.__actionMethod = self.__expandIt
        self.__delayTimer.start()
    
    def __expandIt(self):
        """
        Private method to expand the sidebar.
        """
        self.__minimized = False
        self.__stackedWidget.show()
        self.resize(self.__bigSize)
        if self.__orientation in [E5SideBar.North, E5SideBar.South]:
            minSize = max(self.__minSize, self.minimumSizeHint().height())
            self.setMinimumHeight(minSize)
            self.setMaximumHeight(self.__maxSize)
        else:
            minSize = max(self.__minSize, self.minimumSizeHint().width())
            self.setMinimumWidth(minSize)
            self.setMaximumWidth(self.__maxSize)
        if self.splitter:
            self.splitter.setSizes(self.splitterSizes)
        
        self.__actionMethod = None
    
    def isMinimized(self):
        """
        Public method to check the minimized state.
        
        @return flag indicating the minimized state (boolean)
        """
        return self.__minimized
    
    def isAutoHiding(self):
        """
        Public method to check, if the auto hide function is active.
        
        @return flag indicating the state of auto hiding (boolean)
        """
        return self.__autoHide
    
    def eventFilter(self, obj, evt):
        """
        Public method to handle some events for the tabbar.
        
        @param obj reference to the object (QObject)
        @param evt reference to the event object (QEvent)
        @return flag indicating, if the event was handled (boolean)
        """
        if obj == self.__tabBar:
            if evt.type() == QEvent.MouseButtonPress:
                pos = evt.pos()
                for i in range(self.__tabBar.count()):
                    if self.__tabBar.tabRect(i).contains(pos):
                        break
                
                if i == self.__tabBar.currentIndex():
                    if self.isMinimized():
                        self.expand()
                    else:
                        self.shrink()
                    return True
                elif self.isMinimized():
                    self.expand()
            elif evt.type() == QEvent.Wheel:
                if qVersion() >= "5.0.0":
                    delta = evt.angleDelta().y()
                else:
                    delta = evt.delta()
                if delta > 0:
                    self.prevTab()
                else:
                    self.nextTab()
                return True
        
        return QWidget.eventFilter(self, obj, evt)
    
    def addTab(self, widget, iconOrLabel, label=None):
        """
        Public method to add a tab to the sidebar.
        
        @param widget reference to the widget to add (QWidget)
        @param iconOrLabel reference to the icon or the label text of the tab
            (QIcon, string)
        @param label the labeltext of the tab (string) (only to be
            used, if the second parameter is a QIcon)
        """
        if label:
            index = self.__tabBar.addTab(iconOrLabel, label)
            self.__tabBar.setTabToolTip(index, label)
        else:
            index = self.__tabBar.addTab(iconOrLabel)
            self.__tabBar.setTabToolTip(index, iconOrLabel)
        self.__stackedWidget.addWidget(widget)
        if self.__orientation in [E5SideBar.North, E5SideBar.South]:
            self.__minSize = self.minimumSizeHint().height()
        else:
            self.__minSize = self.minimumSizeHint().width()
    
    def insertTab(self, index, widget, iconOrLabel, label=None):
        """
        Public method to insert a tab into the sidebar.
        
        @param index the index to insert the tab at (integer)
        @param widget reference to the widget to insert (QWidget)
        @param iconOrLabel reference to the icon or the labeltext of the tab
            (QIcon, string)
        @param label the labeltext of the tab (string) (only to be
            used, if the second parameter is a QIcon)
        """
        if label:
            index = self.__tabBar.insertTab(index, iconOrLabel, label)
            self.__tabBar.setTabToolTip(index, label)
        else:
            index = self.__tabBar.insertTab(index, iconOrLabel)
            self.__tabBar.setTabToolTip(index, iconOrLabel)
        self.__stackedWidget.insertWidget(index, widget)
        if self.__orientation in [E5SideBar.North, E5SideBar.South]:
            self.__minSize = self.minimumSizeHint().height()
        else:
            self.__minSize = self.minimumSizeHint().width()
    
    def removeTab(self, index):
        """
        Public method to remove a tab.
        
        @param index the index of the tab to remove (integer)
        """
        self.__stackedWidget.removeWidget(self.__stackedWidget.widget(index))
        self.__tabBar.removeTab(index)
        if self.__orientation in [E5SideBar.North, E5SideBar.South]:
            self.__minSize = self.minimumSizeHint().height()
        else:
            self.__minSize = self.minimumSizeHint().width()
    
    def clear(self):
        """
        Public method to remove all tabs.
        """
        while self.count() > 0:
            self.removeTab(0)
    
    def prevTab(self):
        """
        Public slot used to show the previous tab.
        """
        ind = self.currentIndex() - 1
        if ind == -1:
            ind = self.count() - 1
            
        self.setCurrentIndex(ind)
        self.currentWidget().setFocus()
    
    def nextTab(self):
        """
        Public slot used to show the next tab.
        """
        ind = self.currentIndex() + 1
        if ind == self.count():
            ind = 0
            
        self.setCurrentIndex(ind)
        self.currentWidget().setFocus()
    
    def count(self):
        """
        Public method to get the number of tabs.
        
        @return number of tabs in the sidebar (integer)
        """
        return self.__tabBar.count()
    
    def currentIndex(self):
        """
        Public method to get the index of the current tab.
        
        @return index of the current tab (integer)
        """
        return self.__stackedWidget.currentIndex()
    
    def setCurrentIndex(self, index):
        """
        Public slot to set the current index.
        
        @param index the index to set as the current index (integer)
        """
        self.__tabBar.setCurrentIndex(index)
        self.__stackedWidget.setCurrentIndex(index)
        if self.isMinimized():
            self.expand()
    
    def currentWidget(self):
        """
        Public method to get a reference to the current widget.
        
        @return reference to the current widget (QWidget)
        """
        return self.__stackedWidget.currentWidget()
    
    def setCurrentWidget(self, widget):
        """
        Public slot to set the current widget.
        
        @param widget reference to the widget to become the current widget
            (QWidget)
        """
        self.__stackedWidget.setCurrentWidget(widget)
        self.__tabBar.setCurrentIndex(self.__stackedWidget.currentIndex())
        if self.isMinimized():
            self.expand()
    
    def indexOf(self, widget):
        """
        Public method to get the index of the given widget.
        
        @param widget reference to the widget to get the index of (QWidget)
        @return index of the given widget (integer)
        """
        return self.__stackedWidget.indexOf(widget)
    
    def isTabEnabled(self, index):
        """
        Public method to check, if a tab is enabled.
        
        @param index index of the tab to check (integer)
        @return flag indicating the enabled state (boolean)
        """
        return self.__tabBar.isTabEnabled(index)
    
    def setTabEnabled(self, index, enabled):
        """
        Public method to set the enabled state of a tab.
        
        @param index index of the tab to set (integer)
        @param enabled enabled state to set (boolean)
        """
        self.__tabBar.setTabEnabled(index, enabled)
    
    def orientation(self):
        """
        Public method to get the orientation of the sidebar.
        
        @return orientation of the sidebar (North, East, South, West)
        """
        return self.__orientation
    
    def setOrientation(self, orient):
        """
        Public method to set the orientation of the sidebar.

        @param orient orientation of the sidebar (North, East, South, West)
        """
        if orient == E5SideBar.North:
            self.__tabBar.setShape(QTabBar.RoundedNorth)
            self.__tabBar.setSizePolicy(
                QSizePolicy.Expanding, QSizePolicy.Preferred)
            self.barLayout.setDirection(QBoxLayout.LeftToRight)
            self.layout.setDirection(QBoxLayout.TopToBottom)
            self.layout.setAlignment(self.barLayout, Qt.AlignLeft)
        elif orient == E5SideBar.East:
            self.__tabBar.setShape(QTabBar.RoundedEast)
            self.__tabBar.setSizePolicy(
                QSizePolicy.Preferred, QSizePolicy.Expanding)
            self.barLayout.setDirection(QBoxLayout.TopToBottom)
            self.layout.setDirection(QBoxLayout.RightToLeft)
            self.layout.setAlignment(self.barLayout, Qt.AlignTop)
        elif orient == E5SideBar.South:
            self.__tabBar.setShape(QTabBar.RoundedSouth)
            self.__tabBar.setSizePolicy(
                QSizePolicy.Expanding, QSizePolicy.Preferred)
            self.barLayout.setDirection(QBoxLayout.LeftToRight)
            self.layout.setDirection(QBoxLayout.BottomToTop)
            self.layout.setAlignment(self.barLayout, Qt.AlignLeft)
        elif orient == E5SideBar.West:
            self.__tabBar.setShape(QTabBar.RoundedWest)
            self.__tabBar.setSizePolicy(
                QSizePolicy.Preferred, QSizePolicy.Expanding)
            self.barLayout.setDirection(QBoxLayout.TopToBottom)
            self.layout.setDirection(QBoxLayout.LeftToRight)
            self.layout.setAlignment(self.barLayout, Qt.AlignTop)
        self.__orientation = orient
    
    def tabIcon(self, index):
        """
        Public method to get the icon of a tab.
        
        @param index index of the tab (integer)
        @return icon of the tab (QIcon)
        """
        return self.__tabBar.tabIcon(index)
    
    def setTabIcon(self, index, icon):
        """
        Public method to set the icon of a tab.
        
        @param index index of the tab (integer)
        @param icon icon to be set (QIcon)
        """
        self.__tabBar.setTabIcon(index, icon)
    
    def tabText(self, index):
        """
        Public method to get the text of a tab.
        
        @param index index of the tab (integer)
        @return text of the tab (string)
        """
        return self.__tabBar.tabText(index)
    
    def setTabText(self, index, text):
        """
        Public method to set the text of a tab.
        
        @param index index of the tab (integer)
        @param text text to set (string)
        """
        self.__tabBar.setTabText(index, text)
    
    def tabToolTip(self, index):
        """
        Public method to get the tooltip text of a tab.
        
        @param index index of the tab (integer)
        @return tooltip text of the tab (string)
        """
        return self.__tabBar.tabToolTip(index)
    
    def setTabToolTip(self, index, tip):
        """
        Public method to set the tooltip text of a tab.
        
        @param index index of the tab (integer)
        @param tip tooltip text to set (string)
        """
        self.__tabBar.setTabToolTip(index, tip)
    
    def tabWhatsThis(self, index):
        """
        Public method to get the WhatsThis text of a tab.
        
        @param index index of the tab (integer)
        @return WhatsThis text of the tab (string)
        """
        return self.__tabBar.tabWhatsThis(index)
    
    def setTabWhatsThis(self, index, text):
        """
        Public method to set the WhatsThis text of a tab.
        
        @param index index of the tab (integer)
        @param text WhatsThis text to set (string)
        """
        self.__tabBar.setTabWhatsThis(index, text)
    
    def widget(self, index):
        """
        Public method to get a reference to the widget associated with a tab.
        
        @param index index of the tab (integer)
        @return reference to the widget (QWidget)
        """
        return self.__stackedWidget.widget(index)
    
    def saveState(self):
        """
        Public method to save the state of the sidebar.
        
        @return saved state as a byte array (QByteArray)
        """
        if len(self.splitterSizes) == 0:
            if self.splitter:
                self.splitterSizes = self.splitter.sizes()
            self.__bigSize = self.size()
            if self.__orientation in [E5SideBar.North, E5SideBar.South]:
                self.__minSize = self.minimumSizeHint().height()
                self.__maxSize = self.maximumHeight()
            else:
                self.__minSize = self.minimumSizeHint().width()
                self.__maxSize = self.maximumWidth()
        
        data = QByteArray()
        stream = QDataStream(data, QIODevice.WriteOnly)
        stream.setVersion(QDataStream.Qt_4_6)
        
        stream.writeUInt16(self.Version)
        stream.writeBool(self.__minimized)
        stream << self.__bigSize
        stream.writeUInt16(self.__minSize)
        stream.writeUInt16(self.__maxSize)
        stream.writeUInt16(len(self.splitterSizes))
        for size in self.splitterSizes:
            stream.writeUInt16(size)
        stream.writeBool(self.__autoHide)
        
        return data
    
    def restoreState(self, state):
        """
        Public method to restore the state of the sidebar.
        
        @param state byte array containing the saved state (QByteArray)
        @return flag indicating success (boolean)
        """
        if state.isEmpty():
            return False
        
        if self.__orientation in [E5SideBar.North, E5SideBar.South]:
            minSize = self.layout.minimumSize().height()
            maxSize = self.maximumHeight()
        else:
            minSize = self.layout.minimumSize().width()
            maxSize = self.maximumWidth()
        
        data = QByteArray(state)
        stream = QDataStream(data, QIODevice.ReadOnly)
        stream.setVersion(QDataStream.Qt_4_6)
        stream.readUInt16()  # version
        minimized = stream.readBool()
        
        if minimized and not self.__minimized:
            self.shrink()
        
        stream >> self.__bigSize
        self.__minSize = max(stream.readUInt16(), minSize)
        self.__maxSize = max(stream.readUInt16(), maxSize)
        count = stream.readUInt16()
        self.splitterSizes = []
        for i in range(count):
            self.splitterSizes.append(stream.readUInt16())
        
        self.__autoHide = stream.readBool()
        self.__autoHideButton.setChecked(not self.__autoHide)
        
        if not minimized:
            self.expand()
        
        return True
    
    #######################################################################
    ## methods below implement the autohide functionality
    #######################################################################
    
    def __autoHideToggled(self, checked):
        """
        Private slot to handle the toggling of the autohide button.
        
        @param checked flag indicating the checked state of the button
            (boolean)
        """
        self.__autoHide = not checked
        if self.__autoHide:
            self.__autoHideButton.setIcon(
                UI.PixmapCache.getIcon("autoHideOn.png"))
        else:
            self.__autoHideButton.setIcon(
                UI.PixmapCache.getIcon("autoHideOff.png"))
    
    def __appFocusChanged(self, old, now):
        """
        Private slot to handle a change of the focus.
        
        @param old reference to the widget, that lost focus (QWidget or None)
        @param now reference to the widget having the focus (QWidget or None)
        """
        self.__hasFocus = self.isAncestorOf(now)
        if self.__autoHide and not self.__hasFocus and not self.isMinimized():
            self.shrink()
        elif self.__autoHide and self.__hasFocus and self.isMinimized():
            self.expand()
    
    def enterEvent(self, event):
        """
        Protected method to handle the mouse entering this widget.
        
        @param event reference to the event (QEvent)
        """
        if self.__autoHide and self.isMinimized():
            self.expand()
        else:
            self.__cancelDelayTimer()
    
    def leaveEvent(self, event):
        """
        Protected method to handle the mouse leaving this widget.
        
        @param event reference to the event (QEvent)
        """
        if self.__autoHide and not self.__hasFocus and not self.isMinimized():
            self.shrink()
        else:
            self.__cancelDelayTimer()
    
    def shutdown(self):
        """
        Public method to shut down the object.
        
        This method does some preparations so the object can be deleted
        properly. It disconnects from the focusChanged signal in order to
        avoid trouble later on.
        """
        e5App().focusChanged[QWidget, QWidget].disconnect(
            self.__appFocusChanged)
Exemple #7
0
class BottomPanel(QWidget):
    def __init__(self, app, parent=None):
        super().__init__(parent)
        self._app = app

        self._layout = QHBoxLayout(self)
        self.back_btn = ToolbarButton('⇦', self)
        self.forward_btn = ToolbarButton('⇨', self)
        self.magicbox = MagicBox(self._app)

        self._stack_switch = ToolbarButton('⁐', self)
        self._stacked_widget = QStackedWidget(self)
        self._stacked_widget.addWidget(self.magicbox)
        self._stack_switch.hide()

        self.status_line = StatusLine(self._app)
        self.settings_btn = ToolbarButton('⋮', self)
        self.settings_btn.setToolTip('配置')

        # initialize widgets
        self.status_line.add_item(StatusLineItem('plugin', PluginStatus(self._app)))
        self.status_line.add_item(StatusLineItem('notify', NotifyStatus(self._app)))
        self.back_btn.setEnabled(False)
        self.forward_btn.setEnabled(False)

        self.back_btn.clicked.connect(self._app.browser.back)
        self.forward_btn.clicked.connect(self._app.browser.forward)
        self._stack_switch.clicked.connect(self._show_next_stacked_widget)

        self._setup_ui()

    def _setup_ui(self):
        self.setObjectName('bottom_panel')

        self._layout.addSpacing(5)
        self._layout.addWidget(self.back_btn)
        self._layout.addSpacing(5)
        self._layout.addWidget(self.forward_btn)
        self._layout.addSpacing(80)
        self._layout.addWidget(self._stacked_widget)
        self._layout.addSpacing(10)
        self._layout.addWidget(self._stack_switch)
        # self._layout.addStretch(0)
        self._layout.addSpacing(80)
        self._layout.addWidget(self.status_line)
        self._layout.addWidget(self.settings_btn)

        # assume the magicbox height is about 30
        h_margin, v_margin = 5, 10
        height = self.magicbox.height()

        self.setFixedHeight(height + v_margin * 2 + 10)
        self._layout.setContentsMargins(h_margin, v_margin, h_margin, v_margin)
        self._layout.setSpacing(0)

    def _show_next_stacked_widget(self):
        current_index = self._stacked_widget.currentIndex()
        if current_index < self._stacked_widget.count() - 1:
            next_index = current_index + 1
        else:
            next_index = 0
        self._stacked_widget.setCurrentIndex(next_index)

    def add_stacked_widget(self, widget):
        """

        .. versionadded:: 3.7.10
        """
        self._stacked_widget.addWidget(widget)
        if self._stacked_widget.count() > 1:
            self._stack_switch.show()

    def set_top_stacked_widget(self, widget):
        """

        .. versionadded:: 3.7.10
        """
        self._stacked_widget.setCurrentWidget(widget)

    def clear_stacked_widget(self):
        """

        .. versionadded:: 3.7.10
        """
        while self._stacked_widget.count() > 0:
            self._stacked_widget.removeWidget(self._stacked_widget.currentWidget())
        self._stack_switch.hide()

    def show_and_focus_magicbox(self):
        """Show and focus magicbox

        .. versionadded:: 3.7.10
        """
        if self._stacked_widget.indexOf(self.magicbox) != -1:
            self.set_top_stacked_widget(self.magicbox)
            self.magicbox.setFocus()
class TabWidget(TabStackedWidget):
    def __init__(self, window, parent=None):
        super(TabWidget, self).__init__(parent)

        self._window = None  # BrowserWindow
        self._tabBar = None  # TabBar
        self._locationBars = None  # QStackedWidget
        self._closedTabsManager = None  # ClosedTabsManager

        self._menuTabs = None  # MenuTabs
        self._buttonListTabs = None  # ToolButton
        self._menuClosedTabs = None  # QMenu
        self._buttonClosedTabs = None  # ToolButton
        self._buttonAddTab = None  # AddTabButton
        self._buttonAddTab2 = None  # AddTabButton

        self._lastBackgroundTab = None  # QPoint<WebTab>

        self._dontCloseWithOneTab = False
        self._showClosedTabsButton = False
        self._newTabAfterActive = False
        self._newEmptyTabAfterActive = False
        self._urlOnNewTab = QUrl()  # QUrl

        self._currentTabFresh = False
        self._blockTabMovedSignal = False

        # start init
        self._window = window
        self._locationBars = QStackedWidget()
        self._closedTabsManager = ClosedTabsManager()

        self.setObjectName('tabWidget')
        self._tabBar = TabBar(self._window, self)
        self.setTabBar(self._tabBar)

        self.changed.connect(gVar.app.changeOccurred)
        self.pinStateChanged.connect(self.changed)

        self._tabBar.tabCloseRequested.connect(self.requestCloseTab)
        self._tabBar.tabMoved.connect(self._tabWasMoved)

        self._tabBar.moveAddTabButton.connect(self.moveAddTabButton)
        gVar.app.settingsReloaded.connect(self._loadSettings)

        self._menuTabs = MenuTabs(self)
        self._menuTabs.closeTab.connect(self.requestCloseTab)

        self._menuClosedTabs = QMenu(self)

        # AddTab button displayed next to last tab
        self._buttonAddTab = AddTabButton(self, self._tabBar)
        self._buttonAddTab.setProperty('outside-tabbar', False)
        self._buttonAddTab.clicked.connect(self._window.addTab)

        # AddTab button displayed ouside tabbar (as corner widget)
        self._buttonAddTab2 = AddTabButton(self, self._tabBar)
        self._buttonAddTab2.setProperty('outside-tabbar', True)
        self._buttonAddTab2.hide()
        self._buttonAddTab2.clicked.connect(self._window.addTab)

        # ClosedTabs button displayed as a permanet corner widget
        self._buttonClosedTabs = ToolButton(self._tabBar)
        self._buttonClosedTabs.setObjectName('tabwidget-button-closedtabs')
        self._buttonClosedTabs.setMenu(self._menuClosedTabs)
        self._buttonClosedTabs.setPopupMode(QToolButton.InstantPopup)
        self._buttonClosedTabs.setToolTip('Closed tabs')
        self._buttonClosedTabs.setAutoRaise(True)
        self._buttonClosedTabs.setFocusPolicy(Qt.NoFocus)
        self._buttonClosedTabs.setShowMenuInside(True)
        self._buttonClosedTabs.aboutToShowMenu.connect(
            self._aboutToShowClosedTabsMenu)

        # ListTabs button is showed only when tabbar overflows
        self._buttonListTabs = ToolButton(self._tabBar)
        self._buttonListTabs.setObjectName('tabwidget-button-opentabs')
        self._buttonListTabs.setMenu(self._menuTabs)
        self._buttonListTabs.setPopupMode(QToolButton.InstantPopup)
        self._buttonListTabs.setToolTip('List of tabs')
        self._buttonListTabs.setAutoRaise(True)
        self._buttonListTabs.setFocusPolicy(Qt.NoFocus)
        self._buttonListTabs.setShowMenuInside(True)
        self._buttonListTabs.hide()
        self._buttonListTabs.aboutToShowMenu.connect(self._aboutToShowTabsMenu)

        self._tabBar.addCornerWidget(self._buttonAddTab2, Qt.TopRightCorner)
        self._tabBar.addCornerWidget(self._buttonClosedTabs, Qt.TopRightCorner)
        self._tabBar.addCornerWidget(self._buttonListTabs, Qt.TopRightCorner)
        self._tabBar.overFlowChanged.connect(self.tabBarOverFlowChanged)

        self._loadSettings()

    def browserWindow(self):
        return self._window

    def restoreState(self, tabs, currentTab):
        '''
        @param: tabs QVector<WebTab::SavedTab>
        '''
        if not tabs:
            return False

        childTabs = []  # QVector<QPair<WebTab*, QVector<int>>>

        for tab in tabs:
            index = self.addViewByUrl(QUrl(), const.NT_CleanSelectedTab, False,
                                      tab.isPinned)
            webTab = self._weTab(index)
            webTab.restoreTab(tab)
            if tab.childTabs:
                childTabs.append([webTab, tab.childTabs])

        for webTab, tabs in childTabs:
            for index in tabs:
                t = self._weTab(index)
                if t:
                    webTab.addChildTab(t)

        self.setCurrentIndex(currentTab)
        QTimer.singleShot(0, self._tabBar.ensureVisible)

        self._weTab().tabActivated()

        return True

    def setCurrentIndex(self, index):
        super(TabWidget, self).setCurrentIndex(index)

    def nextTab(self):
        index = (self.currentIndex() + 1) % self.count()
        self.setCurrentIndex(index)

    def previousTab(self):
        if self.currentIndex() == 0:
            index = self.count() - 1
        else:
            index = self.currentIndex() - 1
        self.setCurrentIndex(index)

    def currentTabChanged(self, index):
        if not self._validIndex(index):
            return

        self._lastBackgroundTab = None
        self._currentTabFresh = False

        webTab = self._weTab(index)
        webTab.tabActivated()

        locBar = webTab.locationBar()

        if locBar and self._locationBars.indexOf(locBar) != -1:
            self._locationBars.setCurrentWidget(locBar)

        self._window.currentTabChanged()

        self.changed.emit()

    def normalTabsCount(self):
        return self._tabBar.normalTabsCount()

    def pinnedTabsCount(self):
        return self._tabBar.pinnedTabsCount()

    def extraReservedWidth(self):
        return self._buttonAddTab.width()

    def webTab(self, index=-1):
        '''
        @return: WebTab
        '''
        if index < 0:
            return self._weTab()
        else:
            return self._weTab(index)

    def tabBar(self):
        '''
        @return: TabBar
        '''
        return self._tabBar

    def closedTabsManager(self):
        '''
        @return: ClosedTabsManager
        '''
        return self._closedTabsManager

    def allTabs(self, withPinned=True):
        '''
        @return: QList<WebTab*>
        '''
        allTabs = []

        for idx in range(self.count()):
            tab = self._weTab(idx)
            if not tab or (not withPinned and tab.isPinned()):
                continue
            allTabs.append(tab)

        return allTabs

    def canRestoreTab(self):
        return self._closedTabsManager.isClosedTabAvailable()

    def isCurrentTabFresh(self):
        return self._currentTabFresh

    def setCurrentTabFresh(self, currentTabFresh):
        self._currentTabFresh = currentTabFresh

    def locationBars(self):
        '''
        @return: QStackedWidget
        '''
        return self._locationBars

    def buttonClosedTabs(self):
        '''
        @return: ToolButton
        '''
        return self._buttonClosedTabs

    def buttonAddTab(self):
        '''
        @return: AddTabButton
        '''
        return self._buttonAddTab

    def moveTab(self, from_, to_):
        if not self._validIndex(to_) or from_ == to_:
            return
        tab = self.webTab(from_)
        if not tab:
            return
        self._blockTabMovedSignal = True
        # (Un)pin tab when needed
        if (tab.isPinned() and to_ >= self.pinnedTabsCount()) or \
                (not tab.isPinned() and to_ < self.pinnedTabsCount()):
            tab.togglePinned()
        super(TabWidget, self).moveTab(from_, to_)
        self._blockTabMovedSignal = False
        self.tabMoved.emit(from_, to_)

    def pinUnPinTab(self, index, title=''):
        newIndex = super(TabWidget, self).pinUnPinTab(index, title)
        if index != newIndex and not self._blockTabMovedSignal:
            self.tabMoved.emit(index, newIndex)
        return newIndex

    def detachTab(self, tab):
        '''
        @param: tab WebTab
        '''
        assert (tab)

        if self.count() == 1 and gVar.app.windowCount() == 1:
            return

        self._locationBars.removeWidget(tab.locationBar())
        tab.webView().wantsCloseTab.disconnect(self.closeTab)
        tab.webView().urlChanged.disconnect(self.changed)
        tab.webView().ipChanged.disconnect(self._window.ipLabel().setText)

        index = tab.tabIndex()

        tab.detach()
        tab.setPinned(False)

        self.tabRemoved.emit(index)

        if self.count() == 0:
            self._window.close()

    def addViewByUrl(self, url, openFlags, selectLine=False, pinned=False):
        return self.addViewByReq(LoadRequest(url), openFlags, selectLine,
                                 pinned)

    # Q_SLOTS:
    def addViewByReq(self, req, openFlags, selectLine=False, pinned=False):
        '''
        @param: req LoadRequest
        @param: openFlags Qz::NewTabPositionFlags
        @param: selectLine bool
        @param: pinned bool
        '''
        assert (isinstance(req, LoadRequest))
        return self.addViewByReqTitle(req, '', openFlags, selectLine, -1,
                                      pinned)

    def addViewByUrlTitle(
            self,
            url,
            title="New tab",
            openFlags=const.NT_SelectedTab,  # noqa C901
            selectLine=False,
            position=-1,
            pinned=False):
        return self.addViewByReqTitle(LoadRequest(url), title, openFlags,
                                      selectLine, position, pinned)

    def addViewByReqTitle(
            self,
            req,
            title="New tab",
            openFlags=const.NT_SelectedTab,  # noqa C901
            selectLine=False,
            position=-1,
            pinned=False):
        '''
        @param: req LoadRequest
        @param: title QString
        @param: openFlags Qz::NewTabPositionFlags
        @param: selectLine bool
        @param: position int
        @param: pinned bool
        '''
        if isinstance(req, QUrl):
            req = LoadRequest(req)
        url = req.url()
        self._currentTabFresh = False

        if url.isEmpty() and not (openFlags & const.NT_CleanTab):
            url = self._urlOnNewTab

        openAfterActive = self._newTabAfterActive and not (
            openFlags & const.NT_TabAtTheEnd)

        if openFlags == const.NT_SelectedNewEmptyTab and self._newEmptyTabAfterActive:
            openAfterActive = True

        if openAfterActive and position == -1:
            # If we are opening newBgTab from pinned tab, make sure it won't be
            # openned between other pinned tabs
            if openFlags & const.NT_NotSelectedTab and self._lastBackgroundTab:
                position = self._lastBackgroundTab.tabIndex() + 1
            else:
                position = max(self.currentIndex() + 1,
                               self._tabBar.pinnedTabsCount())

        webTab = WebTab(self._window)
        webTab.setPinned(pinned)
        webTab.locationBar().showUrl(url)
        self._locationBars.addWidget(webTab.locationBar())

        if position == -1:
            insertIndex = self.count()
        else:
            insertIndex = position
        index = self.insertTab(insertIndex, webTab, '', pinned)
        webTab.attach(self._window)

        if title:
            self._tabBar.setTabText(index, title)

        if openFlags & const.NT_SelectedTab:
            self.setCurrentIndex(index)
        else:
            self._lastBackgroundTab = webTab

        webTab.webView().wantsCloseTab.connect(self.closeTab)
        webTab.webView().urlChanged.connect(self.changed)
        webTab.webView().ipChanged.connect(self._window.ipLabel().setText)

        def urlChangedCb(url):
            if url != self._urlOnNewTab:
                self._currentTabFresh = False

        webTab.webView().urlChanged.connect(urlChangedCb)

        if url.isValid() and url != req.url():
            r = LoadRequest(req)
            r.setUrl(url)
            webTab.webView().loadByReq(r)
        elif req.url().isValid():
            webTab.webView().loadByReq(req)

        if selectLine and not self._window.locationBar().text():
            self._window.locationBar().setFocus()

        # Make sure user notice opening new background tabs
        if not (openFlags & const.NT_SelectedTab):
            self._tabBar.ensureVisible(index)

        self.changed.emit()
        self.tabInserted.emit(index)

        return index

    def addViewByTab(self, tab, openFlags):
        '''
        @param: tab WebTab
        @param: openFlags Qz::NewTabPositionFlags
        '''
        return self.insertView(self.count() + 1, tab, openFlags)

    def insertView(self, index, tab, openFlags):
        '''
        @param: index int
        @param: tab WebTab
        @param: openFlags Qz::NewTabPositionFlags
        '''
        self._locationBars.addWidget(tab.locationBar())
        newIndex = self.insertTab(index, tab, '', tab.isPinned())
        tab.attach(self._window)

        if openFlags & const.NT_SelectedTab:
            self.setCurrentIndex(newIndex)
        else:
            self._lastBackgroundTab = tab

        tab.webView().wantsCloseTab.connect(self.closeTab)
        tab.webView().urlChanged.connect(self.changed)
        tab.webView().ipChanged.connect(self._window.ipLabel().setText)

        # Make sure user notice opening new background tabs
        if not (openFlags & const.NT_SelectedTab):
            self._tabBar.ensureVisible(index)

        self.changed.emit()
        self.tabInserted.emit(newIndex)

        return newIndex

    def addTabFromClipboard(self):
        # QString
        selectionClipboard = QApplication.clipboard().text(
            QClipboard.Selection)
        # QUrl
        guessedUrl = QUrl.fromUserInput(selectionClipboard)

        if not guessedUrl.isEmpty():
            self.addViewByUrl(guessedUrl, const.NT_SelectedNewEmptyTab)

    def duplicateTab(self, index):
        if not self._validIndex(index):
            return

        webTab = self._weTab(index)

        index = self.addViewByUrlTitle(QUrl(), webTab.title(),
                                       const.NT_CleanSelectedTab)
        newWebTab = self._weTab(index)
        newWebTab.p_restoreTabByUrl(webTab.url(), webTab.historyData(),
                                    webTab.zoomLevel())

        return index

    def closeTab(self, index=-1):
        '''
        @brief: Force close tab
        '''
        if index == -1:
            index = self.currentIndex()

        webTab = self._weTab(index)
        if not webTab or not self._validIndex(index):
            return

        # This is already handled in requestClosetab
        if self.count() <= 1:
            self.requestCloseTab(index)
            return

        self._closedTabsManager.saveTab(webTab)

        webView = webTab.webView()
        self._locationBars.removeWidget(webView.webTab().locationBar())
        webView.wantsCloseTab.disconnect(self.closeTab)
        webView.urlChanged.disconnect(self.changed)
        webView.ipChanged.disconnect(self._window.ipLabel().setText)

        self._lastBackgroundTab = None

        webTab.detach()
        webTab.deleteLater()

        self._updateClosedTabsButton()

        self.changed.emit()
        self.tabRemoved.emit(index)

    def requestCloseTab(self, index=-1):
        '''
        @brief: Request close tab (may be rejected)
        '''
        if index == -1:
            index = self.currentIndex()

        webTab = self._weTab(index)
        if not webTab or not self._validIndex(index):
            return

        webView = webTab.webView()

        # This would close last tab, so we close the window instead
        if self.count() <= 1:
            # If we are not closing window upon closing last tab, let's just
            # load new-tab-url
            if self._dontCloseWithOneTab:
                # We don't want to accumulate more than one closed tab, if user
                # tries to close the last tab multiple times
                if webView.url() != self._urlOnNewTab:
                    self._closedTabsManager.saveTab(webTab)
                webView.zoomReset()
                webView.load(self._urlOnNewTab)
                return
            self._window.close()
            return

        webView.triggerPageAction(QWebEnginePage.RequestClose)

    def reloadTab(self, index):
        if not self._validIndex(index):
            return

        self._weTab(index).reload()

    def reloadAllTabs(self):
        for idx in range(self.count()):
            self.reloadTab(idx)

    def stopTab(self, index):
        if not self._validIndex(index):
            return

        self._weTab(index).stop()

    def closeAllButCurrent(self, index):
        if not self._validIndex(index):
            return

        akt = self._weTab(index)
        tabs = self.allTabs(False)
        for tab in tabs:
            tabIndex = tab.tabIndex()
            if akt == self.widget(tabIndex):
                continue
            self.requestCloseTab(tabIndex)

    def closeToRight(self, index):
        if not self._validIndex(index):
            return

        tabs = self.allTabs(False)
        for tab in tabs:
            tabIndex = tab.tabIndex()
            if index >= tabIndex:
                continue
            self.requestCloseTab(tabIndex)

    def closeToLeft(self, index):
        if not self._validIndex(index):
            return

        tabs = self.allTabs(False)
        for tab in tabs:
            tabIndex = tab.tabIndex()
            if index <= tabIndex:
                continue
            self.requestCloseTab(tabIndex)

    # NOTICE: detachTab renamed to detachTabByIndex
    def detachTabByIndex(self, index):
        tab = self._weTab(index)
        assert (tab)

        if self.count() == 1 and gVar.app.windowCount() == 1:
            return

        self.detachTab(tab)

        # BrowserWindow* window
        window = gVar.app.createWindow(const.BW_NewWindow)
        window.setStartTab(tab)

    def loadTab(self, index):
        if not self._validIndex(index):
            return

        self._weTab(index).tabActivated()

    def unloadTab(self, index):
        if not self._validIndex(index):
            return

        self._weTab(index).unload()

    def restoreClosedTab(self, obj=0):
        '''
        @param: obj QObject
        '''
        if not obj:
            obj = self.sender()

        if not self._closedTabsManager.isClosedTabAvailable():
            return

        tab = ClosedTabsManager.Tab()

        action = obj
        if isinstance(action, QAction) and action.data():
            tab = self._closedTabsManager.takeTabAt(action.data())
        else:
            tab = self._closedTabsManager.takeLastClosedTab()

        if tab.position < 0:
            return

        index = self.addViewByUrlTitle(QUrl(), tab.tabState.title,
                                       const.NT_CleanSelectedTab, False,
                                       tab.position)
        webTab = self._weTab(index)
        webTab.setParentTab(tab.parentTab)
        webTab.p_restoreTab(tab.tabState)

        self._updateClosedTabsButton()

    def restoreAllClosedTabs(self):
        if not self._closedTabsManager.isClosedTabAvailable():
            return

        closedTabs = self._closedTabsManager.closedTabs()
        for tab in closedTabs:
            index = self.addViewByUrl(QUrl(), tab.tabState.title,
                                      const.NT_CleanSelectedTab)
            webTab = self._weTab(index)
            webTab.setParentTab(tab.parentTab)
            webTab.p_restoreTab(tab.tabState)

        self.clearClosedTabsList()

    def clearClosedTabsList(self):
        self._closedTabsManager.clearClosedTabs()
        self._updateClosedTabsButton()

    def moveAddTabButton(self, posX):
        posY = (self._tabBar.height() - self._buttonAddTab.height()) / 2

        if QApplication.layoutDirection() == Qt.RightToLeft:
            posX = max(posX - self._buttonAddTab.width(), 0)
        else:
            posX = min(posX, self._tabBar.width() - self._buttonAddTab.width())

        self._buttonAddTab.move(posX, posY)

    def tabBarOverFlowChanged(self, overflowed):
        # Show buttons inside tabbar
        self._buttonAddTab.setVisible(not overflowed)

        # Show buttons displayed outside tabbar (corner widgets)
        self._buttonAddTab2.setVisible(overflowed)
        self._buttonListTabs.setVisible(overflowed)

    # Q_SIGNALS:
    changed = pyqtSignal()
    tabInserted = pyqtSignal(int)  # index
    tabRemoved = pyqtSignal(int)  # index
    tabMoved = pyqtSignal(int, int)  # from ,to

    # private Q_SLOTS:
    def _loadSettings(self):
        settings = Settings()
        settings.beginGroup('Browser-Tabs-Settings')
        self._dontCloseWithOneTab = settings.value('dontCloseWithOneTab',
                                                   False,
                                                   type=bool)
        self._showClosedTabsButton = settings.value('showClosedTabsButton',
                                                    False,
                                                    type=bool)
        self._newTabAfterActive = settings.value('newTabAfterActive',
                                                 True,
                                                 type=bool)
        self._newEmptyTabAfterActive = settings.value('newEmptyTabAfterActive',
                                                      False,
                                                      type=bool)
        settings.endGroup()

        settings.beginGroup('Web-URL-Settings')
        self._urlOnNewTab = settings.value('newTabUrl',
                                           'app:speeddial',
                                           type=QUrl)
        settings.endGroup()

        self._tabBar.loadSettings()

        self._updateClosedTabsButton()

    def _aboutToShowTabsMenu(self):
        self._menuTabs.clear()

        for idx in range(self.count()):
            tab = self._weTab(idx)
            if not tab or tab.isPinned():
                continue

            action = QAction(self)
            action.setIcon(tab.icon())

            if idx == self.currentIndex():
                f = action.font()
                f.setBold(True)
                action.setFont(f)

            title = tab.title()
            title.replace('&', '&&')
            action.setText(gVar.appTools.truncatedText(title, 40))

            # TODO: QVariant::fromValue(qobject_cast<QWidget*>(tab)
            action.setData(tab)
            action.triggered.connect(self._actionChangeIndex)
            self._menuTabs.addAction(action)

    def _aboutToShowClosedTabsMenu(self):
        self._menuClosedTabs.clear()

        closedTabs = self.closedTabsManager().closedTabs()
        for idx, tab in enumerate(closedTabs):
            title = gVar.appTools.truncatedText(tab.tabState.title, 40)
            self._menuClosedTabs.addAction(tab.tabState.icon, title,
                                           self.restoreClosedTab).setData(idx)

        if self._menuClosedTabs.isEmpty():
            self._menuClosedTabs.addAction('Empty').setEnabled(False)
        else:
            self._menuClosedTabs.addSeparator()
            self._menuClosedTabs.addAction('Restore All Closed Tabs',
                                           self.restoreAllClosedTabs)
            self._menuClosedTabs.addAction(QIcon.fromTheme('edit-clear'),
                                           'Clear list',
                                           self.clearClosedTabsList)

    def _actionChangeIndex(self):
        # TODO: QAction* action = qobject_cast<QAction*>(sender())
        action = self.sender()
        if action:
            # TODO: qobject_cast<WebTab*>(qvariant_cast<QWidget*>(action.data()))
            tab = action.data()
            if tab:
                self._tabBar.ensureVisible(tab.tabIndex())
                self.setCurrentIndex(tab.tabIndex())

    def _tabWasMoved(self, before, after):
        '''
        @param: before int
        @param: after int
        '''
        self._lastBackgroundTab = None

        self.changed.emit()
        if not self._blockTabMovedSignal:
            self.tabMoved.emit(before, after)

    # private:
    def _weTab(self, index=None):
        '''
        @return: WebTab
        '''
        if index is None:
            index = self.currentIndex()
        result = self.widget(index)
        if isinstance(result, WebTab):
            return result
        else:
            return None

    def _tabIcon(self, index):
        '''
        @return: TabIcon
        '''
        self._weTab(index).tabIcon()

    def _validIndex(self, index):
        return index >= 0 and index < self.count()

    def _updateClosedTabsButton(self):
        self._buttonClosedTabs.setVisible(self._showClosedTabsButton
                                          and self.canRestoreTab())

    # override
    def keyPressEvent(self, event):
        '''
        @param: event QKeyEvent
        '''
        #if gVar.app.plugins().processKeyPress(const.ON_TabWidget, self, event):
        #    return
        super(TabWidget, self).keyPressEvent(event)

    # override
    def keyReleaseEvent(self, event):
        '''
        @param: event QKeyEvent
        '''
        #if gVar.app.plugins().processKeyRelease(const.ON_TabWidget, self, event):
        #    return
        super(TabWidget, self).keyReleaseEvent(event)
Exemple #9
0
class TabStackedWidget(QWidget):
    '''
    @note: just some of QTabWidget's methods were implemented
    '''
    def __init__(self, parent=None):
        super(TabStackedWidget, self).__init__(parent)
        self._stack = None  # QStackedWidget
        self._tabBar = None  # ComboTabBar
        self._mainLayout = None  # QVBoxLayout
        self._dirtyTabBar = False

        self._currentIndex = 0
        self._previousIndex = 0

        self._stack = QStackedWidget(self)
        self._mainLayout = QVBoxLayout()
        self._mainLayout.setSpacing(0)
        self._mainLayout.setContentsMargins(0, 0, 0, 0)

        self._mainLayout.addWidget(self._stack)
        self.setLayout(self._mainLayout)

        self._stack.widgetRemoved.connect(self._tabWasRemoved)

    def tabBar(self):
        '''
        @return: ComboTabBar
        '''
        return self._tabBar

    def setTabBar(self, tb):
        '''
        @param: ComboTabBar
        '''
        assert (tb)

        if tb.parentWidget() != self:
            tb.setParent(self)
            tb.show()

        del self._tabBar
        self._dirtyTabBar = True
        self._tabBar = tb
        self.setFocusProxy(self._tabBar)

        self._tabBar.currentChanged.connect(self._showTab)
        self._tabBar.tabMoved.connect(
            lambda from_, to_: TabStackedWidget._tabWasMoved(self, from_, to_))
        self._tabBar.overFlowChanged.connect(self.setUpLayout)

        if self._tabBar.tabsCloseable():
            self._tabBar.tabCloseRequested.connect(self.tabCloseRequested)

        self.setDocumentMode(self._tabBar.documentMode())

        self._tabBar.installEventFilter(self)
        self.setUpLayout()

    def documentMode(self):
        return self._tabBar.documentMode()

    def setDocumentMode(self, enabled):
        self._tabBar.setDocumentModel(enabled)
        self._tabBar.setExpanding(not enabled)
        self._tabBar.setDrawBase(enabled)

    def addTab(self, widget, label, pinned=False):
        '''
        @return: int
        '''
        return self.insertTab(-1, widget, label, pinned)

    def insertTab(self, index, widget, label, pinned=False):
        '''
        @param: widget QWidget
        @param: label QString
        '''
        if not widget:
            return -1

        if pinned:
            if index < 0:
                index = self._tabBar.pinnedTabsCount()
            else:
                index = min(index, self._tabBar.pinnedTabsCount())
            index = self._stack.insertWidget(index, widget)
            self._tabBar.insertTabByIconText(index, QIcon(), label, True)
        else:
            if index < 0:
                index = self._tabBar.pinnedTabsCount()
            else:
                index = max(index, self._tabBar.pinnedTabsCount())
            index = self._stack.insertWidget(index, widget)
            self._tabBar.insertTabByIconText(index, QIcon(), label, False)

        if self._previousIndex >= index:
            self._previousIndex += 1
        if self._currentIndex >= index:
            self._currentIndex += 1

        QTimer.singleShot(0, self.setUpLayout)

        return index

    def tabText(self, index):
        return self._tabBar.tabText(index)

    def setTabText(self, index, label):
        self._tabBar.setTabText(index, label)

    def tabToolTip(self, index):
        return self._tabBar.tabToolTip(index)

    def setTabToolTip(self, index, tip):
        self._tabBar.setTabToolTip(index, tip)

    def pinUnPinTab(self, index, title=''):
        widget = self._stack.widget(index)
        currentWidget = self._stack.currentWidget()

        if not widget or not currentWidget:
            return -1

        makePinned = index >= self._tabBar.pinnedTabsCount()
        button = self._tabBar.tabButton(index,
                                        self._tabBar.iconButtonPosition())
        # To show tooltip of tab which is pinned in the current session
        toolTip = self.tabToolTip(index)

        self._tabBar._blockCurrentChangedSignal = True
        self._tabBar.setTabButton(index, self._tabBar.iconButtonPosition(),
                                  None)

        self._stack.removeWidget(widget)
        if makePinned:
            insertIndex = 0
        else:
            insertIndex = self._tabBar.pinnedTabsCount()
        newIndex = self.insertTab(insertIndex, widget, title, makePinned)

        self._tabBar.setTabButton(newIndex, self._tabBar.iconButtonPosition(),
                                  button)
        self._tabBar._blockCurrentChangedSignal = False
        self.setTabToolTip(newIndex, toolTip)

        # Restore current widget
        self.setCurrentWidget(currentWidget)

        self.pinStateChanged.emit(newIndex, makePinned)

        return newIndex

    def removeTab(self, index):
        widget = self._stack.widget(index)
        if widget:
            # Select another current tab before remove, so it won't be handled
            # by QTabBar
            if index == self.currentIndex() and self.count() > 1:
                self._selectTabOnRemove()
            self._stack.removeWidget(widget)

    def moveTab(self, from_, to_):
        self._tabBar.moveTab(from_, to_)

    def currentIndex(self):
        '''
        @return: int
        '''
        return self._tabBar.currentIndex()

    def currentWidget(self):
        '''
        @return: QWidget
        '''
        return self._stack.currentWidget()

    def widget(self, index):
        '''
        @return: QWidget
        '''
        return self._stack.widget(index)

    def indexOf(self, widget):
        '''
        @param: widget QWidget
        @return: int
        '''
        return self._stack.indexOf(widget)

    def count(self):
        return self._tabBar.count()

    # Q_SIGNALS
    currentChanged = pyqtSignal(int)  # index
    tabCloseRequested = pyqtSignal(int)  # index
    pinStateChanged = pyqtSignal(int, bool)  # index, pinned

    # public Q_SLOTS
    def setCurrentIndex(self, index):
        self._tabBar.setCurrentIndex(index)

    def setCurrentWidget(self, widget):
        '''
        @widget: widget QWidget
        '''
        self._tabBar.setCurrentIndex(self.indexOf(widget))

    def setUpLayout(self):
        if not self._tabBar.isVisible():
            self._dirtyTabBar = True
            return

        self._tabBar.setElideMode(self._tabBar.elideMode())
        self._dirtyTabBar = False

    # private Q_SLOTS
    def _showTab(self, index):
        if self._validIndex(index):
            self._stack.setCurrentIndex(index)

        self._previousIndex = self._currentIndex
        self._currentIndex = index

        # This is slot connected to ComboTabBar::currentChanged
        # We must send the signal event with invalid index (-1)
        self.currentChanged.emit(index)

    def _tabWasMoved(self, from_, to_):
        self._stack.blockSignals(True)
        widget = self._stack.widget(from_)
        self._stack.removeWidget(widget)
        self._stack.insertWidget(to_, widget)
        self._stack.blockSignals(False)

    def _tabWasRemoved(self, index):
        if self._previousIndex == index:
            self._previousIndex = -1
        elif self._previousIndex > index:
            self._previousIndex -= 1

        if self._currentIndex == index:
            self._currentIndex = -1
        elif self._currentIndex > index:
            self._currentIndex -= 1

        self._tabBar.removeTab(index)

    # protected:
    # override
    def eventFilter(self, obj, event):
        '''
        @param: obj QObject
        @param: event QEvent
        @return: bool
        '''
        if self._dirtyTabBar and obj == self._tabBar and event.type(
        ) == QEvent.Show:
            self.setUpLayout()

        return False

    # override
    def keyPressEvent(self, event):
        '''
        @param: event QKeyEvent
        '''
        evkey = event.key()
        evmodifiers = event.modifiers()
        if evkey == Qt.Key_Tab or evkey == Qt.Key_Backtab and \
                self.count() > 1 and evmodifiers & Qt.ControlModifier:
            pageCount = self.count()
            page = self.currentIndex()
            if (evkey == Qt.Key_Backtab) or (evmodifiers & Qt.ShiftModifier):
                dx = -1
            else:
                dx = 1
            for idx in range(pageCount):
                page += dx
                if page < 0:
                    page = self.count() - 1
                elif page >= pageCount:
                    page = 0
                if self._tabBar.isTabEnabled(page):
                    self.setCurrentIndex(page)
                    break
            if not QApplication.focusWidget():
                self._tabBar.setFocus()
        else:
            event.ignore()

    # private:
    def _validIndex(self, index):
        return index < self._stack.count() and index >= 0

    def _selectTabOnRemove(self):
        assert (self.count() > 1)

        index = -1

        behavior = self._tabBar.selectionBehaviorOnRemove()
        if behavior == QTabBar.SelectPreviousTab:
            if self._validIndex(self._previousIndex):
                index = self._previousIndex
            # fallthrough
        elif behavior == QTabBar.SelectLeftTab:
            index = self.currentIndex() - 1
            if not self._validIndex(index):
                index = 1
        elif behavior == QTabBar.SelectRightTab:
            index = self.currentIndex() + 1
            if not self._validIndex(index):
                index = self.currentIndex() - 1

        assert (self._validIndex(index))
        self.setCurrentIndex(index)
Exemple #10
0
class ComprehensionTestApp(QMainWindow):
    """
	@modifies self.symbol_test, self.question1, self.question2
	@effects makes a new ComprehensionTestApp
	"""
    def __init__(self):
        super().__init__()
        self.symbol_tests = QStackedWidget()
        self.question1 = "Exactly what do you think this symbol means?"
        self.question2 = "What action would you take in response to this symbol?"
        self.init_gui()

    """
	@modifies self
	@effects sets up the main window
	"""

    def init_gui(self):
        exitAct = QAction('&Exit', self)
        exitAct.setShortcut('Ctrl+Q')
        exitAct.setStatusTip('Exit application')
        exitAct.triggered.connect(self.quit)

        saveAct = QAction('&Save as PDF', self)
        saveAct.setShortcut('Ctrl+S')
        saveAct.setStatusTip('Saves as PDF')
        saveAct.triggered.connect(self.save_as_pdf)

        importAct = QAction('&Import symbols', self)
        importAct.setShortcut('Ctrl+I')
        importAct.setStatusTip(
            'Imports all the specified images on to their own test page')
        importAct.triggered.connect(self.import_symbols)

        menu_bar = self.menuBar()
        filemenu = menu_bar.addMenu('&File')
        filemenu.addAction(importAct)
        filemenu.addAction(saveAct)
        filemenu.addAction(exitAct)

        goToAct = QAction('&Go to page', self)
        goToAct.setStatusTip('Go to the specified page')
        goToAct.setShortcut('Ctrl+G')
        goToAct.triggered.connect(self.go_to_page)

        newQuestAct = QAction('&Change default questions', self)
        newQuestAct.setStatusTip(
            'Changes the default questions asked about the symbol')
        newQuestAct.setShortcut('Ctrl+H')
        newQuestAct.triggered.connect(self.change_questions)

        moveLeftAct = QAction('&Move current page left', self)
        moveLeftAct.setShortcut('Ctrl+,')
        moveLeftAct.setStatusTip('Moves the current page one page to the left')
        moveLeftAct.triggered.connect(self.move_test_left)

        moveRightAct = QAction('&Move current page right', self)
        moveRightAct.setShortcut('Ctrl+.')
        moveRightAct.setStatusTip(
            'Moves the current page one page to the right')
        moveRightAct.triggered.connect(self.move_test_right)

        moveToAct = QAction('Move current page to', self)
        moveToAct.setShortcut('Ctrl+M')
        moveToAct.setStatusTip('Moves the current page to the specified page')
        moveToAct.triggered.connect(self.move_to_page)

        delPagesAct = QAction('Delete pages', self)
        delPagesAct.setShortcut('Ctrl+D')
        delPagesAct.setStatusTip('Deletes the specified pages')
        delPagesAct.triggered.connect(self.delete_pages)

        editmenu = menu_bar.addMenu('&Edit')
        editmenu.addAction(delPagesAct)
        editmenu.addAction(newQuestAct)
        editmenu.addAction(goToAct)
        editmenu.addAction(moveLeftAct)
        editmenu.addAction(moveRightAct)
        editmenu.addAction(moveToAct)

        nextAct = QAction(QIcon("next.png"), 'Next page (Ctrl+P)', self)
        nextAct.setShortcut('Ctrl+P')
        nextAct.setStatusTip('Goes to the next page')
        nextAct.triggered.connect(self.next_page)

        prevAct = QAction(QIcon("prev.png"), "Previous page (Ctrl+O)", self)
        prevAct.setShortcut('Ctrl+O')
        prevAct.setStatusTip('Goes to the previous page')
        prevAct.triggered.connect(self.prev_page)

        newAct = QAction(QIcon("add.png"), "Add a new page (Ctrl+N)", self)
        newAct.setShortcut('Ctrl+N')
        newAct.setStatusTip('Creates a new page')
        newAct.triggered.connect(self.add_blank_test)

        remAct = QAction(QIcon("remove.png"), "Delete page (Ctrl+R)", self)
        remAct.setShortcut('Ctrl+R')
        remAct.setStatusTip('Deletes the current page')
        remAct.triggered.connect(self.delete_test)

        tool_bar = self.addToolBar("Next Page")
        tool_bar.addAction(prevAct)
        tool_bar.addAction(nextAct)
        tool_bar.addAction(newAct)
        tool_bar.addAction(remAct)

        self.setWindowTitle("Open Comprehension Test Generator")
        self.setWindowIcon(QIcon("rubber ducky.png"))
        self.setGeometry(500, 500, 500, 450)
        self.add_blank_test()
        self.setCentralWidget(self.symbol_tests)

        self.show()

    """
	@modifies none
	@effects  confirms if the user wishes to exit
	"""

    def closeEvent(self, event):
        msg = QMessageBox.question(self, 'Message', "Really quit?",
                                   QMessageBox.Yes | QMessageBox.No,
                                   QMessageBox.No)
        if msg == QMessageBox.Yes:
            event.accept()
        else:
            event.ignore()

    """
	@modifies none
	@effects  confirms if the user wishes to exit
	"""

    def quit(self):
        msg = QMessageBox.question(self, 'Message', "Really quit?",
                                   QMessageBox.Yes | QMessageBox.No,
                                   QMessageBox.No)
        if msg == QMessageBox.Yes:
            qApp.quit()

    """
	@modifies self.symbol_tests
	@effects  adds a blank symbol_test to the end of symbol_tests
			  sets the current symbol_test to the new one
	"""

    def add_blank_test(self):
        page = self.symbol_tests.count() + 1
        test = SymbolTestWidget(
            self,
            comp_questions=[self.question1, self.question2],
            pageNumber=page,
            pageTotal=page)
        test.set_visual_context("blank image.png")
        self.symbol_tests.addWidget(test)
        self.symbol_tests.setCurrentIndex(page - 1)
        for i in range(0, self.symbol_tests.count() - 1):
            self.symbol_tests.widget(i).set_numPages(self.symbol_tests.count())

    """
	@modifies self.symbol_tests
	@effects  changes the current symbol_test to the next one
	"""

    def next_page(self):
        if self.symbol_tests.currentIndex() < self.symbol_tests.count() - 1:
            self.symbol_tests.setCurrentIndex(
                self.symbol_tests.currentIndex() + 1)

    """
	@modifies self.symbol_test
	@effects  changes the current symbol_test to the previous one
	"""

    def prev_page(self):
        if self.symbol_tests.currentIndex() > 0:
            self.symbol_tests.setCurrentIndex(
                self.symbol_tests.currentIndex() - 1)

    """
	@modifies none
	@effects  saves all the symbol_tests as seperate pages on a PDF document
	"""

    def save_as_pdf(self):
        filename = QFileDialog.getSaveFileName(self, 'Save to PDF', 'c:\\',
                                               "*.pdf")

        if filename != ('', ''):
            if os.path.exists(filename[0]):
                try:
                    infile = PdfFileReader(filename[0], 'rb')
                except:
                    error = QMessageBox()
                    error.setIcon(QMessageBox.Warning)
                    error.setStandardButtons(QMessageBox.Ok)
                    error.setText(
                        "File could not be written to. If the file is currently open, try closing it"
                    )
                    error.exec_()
                    return
                if infile.getNumPages() == 0:
                    print("HERE!")
                    doc = QTextDocument()
                    doc.print(printer)

            printer = QPrinter()
            printer.setPageSize(QPrinter.A4)
            printer.setOutputFormat(QPrinter.PdfFormat)
            printer.setOutputFileName(filename[0])

            painter = QPainter()

            font = QFont("times")
            font.setPointSize(12)

            x = painter.begin(printer)
            if x == False:
                error = QMessageBox()
                error.setIcon(QMessageBox.Warning)
                error.setStandardButtons(QMessageBox.Ok)
                error.setText(
                    "File could not be saved. If the file is currently open, try closing it"
                )
                error.exec_()
                return
            painter.setFont(font)

            for i in range(0, self.symbol_tests.count()):
                cur_symbol_test = self.symbol_tests.widget(i)

                if cur_symbol_test.print_visual_context() == False:
                    pixmap = cur_symbol_test.get_symbol()
                    pixmap = pixmap.scaled(350, 350)

                    painter.drawPixmap(30, 100, pixmap)
                    painter.drawText(750, 20, cur_symbol_test.get_page())
                    painter.drawText(
                        420, 200, 350, 400, QtCore.Qt.TextWordWrap,
                        "Context: " + cur_symbol_test.get_context())
                    painter.drawText(30, 600, cur_symbol_test.get_question1())
                    painter.drawText(30, 830, cur_symbol_test.get_question2())

                    cur_pen = painter.pen()
                    line_pen = QPen()
                    line_pen.setWidth(2)
                    painter.setPen(line_pen)
                    painter.drawLine(70, 656, 600, 656)
                    painter.drawLine(70, 712, 600, 712)
                    painter.drawLine(70, 768, 600, 768)

                    painter.drawLine(70, 886, 600, 886)
                    painter.drawLine(70, 942, 600, 942)
                    painter.drawLine(70, 998, 600, 998)
                    painter.setPen(cur_pen)

                else:
                    pixmap = cur_symbol_test.get_visual_context()
                    pixmap = pixmap.scaled(300, 300)

                    painter.drawPixmap(200, 10, pixmap)

                    pixmap = cur_symbol_test.get_symbol()
                    pixmap = pixmap.scaled(250, 250)

                    painter.drawPixmap(225, 320, pixmap)

                    painter.drawText(750, 20, cur_symbol_test.get_page())
                    #painter.drawText(420, 200, 350, 400, QtCore.Qt.TextWordWrap, "Context: " + cur_symbol_test.get_context())
                    painter.drawText(30, 600, cur_symbol_test.get_question1())
                    painter.drawText(30, 830, cur_symbol_test.get_question2())

                    cur_pen = painter.pen()
                    line_pen = QPen()
                    line_pen.setWidth(2)
                    painter.setPen(line_pen)
                    painter.drawLine(70, 656, 600, 656)
                    painter.drawLine(70, 712, 600, 712)
                    painter.drawLine(70, 768, 600, 768)

                    painter.drawLine(70, 886, 600, 886)
                    painter.drawLine(70, 942, 600, 942)
                    painter.drawLine(70, 998, 600, 998)
                    painter.setPen(cur_pen)
                if (i < self.symbol_tests.count() - 1):
                    printer.newPage()

            painter.end()

    """
	@modifies self.symbol_tests
	@effects  adds all the specified symbols to their own symbol_test and adds those
			  tests to self.symbol_tests
	"""

    def import_symbols(self):
        fnames = QFileDialog.getOpenFileNames(self, 'Open file', 'c:\\',
                                              "Image files (*.jpg *.png)")
        if fnames != ([], ''):
            for i in range(0, len(fnames[0])):
                page = self.symbol_tests.count() + 1
                test = SymbolTestWidget(
                    self,
                    comp_questions=[self.question1, self.question2],
                    pageNumber=page,
                    pageTotal=page)
                test.set_symbol(fnames[0][i])
                test.set_visual_context("blank image.png")
                self.symbol_tests.addWidget(test)
            self.symbol_tests.setCurrentIndex(page - 1)

            for i in range(0, self.symbol_tests.count()):
                self.symbol_tests.widget(i).set_numPages(
                    self.symbol_tests.count())

    """
	@modifies self.symbol_tests
	@effects  removes the current test from self.symbol_tests
	"""

    def delete_test(self):
        msg = QMessageBox.question(self, 'Message', "Delete test?",
                                   QMessageBox.Yes | QMessageBox.No,
                                   QMessageBox.No)
        if msg == QMessageBox.No:
            return

        if self.symbol_tests.currentIndex() != 0:
            cur_idx = self.symbol_tests.currentIndex()
            self.symbol_tests.setCurrentIndex(cur_idx - 1)
            self.symbol_tests.removeWidget(self.symbol_tests.widget(cur_idx))
            for i in range(0, self.symbol_tests.count()):
                cur_symbol_test = self.symbol_tests.widget(i)
                cur_symbol_test.set_numPages(self.symbol_tests.count())

                if i >= cur_idx:
                    cur_symbol_test.set_page(i + 1)

        elif self.symbol_tests.currentIndex(
        ) == 0 and self.symbol_tests.count() > 1:
            self.symbol_tests.setCurrentIndex(1)
            self.symbol_tests.removeWidget(self.symbol_tests.widget(0))
            for i in range(0, self.symbol_tests.count()):
                cur_symbol_test = self.symbol_tests.widget(i)
                cur_symbol_test.set_numPages(self.symbol_tests.count())
                cur_symbol_test.set_page(i + 1)

        elif self.symbol_tests.currentIndex() == 0 and self.symbol_tests.count(
        ) == 1:
            page = 1
            test = SymbolTestWidget(
                self,
                comp_questions=[self.question1, self.question2],
                pageNumber=page,
                pageTotal=page)
            self.symbol_tests.addWidget(test)
            self.symbol_tests.setCurrentIndex(1)
            self.symbol_tests.removeWidget(self.symbol_tests.widget(0))

    """
	@modifies self.symbol_tests
	@effects  decreases the current symbol_test's index in self.symbol_tests by 1,
			  moves it down 1 postion
	"""

    def move_test_left(self):
        cur_idx = self.symbol_tests.currentIndex()
        if cur_idx > 0:
            cur_widget = self.symbol_tests.widget(cur_idx)
            prev_widget = self.symbol_tests.widget(cur_idx - 1)
            cur_widget.set_page(int(cur_widget.get_page()) - 1)
            prev_widget.set_page(int(prev_widget.get_page()) + 1)

            self.symbol_tests.removeWidget(prev_widget)
            self.symbol_tests.insertWidget(
                self.symbol_tests.indexOf(cur_widget) + 1, prev_widget)

    """
	@modifies self.symbol_tests
	@effects  increases the current symbol_test's index in self.symbol_tests by 1,
			  moves it up 1 postion
	"""

    def move_test_right(self):
        cur_idx = self.symbol_tests.currentIndex()
        if cur_idx < self.symbol_tests.count() - 1:
            cur_widget = self.symbol_tests.widget(cur_idx)
            next_widget = self.symbol_tests.widget(cur_idx + 1)
            cur_widget.set_page(int(cur_widget.get_page()) + 1)
            next_widget.set_page(int(next_widget.get_page()) - 1)

            self.symbol_tests.removeWidget(next_widget)
            self.symbol_tests.insertWidget(
                self.symbol_tests.indexOf(cur_widget), next_widget)

    """
	@modifies self.symbol_tests
	@effects  moves the current symbol_test to a specified position in self.symbol_tests
	"""

    def move_to_page(self):
        text = QInputDialog.getText(self, "Move Page",
                                    "Move current page to: ", QLineEdit.Normal)
        if text[1] != False:
            try:
                x = int(text[0])
                if x <= 0 or x > self.symbol_tests.count():
                    error = QMessageBox()
                    error.setIcon(QMessageBox.Warning)
                    error.setStandardButtons(QMessageBox.Ok)
                    error.setText("Page must be between 1 and " +
                                  str(self.symbol_tests.count()))
                    error.exec_()
                    return

                cur_idx = self.symbol_tests.currentIndex()
                cur_widget = self.symbol_tests.widget(cur_idx)
                cur_widget.set_page(x - 1)

                self.symbol_tests.removeWidget(cur_widget)
                self.symbol_tests.insertWidget(x - 1, cur_widget)
                self.symbol_tests.setCurrentIndex(x - 1)

                for i in range(0, self.symbol_tests.count()):
                    loop_widget = self.symbol_tests.widget(i)
                    loop_widget.set_page(i + 1)

            except ValueError:
                error = QMessageBox()
                error.setIcon(QMessageBox.Warning)
                error.setStandardButtons(QMessageBox.Ok)
                error.setText("Can only enter an integer between 1 and " +
                              str(self.symbol_tests.count()))
                error.exec_()

    """
	@modifies self.symbol_tests
	@effects  sets the surrent symbol_test equal to the one at the specified page
	"""

    def go_to_page(self):
        text = QInputDialog.getText(self, "Go to", "Go to page: ",
                                    QLineEdit.Normal)
        if text[1] != False:
            try:
                x = int(text[0])
                if x <= 0 or x > self.symbol_tests.count():
                    error = QMessageBox()
                    error.setIcon(QMessageBox.Warning)
                    error.setStandardButtons(QMessageBox.Ok)
                    error.setText("Page must be between 1 and " +
                                  str(self.symbol_tests.count()))
                    error.exec_()
                    return

                self.symbol_tests.setCurrentIndex(x - 1)
            except ValueError:
                error = QMessageBox()
                error.setIcon(QMessageBox.Warning)
                error.setStandardButtons(QMessageBox.Ok)
                error.setText("Can only enter an integer between 1 and " +
                              str(self.symbol_tests.count()))
                error.exec_()

    """
	@modifies self.question1, self.question2
	@effects  changes the default questions
	"""

    def change_questions(self):
        text = QInputDialog.getText(self, "Question 1 Input", "Question 1: ",
                                    QLineEdit.Normal, self.question1)
        if str(text[0]) != '':
            self.question1 = str(text[0])
        text = QInputDialog.getText(self, "Question 2 Input", "Question 2: ",
                                    QLineEdit.Normal, self.question2)
        if str(text[0]) != '':
            self.question2 = str(text[0])

    def delete_pages(self):
        text = QInputDialog.getText(
            self, "Delete pages", "Enter page numbers seperated by a comma:",
            QLineEdit.Normal)
        error = QMessageBox()
        error.setIcon(QMessageBox.Warning)
        error.setStandardButtons(QMessageBox.Ok)
        if text[1] != False:
            pages = text[0].split(",")
            del_widgets = []
            for page in pages:
                try:
                    x = int(page)
                except:
                    error.setText("All page numbers must be integers")
                    error.exec_()
                    return
                if int(page) > self.symbol_tests.count() or int(page) < 1:
                    error.setText("Page " + page + " does not exist")
                    error.exec_()
                    return
                del_widgets.append(self.symbol_tests.widget(int(page) - 1))

            confirm = QMessageBox.question(
                self, 'Message', "Really delete pages: " + text[0] + "?",
                QMessageBox.Yes | QMessageBox.No, QMessageBox.No)

            for del_widget in del_widgets:
                del_idx = self.symbol_tests.indexOf(del_widget)
                if del_idx != self.symbol_tests.currentIndex():
                    self.symbol_tests.removeWidget(del_widget)
                else:
                    #Removing the only test
                    if self.symbol_tests.count() == 1:
                        self.add_blank_test()
                        self.symbol_tests.removeWidget(del_widget)
                        self.symbol_tests.currentWidget().set_page(1)
                        self.symbol_tests.currentWidget().set_numPages(1)
                        return
                    else:
                        if del_idx == self.symbol_tests.count() - 1:
                            self.symbol_tests.setCurrentIndex(del_idx - 1)
                            self.symbol_tests.removeWidget(del_widget)
                        else:
                            self.symbol_tests.setCurrentIndex(del_idx + 1)
                            self.symbol_tests.removeWidget(del_widget)

            count = 1
            for i in range(0, self.symbol_tests.count()):
                cur_widget = self.symbol_tests.widget(i)
                cur_widget.set_page(count)
                cur_widget.set_numPages(self.symbol_tests.count())
                count += 1