Exemplo n.º 1
0
def main(argv):
    app = QApplication(argv)
    main_window = uic.loadUi('mainwindow.ui')

    table_view = main_window.findChild(QTableView, 'tableView')

    model = QStandardItemModel()
    root_item = Root()
    model.appendRow(root_item)

    table_model = QSortFilterProxyModel()
    table_model.setSourceModel(model)

    table_view.setModel(table_model)
    table_view.setRootIndex(table_model.mapFromSource(root_item.index()))

    main_window.show()
    sys.exit(app.exec_())
Exemplo n.º 2
0
class BreakPointViewer(QTreeView):
    """
    Class implementing the Breakpoint viewer widget.
    
    Breakpoints will be shown with all their details. They can be modified
    through the context menu of this widget.
    
    @signal sourceFile(str, int) emitted to show the source of a breakpoint
    """
    sourceFile = pyqtSignal(str, int)
    
    def __init__(self, parent=None):
        """
        Constructor
        
        @param parent the parent (QWidget)
        """
        super(BreakPointViewer, self).__init__(parent)
        self.setObjectName("BreakPointViewer")
        
        self.__model = None
        
        self.setItemsExpandable(False)
        self.setRootIsDecorated(False)
        self.setAlternatingRowColors(True)
        self.setSelectionMode(QAbstractItemView.ExtendedSelection)
        self.setSelectionBehavior(QAbstractItemView.SelectRows)
        
        self.setWindowTitle(self.tr("Breakpoints"))
        
        self.setContextMenuPolicy(Qt.CustomContextMenu)
        self.customContextMenuRequested.connect(self.__showContextMenu)
        self.doubleClicked.connect(self.__doubleClicked)
        
        self.__createPopupMenus()
        
        self.condHistory = []
        self.fnHistory = []
        self.fnHistory.append('')
        
    def setModel(self, model):
        """
        Public slot to set the breakpoint model.
        
        @param model reference to the breakpoint model (BreakPointModel)
        """
        self.__model = model
        
        self.sortingModel = QSortFilterProxyModel()
        self.sortingModel.setDynamicSortFilter(True)
        self.sortingModel.setSourceModel(self.__model)
        super(BreakPointViewer, self).setModel(self.sortingModel)
        
        header = self.header()
        header.setSortIndicator(0, Qt.AscendingOrder)
        header.setSortIndicatorShown(True)
        if qVersion() >= "5.0.0":
            header.setSectionsClickable(True)
        else:
            header.setClickable(True)
        
        self.setSortingEnabled(True)
        
        self.__layoutDisplay()
        
    def __layoutDisplay(self):
        """
        Private slot to perform a layout operation.
        """
        self.__resizeColumns()
        self.__resort()
        
    def __resizeColumns(self):
        """
        Private slot to resize the view when items get added, edited or
        deleted.
        """
        self.header().resizeSections(QHeaderView.ResizeToContents)
        self.header().setStretchLastSection(True)
    
    def __resort(self):
        """
        Private slot to resort the tree.
        """
        self.model().sort(self.header().sortIndicatorSection(),
                          self.header().sortIndicatorOrder())
        
    def __toSourceIndex(self, index):
        """
        Private slot to convert an index to a source index.
        
        @param index index to be converted (QModelIndex)
        @return mapped index (QModelIndex)
        """
        return self.sortingModel.mapToSource(index)
        
    def __fromSourceIndex(self, sindex):
        """
        Private slot to convert a source index to an index.
        
        @param sindex source index to be converted (QModelIndex)
        @return mapped index (QModelIndex)
        """
        return self.sortingModel.mapFromSource(sindex)
        
    def __setRowSelected(self, index, selected=True):
        """
        Private slot to select a complete row.
        
        @param index index determining the row to be selected (QModelIndex)
        @param selected flag indicating the action (bool)
        """
        if not index.isValid():
            return
        
        if selected:
            flags = QItemSelectionModel.SelectionFlags(
                QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows)
        else:
            flags = QItemSelectionModel.SelectionFlags(
                QItemSelectionModel.Deselect | QItemSelectionModel.Rows)
        self.selectionModel().select(index, flags)
        
    def __createPopupMenus(self):
        """
        Private method to generate the popup menus.
        """
        self.menu = QMenu()
        self.menu.addAction(self.tr("Add"), self.__addBreak)
        self.menu.addAction(self.tr("Edit..."), self.__editBreak)
        self.menu.addSeparator()
        self.menu.addAction(self.tr("Enable"), self.__enableBreak)
        self.menu.addAction(self.tr("Enable all"), self.__enableAllBreaks)
        self.menu.addSeparator()
        self.menu.addAction(self.tr("Disable"), self.__disableBreak)
        self.menu.addAction(self.tr("Disable all"),
                            self.__disableAllBreaks)
        self.menu.addSeparator()
        self.menu.addAction(self.tr("Delete"), self.__deleteBreak)
        self.menu.addAction(self.tr("Delete all"), self.__deleteAllBreaks)
        self.menu.addSeparator()
        self.menu.addAction(self.tr("Goto"), self.__showSource)
        self.menu.addSeparator()
        self.menu.addAction(self.tr("Configure..."), self.__configure)

        self.backMenuActions = {}
        self.backMenu = QMenu()
        self.backMenu.addAction(self.tr("Add"), self.__addBreak)
        self.backMenuActions["EnableAll"] = \
            self.backMenu.addAction(self.tr("Enable all"),
                                    self.__enableAllBreaks)
        self.backMenuActions["DisableAll"] = \
            self.backMenu.addAction(self.tr("Disable all"),
                                    self.__disableAllBreaks)
        self.backMenuActions["DeleteAll"] = \
            self.backMenu.addAction(self.tr("Delete all"),
                                    self.__deleteAllBreaks)
        self.backMenu.aboutToShow.connect(self.__showBackMenu)
        self.backMenu.addSeparator()
        self.backMenu.addAction(self.tr("Configure..."), self.__configure)

        self.multiMenu = QMenu()
        self.multiMenu.addAction(self.tr("Add"), self.__addBreak)
        self.multiMenu.addSeparator()
        self.multiMenu.addAction(self.tr("Enable selected"),
                                 self.__enableSelectedBreaks)
        self.multiMenu.addAction(self.tr("Enable all"),
                                 self.__enableAllBreaks)
        self.multiMenu.addSeparator()
        self.multiMenu.addAction(self.tr("Disable selected"),
                                 self.__disableSelectedBreaks)
        self.multiMenu.addAction(self.tr("Disable all"),
                                 self.__disableAllBreaks)
        self.multiMenu.addSeparator()
        self.multiMenu.addAction(self.tr("Delete selected"),
                                 self.__deleteSelectedBreaks)
        self.multiMenu.addAction(self.tr("Delete all"),
                                 self.__deleteAllBreaks)
        self.multiMenu.addSeparator()
        self.multiMenu.addAction(self.tr("Configure..."), self.__configure)
    
    def __showContextMenu(self, coord):
        """
        Private slot to show the context menu.
        
        @param coord the position of the mouse pointer (QPoint)
        """
        cnt = self.__getSelectedItemsCount()
        if cnt <= 1:
            index = self.indexAt(coord)
            if index.isValid():
                cnt = 1
                self.__setRowSelected(index)
        coord = self.mapToGlobal(coord)
        if cnt > 1:
            self.multiMenu.popup(coord)
        elif cnt == 1:
            self.menu.popup(coord)
        else:
            self.backMenu.popup(coord)
    
    def __clearSelection(self):
        """
        Private slot to clear the selection.
        """
        for index in self.selectedIndexes():
            self.__setRowSelected(index, False)
            
    def __addBreak(self):
        """
        Private slot to handle the add breakpoint context menu entry.
        """
        from .EditBreakpointDialog import EditBreakpointDialog
        
        dlg = EditBreakpointDialog((self.fnHistory[0], None), None,
                                   self.condHistory, self, modal=1,
                                   addMode=1, filenameHistory=self.fnHistory)
        if dlg.exec_() == QDialog.Accepted:
            fn, line, cond, temp, enabled, count = dlg.getAddData()
            if fn is not None:
                if fn in self.fnHistory:
                    self.fnHistory.remove(fn)
                self.fnHistory.insert(0, fn)
            
            if cond:
                if cond in self.condHistory:
                    self.condHistory.remove(cond)
                self.condHistory.insert(0, cond)
            
            self.__model.addBreakPoint(fn, line, (cond, temp, enabled, count))
            self.__resizeColumns()
            self.__resort()
    
    def __doubleClicked(self, index):
        """
        Private slot to handle the double clicked signal.
        
        @param index index of the entry that was double clicked (QModelIndex)
        """
        if index.isValid():
            self.__editBreakpoint(index)

    def __editBreak(self):
        """
        Private slot to handle the edit breakpoint context menu entry.
        """
        index = self.currentIndex()
        if index.isValid():
            self.__editBreakpoint(index)
    
    def __editBreakpoint(self, index):
        """
        Private slot to edit a breakpoint.
        
        @param index index of breakpoint to be edited (QModelIndex)
        """
        sindex = self.__toSourceIndex(index)
        if sindex.isValid():
            bp = self.__model.getBreakPointByIndex(sindex)
            if not bp:
                return
            
            fn, line, cond, temp, enabled, count = bp[:6]
            
            from .EditBreakpointDialog import EditBreakpointDialog
            dlg = EditBreakpointDialog(
                (fn, line), (cond, temp, enabled, count),
                self.condHistory, self, modal=True)
            if dlg.exec_() == QDialog.Accepted:
                cond, temp, enabled, count = dlg.getData()
                if cond:
                    if cond in self.condHistory:
                        self.condHistory.remove(cond)
                    self.condHistory.insert(0, cond)
                
                self.__model.setBreakPointByIndex(
                    sindex, fn, line, (cond, temp, enabled, count))
                self.__resizeColumns()
                self.__resort()

    def __setBpEnabled(self, index, enabled):
        """
        Private method to set the enabled status of a breakpoint.
        
        @param index index of breakpoint to be enabled/disabled (QModelIndex)
        @param enabled flag indicating the enabled status to be set (boolean)
        """
        sindex = self.__toSourceIndex(index)
        if sindex.isValid():
            self.__model.setBreakPointEnabledByIndex(sindex, enabled)
        
    def __enableBreak(self):
        """
        Private slot to handle the enable breakpoint context menu entry.
        """
        index = self.currentIndex()
        self.__setBpEnabled(index, True)
        self.__resizeColumns()
        self.__resort()

    def __enableAllBreaks(self):
        """
        Private slot to handle the enable all breakpoints context menu entry.
        """
        index = self.model().index(0, 0)
        while index.isValid():
            self.__setBpEnabled(index, True)
            index = self.indexBelow(index)
        self.__resizeColumns()
        self.__resort()

    def __enableSelectedBreaks(self):
        """
        Private slot to handle the enable selected breakpoints context menu
        entry.
        """
        for index in self.selectedIndexes():
            if index.column() == 0:
                self.__setBpEnabled(index, True)
        self.__resizeColumns()
        self.__resort()

    def __disableBreak(self):
        """
        Private slot to handle the disable breakpoint context menu entry.
        """
        index = self.currentIndex()
        self.__setBpEnabled(index, False)
        self.__resizeColumns()
        self.__resort()

    def __disableAllBreaks(self):
        """
        Private slot to handle the disable all breakpoints context menu entry.
        """
        index = self.model().index(0, 0)
        while index.isValid():
            self.__setBpEnabled(index, False)
            index = self.indexBelow(index)
        self.__resizeColumns()
        self.__resort()

    def __disableSelectedBreaks(self):
        """
        Private slot to handle the disable selected breakpoints context menu
        entry.
        """
        for index in self.selectedIndexes():
            if index.column() == 0:
                self.__setBpEnabled(index, False)
        self.__resizeColumns()
        self.__resort()

    def __deleteBreak(self):
        """
        Private slot to handle the delete breakpoint context menu entry.
        """
        index = self.currentIndex()
        sindex = self.__toSourceIndex(index)
        if sindex.isValid():
            self.__model.deleteBreakPointByIndex(sindex)
        
    def __deleteAllBreaks(self):
        """
        Private slot to handle the delete all breakpoints context menu entry.
        """
        self.__model.deleteAll()

    def __deleteSelectedBreaks(self):
        """
        Private slot to handle the delete selected breakpoints context menu
        entry.
        """
        idxList = []
        for index in self.selectedIndexes():
            sindex = self.__toSourceIndex(index)
            if sindex.isValid() and index.column() == 0:
                idxList.append(sindex)
        self.__model.deleteBreakPoints(idxList)

    def __showSource(self):
        """
        Private slot to handle the goto context menu entry.
        """
        index = self.currentIndex()
        sindex = self.__toSourceIndex(index)
        bp = self.__model.getBreakPointByIndex(sindex)
        if not bp:
            return
        
        fn, line = bp[:2]
        self.sourceFile.emit(fn, line)
    
    def highlightBreakpoint(self, fn, lineno):
        """
        Public slot to handle the clientLine signal.
        
        @param fn filename of the breakpoint (string)
        @param lineno line number of the breakpoint (integer)
        """
        sindex = self.__model.getBreakPointIndex(fn, lineno)
        if sindex.isValid():
            return
        
        index = self.__fromSourceIndex(sindex)
        if index.isValid():
            self.__clearSelection()
            self.__setRowSelected(index, True)
    
    def handleResetUI(self):
        """
        Public slot to reset the breakpoint viewer.
        """
        self.__clearSelection()
    
    def __showBackMenu(self):
        """
        Private slot to handle the aboutToShow signal of the background menu.
        """
        if self.model().rowCount() == 0:
            self.backMenuActions["EnableAll"].setEnabled(False)
            self.backMenuActions["DisableAll"].setEnabled(False)
            self.backMenuActions["DeleteAll"].setEnabled(False)
        else:
            self.backMenuActions["EnableAll"].setEnabled(True)
            self.backMenuActions["DisableAll"].setEnabled(True)
            self.backMenuActions["DeleteAll"].setEnabled(True)

    def __getSelectedItemsCount(self):
        """
        Private method to get the count of items selected.
        
        @return count of items selected (integer)
        """
        count = len(self.selectedIndexes()) // (self.__model.columnCount() - 1)
        # column count is 1 greater than selectable
        return count
    
    def __configure(self):
        """
        Private method to open the configuration dialog.
        """
        e5App().getObject("UserInterface").showPreferences(
            "debuggerGeneralPage")
Exemplo n.º 3
0
class Explorer(QWidget):
    """
    This class implements the diagram predicate node explorer.
    """
    def __init__(self, mainwindow):
        """
        Initialize the Explorer.
        :type mainwindow: MainWindow
        """
        super().__init__(mainwindow)
        self.expanded = {}
        self.searched = {}
        self.scrolled = {}
        self.mainview = None
        self.iconA = QIcon(':/icons/treeview-icon-attribute')
        self.iconC = QIcon(':/icons/treeview-icon-concept')
        self.iconD = QIcon(':/icons/treeview-icon-datarange')
        self.iconI = QIcon(':/icons/treeview-icon-instance')
        self.iconR = QIcon(':/icons/treeview-icon-role')
        self.iconV = QIcon(':/icons/treeview-icon-value')
        self.search = StringField(self)
        self.search.setAcceptDrops(False)
        self.search.setClearButtonEnabled(True)
        self.search.setPlaceholderText('Search...')
        self.search.setFixedHeight(30)
        self.model = QStandardItemModel(self)
        self.proxy = QSortFilterProxyModel(self)
        self.proxy.setDynamicSortFilter(False)
        self.proxy.setFilterCaseSensitivity(Qt.CaseInsensitive)
        self.proxy.setSortCaseSensitivity(Qt.CaseSensitive)
        self.proxy.setSourceModel(self.model)
        self.view = ExplorerView(mainwindow, self)
        self.view.setModel(self.proxy)
        self.mainLayout = QVBoxLayout(self)
        self.mainLayout.setContentsMargins(0, 0, 0, 0)
        self.mainLayout.addWidget(self.search)
        self.mainLayout.addWidget(self.view)
        self.setContentsMargins(0, 0, 0, 0)
        self.setMinimumWidth(216)
        self.setMinimumHeight(160)

        connect(self.view.doubleClicked, self.itemDoubleClicked)
        connect(self.view.pressed, self.itemPressed)
        connect(self.view.collapsed, self.itemCollapsed)
        connect(self.view.expanded, self.itemExpanded)
        connect(self.search.textChanged, self.filterItem)

    ####################################################################################################################
    #                                                                                                                  #
    #   EVENTS                                                                                                         #
    #                                                                                                                  #
    ####################################################################################################################

    def paintEvent(self, paintEvent):
        """
        This is needed for the widget to pick the stylesheet.
        :type paintEvent: QPaintEvent
        """
        option = QStyleOption()
        option.initFrom(self)
        painter = QPainter(self)
        style = self.style()
        style.drawPrimitive(QStyle.PE_Widget, option, painter, self)

    ####################################################################################################################
    #                                                                                                                  #
    #   SLOTS                                                                                                          #
    #                                                                                                                  #
    ####################################################################################################################

    @pyqtSlot('QGraphicsItem')
    def add(self, item):
        """
        Add a node in the tree view.
        :type item: AbstractItem
        """
        if item.node and item.predicate:
            parent = self.parentFor(item)
            if not parent:
                parent = ParentItem(item)
                parent.setIcon(self.iconFor(item))
                self.model.appendRow(parent)
                self.proxy.sort(0, Qt.AscendingOrder)
            child = ChildItem(item)
            child.setData(item)
            parent.appendRow(child)
            self.proxy.sort(0, Qt.AscendingOrder)

    @pyqtSlot(str)
    def filterItem(self, key):
        """
        Executed when the search box is filled with data.
        :type key: str
        """
        if self.mainview:
            self.proxy.setFilterFixedString(key)
            self.proxy.sort(Qt.AscendingOrder)
            self.searched[self.mainview] = key

    @pyqtSlot('QModelIndex')
    def itemCollapsed(self, index):
        """
        Executed when an item in the tree view is collapsed.
        :type index: QModelIndex
        """
        if self.mainview:
            if self.mainview in self.expanded:
                item = self.model.itemFromIndex(self.proxy.mapToSource(index))
                expanded = self.expanded[self.mainview]
                expanded.remove(item.text())

    @pyqtSlot('QModelIndex')
    def itemDoubleClicked(self, index):
        """
        Executed when an item in the tree view is double clicked.
        :type index: QModelIndex
        """
        item = self.model.itemFromIndex(self.proxy.mapToSource(index))
        node = item.data()
        if node:
            self.selectNode(node)
            self.focusNode(node)

    @pyqtSlot('QModelIndex')
    def itemExpanded(self, index):
        """
        Executed when an item in the tree view is expanded.
        :type index: QModelIndex
        """
        if self.mainview:
            item = self.model.itemFromIndex(self.proxy.mapToSource(index))
            if self.mainview not in self.expanded:
                self.expanded[self.mainview] = set()
            expanded = self.expanded[self.mainview]
            expanded.add(item.text())

    @pyqtSlot('QModelIndex')
    def itemPressed(self, index):
        """
        Executed when an item in the tree view is clicked.
        :type index: QModelIndex
        """
        item = self.model.itemFromIndex(self.proxy.mapToSource(index))
        node = item.data()
        if node:
            self.selectNode(node)

    @pyqtSlot('QGraphicsItem')
    def remove(self, item):
        """
        Remove a node from the tree view.
        :type item: AbstractItem
        """
        if item.node and item.predicate:
            parent = self.parentFor(item)
            if parent:
                child = self.childFor(parent, item)
                if child:
                    parent.removeRow(child.index().row())
                if not parent.rowCount():
                    self.model.removeRow(parent.index().row())

    ####################################################################################################################
    #                                                                                                                  #
    #   AUXILIARY METHODS                                                                                              #
    #                                                                                                                  #
    ####################################################################################################################

    @staticmethod
    def childFor(parent, node):
        """
        Search the item representing this node among parent children.
        :type parent: QStandardItem
        :type node: AbstractNode
        """
        key = ChildItem.key(node)
        for i in range(parent.rowCount()):
            child = parent.child(i)
            if child.text() == key:
                return child
        return None

    def parentFor(self, node):
        """
        Search the parent element of the given node.
        :type node: AbstractNode
        :rtype: QStandardItem
        """
        key = ParentItem.key(node)
        for i in self.model.findItems(key, Qt.MatchExactly):
            n = i.child(0).data()
            if node.item is n.item:
                return i
        return None

    ####################################################################################################################
    #                                                                                                                  #
    #   INTERFACE                                                                                                      #
    #                                                                                                                  #
    ####################################################################################################################

    def browse(self, view):
        """
        Set the widget to inspect the given view.
        :type view: MainView
        """
        self.reset()
        self.mainview = view

        if self.mainview:

            scene = self.mainview.scene()
            connect(scene.index.sgnItemAdded, self.add)
            connect(scene.index.sgnItemRemoved, self.remove)

            for item in scene.index.nodes():
                self.add(item)

            if self.mainview in self.expanded:
                expanded = self.expanded[self.mainview]
                for i in range(self.model.rowCount()):
                    item = self.model.item(i)
                    index = self.proxy.mapFromSource(
                        self.model.indexFromItem(item))
                    self.view.setExpanded(index, item.text() in expanded)

            key = ''
            if self.mainview in self.searched:
                key = self.searched[self.mainview]
            self.search.setText(key)

            if self.mainview in self.scrolled:
                rect = self.rect()
                item = first(self.model.findItems(
                    self.scrolled[self.mainview]))
                for i in range(self.model.rowCount()):
                    self.view.scrollTo(
                        self.proxy.mapFromSource(
                            self.model.indexFromItem(self.model.item(i))))
                    index = self.proxy.mapToSource(
                        self.view.indexAt(rect.topLeft()))
                    if self.model.itemFromIndex(index) is item:
                        break

    def reset(self):
        """
        Clear the widget from inspecting the current view.
        """
        if self.mainview:

            rect = self.rect()
            item = self.model.itemFromIndex(
                self.proxy.mapToSource(self.view.indexAt(rect.topLeft())))
            if item:
                node = item.data()
                key = ParentItem.key(node) if node else item.text()
                self.scrolled[self.mainview] = key
            else:
                self.scrolled.pop(self.mainview, None)

            try:
                scene = self.mainview.scene()
                disconnect(scene.index.sgnItemAdded, self.add)
                disconnect(scene.index.sgnItemRemoved, self.remove)
            except RuntimeError:
                pass
            finally:
                self.mainview = None

        self.model.clear()

    def flush(self, view):
        """
        Flush the cache of the given mainview.
        :type view: MainView
        """
        self.expanded.pop(view, None)
        self.searched.pop(view, None)
        self.scrolled.pop(view, None)

    def iconFor(self, node):
        """
        Returns the icon for the given node.
        :type node:
        """
        if node.item is Item.AttributeNode:
            return self.iconA
        if node.item is Item.ConceptNode:
            return self.iconC
        if node.item is Item.ValueDomainNode:
            return self.iconD
        if node.item is Item.ValueRestrictionNode:
            return self.iconD
        if node.item is Item.IndividualNode:
            if node.identity is Identity.Instance:
                return self.iconI
            if node.identity is Identity.Value:
                return self.iconV
        if node.item is Item.RoleNode:
            return self.iconR

    def focusNode(self, node):
        """
        Focus the given node in the main view.
        :type node: AbstractNode
        """
        if self.mainview:
            self.mainview.centerOn(node)

    def selectNode(self, node):
        """
        Select the given node in the main view.
        :type node: AbstractNode
        """
        if self.mainview:
            scene = self.mainview.scene()
            scene.clearSelection()
            node.setSelected(True)
Exemplo n.º 4
0
class WatchPointViewer(QTreeView):
    """
    Class implementing the watch expression viewer widget.
    
    Watch expressions will be shown with all their details. They can be
    modified through the context menu of this widget.
    """
    def __init__(self, parent=None):
        """
        Constructor
        
        @param parent the parent (QWidget)
        """
        super(WatchPointViewer, self).__init__(parent)
        self.setObjectName("WatchExpressionViewer")

        self.__model = None

        self.setItemsExpandable(False)
        self.setRootIsDecorated(False)
        self.setAlternatingRowColors(True)
        self.setSelectionMode(QAbstractItemView.ExtendedSelection)
        self.setSelectionBehavior(QAbstractItemView.SelectRows)

        self.setWindowTitle(self.tr("Watchpoints"))

        self.setContextMenuPolicy(Qt.CustomContextMenu)
        self.customContextMenuRequested.connect(self.__showContextMenu)
        self.doubleClicked.connect(self.__doubleClicked)

        self.__createPopupMenus()

    def setModel(self, model):
        """
        Public slot to set the watch expression model.
        
        @param model reference to the watch expression model (WatchPointModel)
        """
        self.__model = model

        self.sortingModel = QSortFilterProxyModel()
        self.sortingModel.setDynamicSortFilter(True)
        self.sortingModel.setSourceModel(self.__model)
        super(WatchPointViewer, self).setModel(self.sortingModel)

        header = self.header()
        header.setSortIndicator(0, Qt.AscendingOrder)
        header.setSortIndicatorShown(True)
        if qVersion() >= "5.0.0":
            header.setSectionsClickable(True)
        else:
            header.setClickable(True)

        self.setSortingEnabled(True)

        self.__layoutDisplay()

    def __layoutDisplay(self):
        """
        Private slot to perform a layout operation.
        """
        self.__resizeColumns()
        self.__resort()

    def __resizeColumns(self):
        """
        Private slot to resize the view when items get added, edited or
        deleted.
        """
        self.header().resizeSections(QHeaderView.ResizeToContents)
        self.header().setStretchLastSection(True)

    def __resort(self):
        """
        Private slot to resort the tree.
        """
        self.model().sort(self.header().sortIndicatorSection(),
                          self.header().sortIndicatorOrder())

    def __toSourceIndex(self, index):
        """
        Private slot to convert an index to a source index.
        
        @param index index to be converted (QModelIndex)
        @return mapped index (QModelIndex)
        """
        return self.sortingModel.mapToSource(index)

    def __fromSourceIndex(self, sindex):
        """
        Private slot to convert a source index to an index.
        
        @param sindex source index to be converted (QModelIndex)
        @return mapped index (QModelIndex)
        """
        return self.sortingModel.mapFromSource(sindex)

    def __setRowSelected(self, index, selected=True):
        """
        Private slot to select a complete row.
        
        @param index index determining the row to be selected (QModelIndex)
        @param selected flag indicating the action (bool)
        """
        if not index.isValid():
            return

        if selected:
            flags = QItemSelectionModel.SelectionFlags(
                QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows)
        else:
            flags = QItemSelectionModel.SelectionFlags(
                QItemSelectionModel.Deselect | QItemSelectionModel.Rows)
        self.selectionModel().select(index, flags)

    def __createPopupMenus(self):
        """
        Private method to generate the popup menus.
        """
        self.menu = QMenu()
        self.menu.addAction(self.tr("Add"), self.__addWatchPoint)
        self.menu.addAction(self.tr("Edit..."), self.__editWatchPoint)
        self.menu.addSeparator()
        self.menu.addAction(self.tr("Enable"), self.__enableWatchPoint)
        self.menu.addAction(self.tr("Enable all"), self.__enableAllWatchPoints)
        self.menu.addSeparator()
        self.menu.addAction(self.tr("Disable"), self.__disableWatchPoint)
        self.menu.addAction(self.tr("Disable all"),
                            self.__disableAllWatchPoints)
        self.menu.addSeparator()
        self.menu.addAction(self.tr("Delete"), self.__deleteWatchPoint)
        self.menu.addAction(self.tr("Delete all"), self.__deleteAllWatchPoints)
        self.menu.addSeparator()
        self.menu.addAction(self.tr("Configure..."), self.__configure)

        self.backMenuActions = {}
        self.backMenu = QMenu()
        self.backMenu.addAction(self.tr("Add"), self.__addWatchPoint)
        self.backMenuActions["EnableAll"] = \
            self.backMenu.addAction(self.tr("Enable all"),
                                    self.__enableAllWatchPoints)
        self.backMenuActions["DisableAll"] = \
            self.backMenu.addAction(self.tr("Disable all"),
                                    self.__disableAllWatchPoints)
        self.backMenuActions["DeleteAll"] = \
            self.backMenu.addAction(self.tr("Delete all"),
                                    self.__deleteAllWatchPoints)
        self.backMenu.addSeparator()
        self.backMenu.addAction(self.tr("Configure..."), self.__configure)
        self.backMenu.aboutToShow.connect(self.__showBackMenu)

        self.multiMenu = QMenu()
        self.multiMenu.addAction(self.tr("Add"), self.__addWatchPoint)
        self.multiMenu.addSeparator()
        self.multiMenu.addAction(self.tr("Enable selected"),
                                 self.__enableSelectedWatchPoints)
        self.multiMenu.addAction(self.tr("Enable all"),
                                 self.__enableAllWatchPoints)
        self.multiMenu.addSeparator()
        self.multiMenu.addAction(self.tr("Disable selected"),
                                 self.__disableSelectedWatchPoints)
        self.multiMenu.addAction(self.tr("Disable all"),
                                 self.__disableAllWatchPoints)
        self.multiMenu.addSeparator()
        self.multiMenu.addAction(self.tr("Delete selected"),
                                 self.__deleteSelectedWatchPoints)
        self.multiMenu.addAction(self.tr("Delete all"),
                                 self.__deleteAllWatchPoints)
        self.multiMenu.addSeparator()
        self.multiMenu.addAction(self.tr("Configure..."), self.__configure)

    def __showContextMenu(self, coord):
        """
        Private slot to show the context menu.
        
        @param coord the position of the mouse pointer (QPoint)
        """
        cnt = self.__getSelectedItemsCount()
        if cnt <= 1:
            index = self.indexAt(coord)
            if index.isValid():
                cnt = 1
                self.__setRowSelected(index)
        coord = self.mapToGlobal(coord)
        if cnt > 1:
            self.multiMenu.popup(coord)
        elif cnt == 1:
            self.menu.popup(coord)
        else:
            self.backMenu.popup(coord)

    def __clearSelection(self):
        """
        Private slot to clear the selection.
        """
        for index in self.selectedIndexes():
            self.__setRowSelected(index, False)

    def __findDuplicates(self,
                         cond,
                         special,
                         showMessage=False,
                         index=QModelIndex()):
        """
        Private method to check, if an entry already exists.
        
        @param cond condition to check (string)
        @param special special condition to check (string)
        @param showMessage flag indicating a message should be shown,
            if a duplicate entry is found (boolean)
        @param index index that should not be considered duplicate
            (QModelIndex)
        @return flag indicating a duplicate entry (boolean)
        """
        idx = self.__model.getWatchPointIndex(cond, special)
        duplicate = idx.isValid() and \
            idx.internalPointer() != index.internalPointer()
        if showMessage and duplicate:
            if not special:
                msg = self.tr("""<p>A watch expression '<b>{0}</b>'"""
                              """ already exists.</p>""")\
                    .format(Utilities.html_encode(cond))
            else:
                msg = self.tr(
                    """<p>A watch expression '<b>{0}</b>'"""
                    """ for the variable <b>{1}</b> already exists.</p>""")\
                    .format(special, Utilities.html_encode(cond))
            E5MessageBox.warning(self,
                                 self.tr("Watch expression already exists"),
                                 msg)

        return duplicate

    def __addWatchPoint(self):
        """
        Private slot to handle the add watch expression context menu entry.
        """
        from .EditWatchpointDialog import EditWatchpointDialog
        dlg = EditWatchpointDialog(("", False, True, 0, ""), self)
        if dlg.exec_() == QDialog.Accepted:
            cond, temp, enabled, ignorecount, special = dlg.getData()
            if not self.__findDuplicates(cond, special, True):
                self.__model.addWatchPoint(cond, special,
                                           (temp, enabled, ignorecount))
                self.__resizeColumns()
                self.__resort()

    def __doubleClicked(self, index):
        """
        Private slot to handle the double clicked signal.
        
        @param index index of the entry that was double clicked (QModelIndex)
        """
        if index.isValid():
            self.__doEditWatchPoint(index)

    def __editWatchPoint(self):
        """
        Private slot to handle the edit watch expression context menu entry.
        """
        index = self.currentIndex()
        if index.isValid():
            self.__doEditWatchPoint(index)

    def __doEditWatchPoint(self, index):
        """
        Private slot to edit a watch expression.
        
        @param index index of watch expression to be edited (QModelIndex)
        """
        sindex = self.__toSourceIndex(index)
        if sindex.isValid():
            wp = self.__model.getWatchPointByIndex(sindex)
            if not wp:
                return

            cond, special, temp, enabled, count = wp[:5]

            from .EditWatchpointDialog import EditWatchpointDialog
            dlg = EditWatchpointDialog((cond, temp, enabled, count, special),
                                       self)
            if dlg.exec_() == QDialog.Accepted:
                cond, temp, enabled, count, special = dlg.getData()
                if not self.__findDuplicates(cond, special, True, sindex):
                    self.__model.setWatchPointByIndex(sindex, cond, special,
                                                      (temp, enabled, count))
                    self.__resizeColumns()
                    self.__resort()

    def __setWpEnabled(self, index, enabled):
        """
        Private method to set the enabled status of a watch expression.
        
        @param index index of watch expression to be enabled/disabled
            (QModelIndex)
        @param enabled flag indicating the enabled status to be set (boolean)
        """
        sindex = self.__toSourceIndex(index)
        if sindex.isValid():
            self.__model.setWatchPointEnabledByIndex(sindex, enabled)

    def __enableWatchPoint(self):
        """
        Private slot to handle the enable watch expression context menu entry.
        """
        index = self.currentIndex()
        self.__setWpEnabled(index, True)
        self.__resizeColumns()
        self.__resort()

    def __enableAllWatchPoints(self):
        """
        Private slot to handle the enable all watch expressions context menu
        entry.
        """
        index = self.model().index(0, 0)
        while index.isValid():
            self.__setWpEnabled(index, True)
            index = self.indexBelow(index)
        self.__resizeColumns()
        self.__resort()

    def __enableSelectedWatchPoints(self):
        """
        Private slot to handle the enable selected watch expressions context
        menu entry.
        """
        for index in self.selectedIndexes():
            if index.column() == 0:
                self.__setWpEnabled(index, True)
        self.__resizeColumns()
        self.__resort()

    def __disableWatchPoint(self):
        """
        Private slot to handle the disable watch expression context menu entry.
        """
        index = self.currentIndex()
        self.__setWpEnabled(index, False)
        self.__resizeColumns()
        self.__resort()

    def __disableAllWatchPoints(self):
        """
        Private slot to handle the disable all watch expressions context menu
        entry.
        """
        index = self.model().index(0, 0)
        while index.isValid():
            self.__setWpEnabled(index, False)
            index = self.indexBelow(index)
        self.__resizeColumns()
        self.__resort()

    def __disableSelectedWatchPoints(self):
        """
        Private slot to handle the disable selected watch expressions context
        menu entry.
        """
        for index in self.selectedIndexes():
            if index.column() == 0:
                self.__setWpEnabled(index, False)
        self.__resizeColumns()
        self.__resort()

    def __deleteWatchPoint(self):
        """
        Private slot to handle the delete watch expression context menu entry.
        """
        index = self.currentIndex()
        sindex = self.__toSourceIndex(index)
        if sindex.isValid():
            self.__model.deleteWatchPointByIndex(sindex)

    def __deleteAllWatchPoints(self):
        """
        Private slot to handle the delete all watch expressions context menu
        entry.
        """
        self.__model.deleteAll()

    def __deleteSelectedWatchPoints(self):
        """
        Private slot to handle the delete selected watch expressions context
        menu entry.
        """
        idxList = []
        for index in self.selectedIndexes():
            sindex = self.__toSourceIndex(index)
            if sindex.isValid() and index.column() == 0:
                idxList.append(sindex)
        self.__model.deleteWatchPoints(idxList)

    def __showBackMenu(self):
        """
        Private slot to handle the aboutToShow signal of the background menu.
        """
        if self.model().rowCount() == 0:
            self.backMenuActions["EnableAll"].setEnabled(False)
            self.backMenuActions["DisableAll"].setEnabled(False)
            self.backMenuActions["DeleteAll"].setEnabled(False)
        else:
            self.backMenuActions["EnableAll"].setEnabled(True)
            self.backMenuActions["DisableAll"].setEnabled(True)
            self.backMenuActions["DeleteAll"].setEnabled(True)

    def __getSelectedItemsCount(self):
        """
        Private method to get the count of items selected.
        
        @return count of items selected (integer)
        """
        count = len(self.selectedIndexes()) // (self.__model.columnCount() - 1)
        # column count is 1 greater than selectable
        return count

    def __configure(self):
        """
        Private method to open the configuration dialog.
        """
        e5App().getObject("UserInterface")\
            .showPreferences("debuggerGeneralPage")
Exemplo n.º 5
0
class TileStampsDock(QDockWidget):
    setStamp = pyqtSignal(TileStamp)

    def __init__(self, stampManager, parent=None):
        super().__init__(parent)

        self.mTileStampManager = stampManager
        self.mTileStampModel = stampManager.tileStampModel()
        self.mProxyModel = QSortFilterProxyModel(self.mTileStampModel)
        self.mFilterEdit = QLineEdit(self)
        self.mNewStamp = QAction(self)
        self.mAddVariation = QAction(self)
        self.mDuplicate = QAction(self)
        self.mDelete = QAction(self)
        self.mChooseFolder = QAction(self)

        self.setObjectName("TileStampsDock")
        self.mProxyModel.setSortLocaleAware(True)
        self.mProxyModel.setSortCaseSensitivity(Qt.CaseInsensitive)
        self.mProxyModel.setFilterCaseSensitivity(Qt.CaseInsensitive)
        self.mProxyModel.setSourceModel(self.mTileStampModel)
        self.mProxyModel.sort(0)
        self.mTileStampView = TileStampView(self)
        self.mTileStampView.setModel(self.mProxyModel)
        self.mTileStampView.setVerticalScrollMode(
            QAbstractItemView.ScrollPerPixel)
        self.mTileStampView.header().setStretchLastSection(False)
        self.mTileStampView.header().setSectionResizeMode(
            0, QHeaderView.Stretch)
        self.mTileStampView.header().setSectionResizeMode(
            1, QHeaderView.ResizeToContents)
        self.mTileStampView.setContextMenuPolicy(Qt.CustomContextMenu)
        self.mTileStampView.customContextMenuRequested.connect(
            self.showContextMenu)
        self.mNewStamp.setIcon(QIcon(":images/16x16/document-new.png"))
        self.mAddVariation.setIcon(QIcon(":/images/16x16/add.png"))
        self.mDuplicate.setIcon(QIcon(":/images/16x16/stock-duplicate-16.png"))
        self.mDelete.setIcon(QIcon(":images/16x16/edit-delete.png"))
        self.mChooseFolder.setIcon(QIcon(":images/16x16/document-open.png"))
        Utils.setThemeIcon(self.mNewStamp, "document-new")
        Utils.setThemeIcon(self.mAddVariation, "add")
        Utils.setThemeIcon(self.mDelete, "edit-delete")
        Utils.setThemeIcon(self.mChooseFolder, "document-open")

        self.mFilterEdit.setClearButtonEnabled(True)
        self.mFilterEdit.textChanged.connect(
            self.mProxyModel.setFilterFixedString)
        self.mTileStampModel.stampRenamed.connect(self.ensureStampVisible)
        self.mNewStamp.triggered.connect(self.newStamp)
        self.mAddVariation.triggered.connect(self.addVariation)
        self.mDuplicate.triggered.connect(self.duplicate)
        self.mDelete.triggered.connect(self.delete_)
        self.mChooseFolder.triggered.connect(self.chooseFolder)
        self.mDuplicate.setEnabled(False)
        self.mDelete.setEnabled(False)
        self.mAddVariation.setEnabled(False)
        widget = QWidget(self)
        layout = QVBoxLayout(widget)
        layout.setContentsMargins(5, 5, 5, 5)

        buttonContainer = QToolBar()
        buttonContainer.setFloatable(False)
        buttonContainer.setMovable(False)
        buttonContainer.setIconSize(QSize(16, 16))
        buttonContainer.addAction(self.mNewStamp)
        buttonContainer.addAction(self.mAddVariation)
        buttonContainer.addAction(self.mDuplicate)
        buttonContainer.addAction(self.mDelete)
        stretch = QWidget()
        stretch.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)
        buttonContainer.addWidget(stretch)
        buttonContainer.addAction(self.mChooseFolder)
        listAndToolBar = QVBoxLayout()
        listAndToolBar.setSpacing(0)
        listAndToolBar.addWidget(self.mFilterEdit)
        listAndToolBar.addWidget(self.mTileStampView)
        listAndToolBar.addWidget(buttonContainer)
        layout.addLayout(listAndToolBar)
        selectionModel = self.mTileStampView.selectionModel()
        selectionModel.currentRowChanged.connect(self.currentRowChanged)
        self.setWidget(widget)
        self.retranslateUi()

    def changeEvent(self, e):
        super().changeEvent(e)
        x = e.type()
        if x == QEvent.LanguageChange:
            self.retranslateUi()
        else:
            pass

    def keyPressEvent(self, event):
        x = event.key()
        if x == Qt.Key_Delete or x == Qt.Key_Backspace:
            self.delete_()
            return

        super().keyPressEvent(event)

    def currentRowChanged(self, index):
        sourceIndex = self.mProxyModel.mapToSource(index)
        isStamp = self.mTileStampModel.isStamp(sourceIndex)
        self.mDuplicate.setEnabled(isStamp)
        self.mDelete.setEnabled(sourceIndex.isValid())
        self.mAddVariation.setEnabled(isStamp)
        if (isStamp):
            self.setStamp.emit(self.mTileStampModel.stampAt(sourceIndex))
        else:
            variation = self.mTileStampModel.variationAt(sourceIndex)
            if variation:
                # single variation clicked, use it specifically
                self.setStamp.emit(TileStamp(Map(variation.map)))

    def showContextMenu(self, pos):
        index = self.mTileStampView.indexAt(pos)
        if (not index.isValid()):
            return
        menu = QMenu()
        sourceIndex = self.mProxyModel.mapToSource(index)
        if (self.mTileStampModel.isStamp(sourceIndex)):
            addStampVariation = QAction(self.mAddVariation.icon(),
                                        self.mAddVariation.text(), menu)
            deleteStamp = QAction(self.mDelete.icon(), self.tr("Delete Stamp"),
                                  menu)
            deleteStamp.triggered.connect(self.delete_)
            addStampVariation.triggered.connect(self.addVariation)
            menu.addAction(addStampVariation)
            menu.addSeparator()
            menu.addAction(deleteStamp)
        else:
            removeVariation = QAction(QIcon(":/images/16x16/remove.png"),
                                      self.tr("Remove Variation"), menu)
            Utils.setThemeIcon(removeVariation, "remove")
            removeVariation.triggered.connect(self.delete_)
            menu.addAction(removeVariation)

        menu.exec(self.mTileStampView.viewport().mapToGlobal(pos))

    def newStamp(self):
        stamp = self.mTileStampManager.createStamp()
        if (self.isVisible() and not stamp.isEmpty()):
            stampIndex = self.mTileStampModel.index(stamp)
            if (stampIndex.isValid()):
                viewIndex = self.mProxyModel.mapFromSource(stampIndex)
                self.mTileStampView.setCurrentIndex(viewIndex)
                self.mTileStampView.edit(viewIndex)

    def delete_(self):
        index = self.mTileStampView.currentIndex()
        if (not index.isValid()):
            return
        sourceIndex = self.mProxyModel.mapToSource(index)
        self.mTileStampModel.removeRow(sourceIndex.row(), sourceIndex.parent())

    def duplicate(self):
        index = self.mTileStampView.currentIndex()
        if (not index.isValid()):
            return
        sourceIndex = self.mProxyModel.mapToSource(index)
        if (not self.mTileStampModel.isStamp(sourceIndex)):
            return
        stamp = self.mTileStampModel.stampAt = TileStamp(sourceIndex)
        self.mTileStampModel.addStamp(stamp.clone())

    def addVariation(self):
        index = self.mTileStampView.currentIndex()
        if (not index.isValid()):
            return
        sourceIndex = self.mProxyModel.mapToSource(index)
        if (not self.mTileStampModel.isStamp(sourceIndex)):
            return
        stamp = self.mTileStampModel.stampAt(sourceIndex)
        self.mTileStampManager.addVariation(stamp)

    def chooseFolder(self):
        prefs = Preferences.instance()
        stampsDirectory = prefs.stampsDirectory()
        stampsDirectory = QFileDialog.getExistingDirectory(
            self.window(), self.tr("Choose the Stamps Folder"),
            stampsDirectory)
        if (not stampsDirectory.isEmpty()):
            prefs.setStampsDirectory(stampsDirectory)

    def ensureStampVisible(self, stamp):
        stampIndex = self.mTileStampModel.index(stamp)
        if (stampIndex.isValid()):
            self.mTileStampView.scrollTo(
                self.mProxyModel.mapFromSource(stampIndex))

    def retranslateUi(self):
        self.setWindowTitle(self.tr("Tile Stamps"))
        self.mNewStamp.setText(self.tr("Add New Stamp"))
        self.mAddVariation.setText(self.tr("Add Variation"))
        self.mDuplicate.setText(self.tr("Duplicate Stamp"))
        self.mDelete.setText(self.tr("Delete Selected"))
        self.mChooseFolder.setText(self.tr("Set Stamps Folder"))
        self.mFilterEdit.setPlaceholderText(self.tr("Filter"))
Exemplo n.º 6
0
class BreakPointViewer(QTreeView):
    """
    Class implementing the Breakpoint viewer widget.
    
    Breakpoints will be shown with all their details. They can be modified
    through the context menu of this widget.
    
    @signal sourceFile(str, int) emitted to show the source of a breakpoint
    """
    sourceFile = pyqtSignal(str, int)

    def __init__(self, parent=None):
        """
        Constructor
        
        @param parent the parent (QWidget)
        """
        super(BreakPointViewer, self).__init__(parent)
        self.setObjectName("BreakPointViewer")

        self.__model = None

        self.setItemsExpandable(False)
        self.setRootIsDecorated(False)
        self.setAlternatingRowColors(True)
        self.setSelectionMode(QAbstractItemView.ExtendedSelection)
        self.setSelectionBehavior(QAbstractItemView.SelectRows)

        self.setWindowTitle(self.tr("Breakpoints"))

        self.setContextMenuPolicy(Qt.CustomContextMenu)
        self.customContextMenuRequested.connect(self.__showContextMenu)
        self.doubleClicked.connect(self.__doubleClicked)

        self.__createPopupMenus()

        self.condHistory = []
        self.fnHistory = []
        self.fnHistory.append('')

        self.__loadRecent()

    def setModel(self, model):
        """
        Public slot to set the breakpoint model.
        
        @param model reference to the breakpoint model (BreakPointModel)
        """
        self.__model = model

        self.sortingModel = QSortFilterProxyModel()
        self.sortingModel.setDynamicSortFilter(True)
        self.sortingModel.setSourceModel(self.__model)
        super(BreakPointViewer, self).setModel(self.sortingModel)

        header = self.header()
        header.setSortIndicator(0, Qt.AscendingOrder)
        header.setSortIndicatorShown(True)
        header.setSectionsClickable(True)

        self.setSortingEnabled(True)

        self.__layoutDisplay()

    def __layoutDisplay(self):
        """
        Private slot to perform a layout operation.
        """
        self.__resizeColumns()
        self.__resort()

    def __resizeColumns(self):
        """
        Private slot to resize the view when items get added, edited or
        deleted.
        """
        self.header().resizeSections(QHeaderView.ResizeToContents)
        self.header().setStretchLastSection(True)

    def __resort(self):
        """
        Private slot to resort the tree.
        """
        self.model().sort(self.header().sortIndicatorSection(),
                          self.header().sortIndicatorOrder())

    def __toSourceIndex(self, index):
        """
        Private slot to convert an index to a source index.
        
        @param index index to be converted (QModelIndex)
        @return mapped index (QModelIndex)
        """
        return self.sortingModel.mapToSource(index)

    def __fromSourceIndex(self, sindex):
        """
        Private slot to convert a source index to an index.
        
        @param sindex source index to be converted (QModelIndex)
        @return mapped index (QModelIndex)
        """
        return self.sortingModel.mapFromSource(sindex)

    def __setRowSelected(self, index, selected=True):
        """
        Private slot to select a complete row.
        
        @param index index determining the row to be selected (QModelIndex)
        @param selected flag indicating the action (bool)
        """
        if not index.isValid():
            return

        if selected:
            flags = QItemSelectionModel.SelectionFlags(
                QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows)
        else:
            flags = QItemSelectionModel.SelectionFlags(
                QItemSelectionModel.Deselect | QItemSelectionModel.Rows)
        self.selectionModel().select(index, flags)

    def __createPopupMenus(self):
        """
        Private method to generate the popup menus.
        """
        self.menu = QMenu()
        self.menu.addAction(self.tr("Add"), self.__addBreak)
        self.menu.addAction(self.tr("Edit..."), self.__editBreak)
        self.menu.addSeparator()
        self.menu.addAction(self.tr("Enable"), self.__enableBreak)
        self.menu.addAction(self.tr("Enable all"), self.__enableAllBreaks)
        self.menu.addSeparator()
        self.menu.addAction(self.tr("Disable"), self.__disableBreak)
        self.menu.addAction(self.tr("Disable all"), self.__disableAllBreaks)
        self.menu.addSeparator()
        self.menu.addAction(self.tr("Delete"), self.__deleteBreak)
        self.menu.addAction(self.tr("Delete all"), self.__deleteAllBreaks)
        self.menu.addSeparator()
        self.menu.addAction(self.tr("Goto"), self.__showSource)
        self.menu.addSeparator()
        self.menu.addAction(self.tr("Configure..."), self.__configure)

        self.backMenuActions = {}
        self.backMenu = QMenu()
        self.backMenu.addAction(self.tr("Add"), self.__addBreak)
        self.backMenuActions["EnableAll"] = self.backMenu.addAction(
            self.tr("Enable all"), self.__enableAllBreaks)
        self.backMenuActions["DisableAll"] = self.backMenu.addAction(
            self.tr("Disable all"), self.__disableAllBreaks)
        self.backMenuActions["DeleteAll"] = self.backMenu.addAction(
            self.tr("Delete all"), self.__deleteAllBreaks)
        self.backMenu.aboutToShow.connect(self.__showBackMenu)
        self.backMenu.addSeparator()
        self.backMenu.addAction(self.tr("Configure..."), self.__configure)

        self.multiMenu = QMenu()
        self.multiMenu.addAction(self.tr("Add"), self.__addBreak)
        self.multiMenu.addSeparator()
        self.multiMenu.addAction(self.tr("Enable selected"),
                                 self.__enableSelectedBreaks)
        self.multiMenu.addAction(self.tr("Enable all"), self.__enableAllBreaks)
        self.multiMenu.addSeparator()
        self.multiMenu.addAction(self.tr("Disable selected"),
                                 self.__disableSelectedBreaks)
        self.multiMenu.addAction(self.tr("Disable all"),
                                 self.__disableAllBreaks)
        self.multiMenu.addSeparator()
        self.multiMenu.addAction(self.tr("Delete selected"),
                                 self.__deleteSelectedBreaks)
        self.multiMenu.addAction(self.tr("Delete all"), self.__deleteAllBreaks)
        self.multiMenu.addSeparator()
        self.multiMenu.addAction(self.tr("Configure..."), self.__configure)

    def __showContextMenu(self, coord):
        """
        Private slot to show the context menu.
        
        @param coord the position of the mouse pointer (QPoint)
        """
        cnt = self.__getSelectedItemsCount()
        if cnt <= 1:
            index = self.indexAt(coord)
            if index.isValid():
                cnt = 1
                self.__setRowSelected(index)
        coord = self.mapToGlobal(coord)
        if cnt > 1:
            self.multiMenu.popup(coord)
        elif cnt == 1:
            self.menu.popup(coord)
        else:
            self.backMenu.popup(coord)

    def __clearSelection(self):
        """
        Private slot to clear the selection.
        """
        for index in self.selectedIndexes():
            self.__setRowSelected(index, False)

    def __addBreak(self):
        """
        Private slot to handle the add breakpoint context menu entry.
        """
        from .EditBreakpointDialog import EditBreakpointDialog

        dlg = EditBreakpointDialog((self.fnHistory[0], None),
                                   None,
                                   self.condHistory,
                                   self,
                                   modal=1,
                                   addMode=1,
                                   filenameHistory=self.fnHistory)
        if dlg.exec_() == QDialog.Accepted:
            fn, line, cond, temp, enabled, count = dlg.getAddData()
            if fn is not None:
                if fn in self.fnHistory:
                    self.fnHistory.remove(fn)
                self.fnHistory.insert(0, fn)

            if cond:
                if cond in self.condHistory:
                    self.condHistory.remove(cond)
                self.condHistory.insert(0, cond)

            self.__saveRecent()

            self.__model.addBreakPoint(fn, line, (cond, temp, enabled, count))
            self.__resizeColumns()
            self.__resort()

    def __doubleClicked(self, index):
        """
        Private slot to handle the double clicked signal.
        
        @param index index of the entry that was double clicked (QModelIndex)
        """
        if index.isValid():
            self.__editBreakpoint(index)

    def __editBreak(self):
        """
        Private slot to handle the edit breakpoint context menu entry.
        """
        index = self.currentIndex()
        if index.isValid():
            self.__editBreakpoint(index)

    def __editBreakpoint(self, index):
        """
        Private slot to edit a breakpoint.
        
        @param index index of breakpoint to be edited (QModelIndex)
        """
        sindex = self.__toSourceIndex(index)
        if sindex.isValid():
            bp = self.__model.getBreakPointByIndex(sindex)
            if not bp:
                return

            fn, line, cond, temp, enabled, count = bp[:6]

            from .EditBreakpointDialog import EditBreakpointDialog
            dlg = EditBreakpointDialog((fn, line),
                                       (cond, temp, enabled, count),
                                       self.condHistory,
                                       self,
                                       modal=True)
            if dlg.exec_() == QDialog.Accepted:
                cond, temp, enabled, count = dlg.getData()
                if cond:
                    if cond in self.condHistory:
                        self.condHistory.remove(cond)
                    self.condHistory.insert(0, cond)

                    self.__saveRecent()

                self.__model.setBreakPointByIndex(sindex, fn, line,
                                                  (cond, temp, enabled, count))
                self.__resizeColumns()
                self.__resort()

    def __setBpEnabled(self, index, enabled):
        """
        Private method to set the enabled status of a breakpoint.
        
        @param index index of breakpoint to be enabled/disabled (QModelIndex)
        @param enabled flag indicating the enabled status to be set (boolean)
        """
        sindex = self.__toSourceIndex(index)
        if sindex.isValid():
            self.__model.setBreakPointEnabledByIndex(sindex, enabled)

    def __enableBreak(self):
        """
        Private slot to handle the enable breakpoint context menu entry.
        """
        index = self.currentIndex()
        self.__setBpEnabled(index, True)
        self.__resizeColumns()
        self.__resort()

    def __enableAllBreaks(self):
        """
        Private slot to handle the enable all breakpoints context menu entry.
        """
        index = self.model().index(0, 0)
        while index.isValid():
            self.__setBpEnabled(index, True)
            index = self.indexBelow(index)
        self.__resizeColumns()
        self.__resort()

    def __enableSelectedBreaks(self):
        """
        Private slot to handle the enable selected breakpoints context menu
        entry.
        """
        for index in self.selectedIndexes():
            if index.column() == 0:
                self.__setBpEnabled(index, True)
        self.__resizeColumns()
        self.__resort()

    def __disableBreak(self):
        """
        Private slot to handle the disable breakpoint context menu entry.
        """
        index = self.currentIndex()
        self.__setBpEnabled(index, False)
        self.__resizeColumns()
        self.__resort()

    def __disableAllBreaks(self):
        """
        Private slot to handle the disable all breakpoints context menu entry.
        """
        index = self.model().index(0, 0)
        while index.isValid():
            self.__setBpEnabled(index, False)
            index = self.indexBelow(index)
        self.__resizeColumns()
        self.__resort()

    def __disableSelectedBreaks(self):
        """
        Private slot to handle the disable selected breakpoints context menu
        entry.
        """
        for index in self.selectedIndexes():
            if index.column() == 0:
                self.__setBpEnabled(index, False)
        self.__resizeColumns()
        self.__resort()

    def __deleteBreak(self):
        """
        Private slot to handle the delete breakpoint context menu entry.
        """
        index = self.currentIndex()
        sindex = self.__toSourceIndex(index)
        if sindex.isValid():
            self.__model.deleteBreakPointByIndex(sindex)

    def __deleteAllBreaks(self):
        """
        Private slot to handle the delete all breakpoints context menu entry.
        """
        self.__model.deleteAll()

    def __deleteSelectedBreaks(self):
        """
        Private slot to handle the delete selected breakpoints context menu
        entry.
        """
        idxList = []
        for index in self.selectedIndexes():
            sindex = self.__toSourceIndex(index)
            if sindex.isValid() and index.column() == 0:
                idxList.append(sindex)
        self.__model.deleteBreakPoints(idxList)

    def __showSource(self):
        """
        Private slot to handle the goto context menu entry.
        """
        index = self.currentIndex()
        sindex = self.__toSourceIndex(index)
        bp = self.__model.getBreakPointByIndex(sindex)
        if not bp:
            return

        fn, line = bp[:2]
        self.sourceFile.emit(fn, line)

    def highlightBreakpoint(self, fn, lineno):
        """
        Public slot to handle the clientLine signal.
        
        @param fn filename of the breakpoint (string)
        @param lineno line number of the breakpoint (integer)
        """
        sindex = self.__model.getBreakPointIndex(fn, lineno)
        if sindex.isValid():
            return

        index = self.__fromSourceIndex(sindex)
        if index.isValid():
            self.__clearSelection()
            self.__setRowSelected(index, True)

    def handleResetUI(self):
        """
        Public slot to reset the breakpoint viewer.
        """
        self.__clearSelection()

    def __showBackMenu(self):
        """
        Private slot to handle the aboutToShow signal of the background menu.
        """
        if self.model().rowCount() == 0:
            self.backMenuActions["EnableAll"].setEnabled(False)
            self.backMenuActions["DisableAll"].setEnabled(False)
            self.backMenuActions["DeleteAll"].setEnabled(False)
        else:
            self.backMenuActions["EnableAll"].setEnabled(True)
            self.backMenuActions["DisableAll"].setEnabled(True)
            self.backMenuActions["DeleteAll"].setEnabled(True)

    def __getSelectedItemsCount(self):
        """
        Private method to get the count of items selected.
        
        @return count of items selected (integer)
        """
        count = len(self.selectedIndexes()) // (self.__model.columnCount() - 1)
        # column count is 1 greater than selectable
        return count

    def __configure(self):
        """
        Private method to open the configuration dialog.
        """
        e5App().getObject("UserInterface").showPreferences(
            "debuggerGeneralPage")

    def __loadRecent(self):
        """
        Private method to load the recently used file names.
        """
        Preferences.Prefs.rsettings.sync()

        # load recently used file names
        self.fnHistory = []
        self.fnHistory.append('')
        rs = Preferences.Prefs.rsettings.value(recentNameBreakpointFiles)
        if rs is not None:
            recent = [
                f for f in Preferences.toList(rs) if QFileInfo(f).exists()
            ]
            self.fnHistory.extend(
                recent[:Preferences.getDebugger("RecentNumber")])

        # load recently entered condition expressions
        self.condHistory = []
        rs = Preferences.Prefs.rsettings.value(recentNameBreakpointConditions)
        if rs is not None:
            self.condHistory = Preferences.toList(
                rs)[:Preferences.getDebugger("RecentNumber")]

    def __saveRecent(self):
        """
        Private method to save the list of recently used file names.
        """
        recent = [f for f in self.fnHistory if f]
        Preferences.Prefs.rsettings.setValue(recentNameBreakpointFiles, recent)
        Preferences.Prefs.rsettings.setValue(recentNameBreakpointConditions,
                                             self.condHistory)
        Preferences.Prefs.rsettings.sync()
Exemplo n.º 7
0
class MainWindow(QMainWindow):
    """Controller class for VmsParser

    ### Args:
        QMainWindow ([class]): Create the controller class
    """

    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)
        #Load VIEW from resource file
        uic.loadUi(self.resourcePath('gui.ui'), self)


        #Create first data for the model
        self.data = [VAMAS_File()]
        #Create the MODEL
        self.model = ParameterModel(self.data)

        #Selected first model column
        self.selectedModelColumn = 1
     

        #Create the maptlotlib FigureCanvas object, 
        #which defines a single set of axes as self.axes.
        self.spectralPlot = MplCanvas(self, width=8, height=4, dpi=100)
        #self.spectralPlot.axes.plot([0,1,2,3,4], [10,1,20,3,40])
        toolbar = NavigationToolbar(self.spectralPlot, self)
        self.plotToolBar.addWidget(toolbar)
        self.plotWidget.addWidget(self.spectralPlot)        

        #Popup menu event
        self.dataSelector.currentIndexChanged.connect(lambda index: (self.selectModelColumn(index+1)))
        self.dataSelector.setCurrentIndex(0)    

        #Display model as table
        self.vmsTable.setModel(self.model)
        self.vmsTable.verticalHeader().setMaximumWidth(160)
        self.vmsTable.setColumnWidth(0, 30)
        #Hide the first row with model-column selector checkbox
        self.vmsTable.hideRow(0)
        self.model.dataChanged.connect(lambda index: (self.modelEditedEvent(index)))
        
        #Proxy model with swapped rows/columns for paramTabel
        self.proxy = QTransposeProxyModel()
        self.proxy.setSourceModel(self.model)

        #Sorting model
        self.proxy2 = QSortFilterProxyModel()
        self.proxy2.setSourceModel(self.proxy)

        #Display sorted proxy model for param table
        self.paramTable.setModel(self.proxy2)
        #Show only selected columns
        self.initSelectedFields()  
        #Movable columns
        self.paramTable.horizontalHeader().setSectionsMovable(True)
        self.paramTable.setColumnWidth(0, 30)
        #Double click event selects row from model and updates view
        self.paramTable.doubleClicked.connect(lambda index: (self.selectModelColumn(self.proxy2.mapToSource(index).row())))
        #Hide first row with model-row selector checkboxes
        self.paramTable.hideRow(0)

        #Menu Bar events
        self.actionSave.triggered.connect(self.saveModel)
        self.actionLoad.triggered.connect(self.loadModel)  
        self.actionAppend_Files.triggered.connect(self.appendData)                
        self.actionQuit.triggered.connect(self.close)
    
        #Button events
        self.buttonPrevArea.clicked.connect(self.goToPreviousColumn)
        
        self.buttonNextArea.clicked.connect(self.goToNextColumn)

        self.colNumber.setText("0/0")

        self.show()

    def modelEditedEvent(self, index):
        """Event triggered when model data changed by clicking a checkbox

        ### Arguments:
            index {QModelIndex} -- row/colum of changed data
        """
        print("Model edited event row {0}, column {1} changed".format(index.row(), index.column()))
        #First column changes selected fields which are displayed in paramTable
        if index.column() == 0:
            if self.model.selectedRows[index.row()-1] == True:
                self.paramTable.showColumn(index.row())
            else:
                self.paramTable.hideColumn(index.row())

        #First row changes data columns to be plotted
        if index.row() == 0:
            self.updatePlot()


    def initSelectedFields(self):
        """Init selected dataclass fields = columns in param Table with default values
        """
        #Show only selected columns
        visibleColumns = ["sampleName", "blockName", "posName", "date", "technique", "analyserSettingStr", "analyzerPEorRR", "dwellTime"]
        for r in range(self.proxy.columnCount()):
            if not self.proxy.headerData(r, Qt.Horizontal) in visibleColumns:
                self.paramTable.hideColumn(r)
            else:
                self.model.selectedRows[r-1] = True
        self.paramTable.showColumn(0)  
    
    def goToNextColumn(self):
        """Select next column from model
        """
        if self.selectedModelColumn < self.model.columnCount()-1:
            self.selectedModelColumn+=1
            self.updateSelectedData()
    
    def goToPreviousColumn(self):
        """Select previous column from model
        """
        if self.selectedModelColumn > 1:
            self.selectedModelColumn-=1
            self.updateSelectedData()            
    
    def selectModelColumn(self, index):
        """Select indexed data column from the model, update the datamapper and view

        ### Arguments:
            index {int} -- Model column index
        """
        # self.dataMapper.submit()
        #self.dataMapper.setCurrentIndex(index)
        print("Selecting model column " + str(index))
        self.selectedModelColumn = index
        self.updateSelectedData()


    def updateSelectedData(self):
        """update dataSelector PopupMenu, Table and plot after a different data column selected from the model
        """
        #Update colNumber label
        self.colNumber.setText(str(self.selectedModelColumn) + "/" + str(self.model.columnCount()-1))
        #Select corresponding line in popupmenu without triggering event
        self.dataSelector.blockSignals(True)
        self.dataSelector.setCurrentIndex(self.selectedModelColumn-1)
        self.dataSelector.blockSignals(False)
        #Show only only the selected column (=dataclass) from the model in the vmsTable
        for column in range(self.model.columnCount()):
            if column == self.selectedModelColumn or column == 0:
                self.vmsTable.showColumn(column)
            else:
                self.vmsTable.hideColumn(column)
        
        #print ("Combobox currentIndex", self.dataSelector.currentIndex(), " from ", self.dataSelector.count())
        #Select line in inverse paramTable
        index = self.proxy.index(self.selectedModelColumn, 0) #Get column = row index from Transpose Proxy model
        print("Param table selecting row {0}".format(self.proxy2.mapFromSource(index).row()))
        #Deselect all
        self.paramTable.clearSelection()
        self.paramTable.selectRow(self.proxy2.mapFromSource(index).row()) #Convert to row index in sorted Proxy model

        #Plot data
        #Get current data class object
        self.updatePlot()

    def updatePlot(self):
        """update the plot with the selected data
        """
        #Get current data class object
        data = self.model.getObject(self.selectedModelColumn)
        #print("plotting column " + str(self.selectedModelColumn))
        if len(data.yAxisValuesList) > 0:
            self.spectralPlot.axes.cla()
            self.spectralPlot.axes.plot(data.xAxisValuesList, data.yAxisValuesList[0])
            #Create axis labels from data
            self.spectralPlot.axes.set_xlabel("{0} [{1}]".format(data.xAxisLabel, data.xAxisUnit))
            self.spectralPlot.axes.set_ylabel("{0} [{1}]".format(data.yAxisVarsLabelList[0], data.yAxisVarsUnitList[0]))


        #Plot also selected data columns
        for colIndex, checked in enumerate(self.model.selectedColumns):
            if checked and colIndex+1 != self.selectedModelColumn: #Starts from 0
                #print("plotting also column " + str(colIndex+1))
                #Get current data class object
                data = self.model.getObject(colIndex+1) #Starts from 0
                self.spectralPlot.axes.plot(data.xAxisValuesList, data.yAxisValuesList[0])

        #redraw
        self.spectralPlot.draw()            
            
            


    def resourcePath(self, relPath):
        """To access resources when bundled as an executable using PyInstaller relative paths are redirected to temporary _MEIPASS folder
            Ref.: https://blog.aaronhktan.com/posts/2018/05/14/pyqt5-pyinstaller-executable
        ### Args:
            relPath {str}: Relative path to resource file

        ### Returns:
            {str}: Converted path
        """
        if hasattr(sys, '_MEIPASS'):
            return os.path.join(sys._MEIPASS, relPath)
        return os.path.join(os.path.abspath('.'), relPath)


    def saveModel(self):
        """Present file dialog to save model data to config (.ini) file
        """
        #Present file dialog using last saved folder
        lastFolder = self.getLastSaveFolder()
        dialog = QFileDialog(self)
        dialog.setWindowTitle("Save settings")
        dialog.setNameFilter("Ini files (*.ini)")
        dialog.setFileMode(QFileDialog.AnyFile)
        dialog.setDirectory(lastFolder)
        dialog.setAcceptMode(QFileDialog.AcceptSave)
        if dialog.exec_() == QDialog.Accepted:
            fileName = dialog.selectedFiles()[0]
            self.saveLastFolder(fileName)
            #Save the model to config file
            self.model.saveModelToConfigFile(fileName)
            print ("Saved to " + fileName)
        


    def getLastSaveFolder(self):
        """ Try to get a selected folder from QSettings file
            (Mac: ~\library\preferences\)
            Defaults to userfolder ~
        ### Returns:
            {str}: folder path
        """
        try:
            settings = QSettings('vmsParser', 'vmsParser')
            print(settings.fileName())
            lastFolder = settings.value('saveFolder', type=str)
            
        except:
            lastFolder = os.path.expanduser('~')

        return lastFolder


    def saveLastFolder(self, foldername):
        """ Saves the last visited folder as QSettings
            (Mac: ~\library\preferences\)
        ### Args:
            foldername {str}: foldername
        """
        settings = QSettings('vmsParser', 'vmsParser')
        settings.setValue('saveFolder', os.path.dirname(foldername))

    def loadModel(self):
        """Present file dialog to load model data from .vms files
        """
        print("model column count before load " + str(self.model.columnCount()))
        #Present file dialog using last saved folder
        fileNames = self.vmsFileSelectorDialog()
        if fileNames: #Continue if files selected
            #Clear popup menu before loading new data
            self.dataSelector.clear()
            dataList = self.loadfilesIntoList(fileNames)
            #Supply the new datalist to the model to replace its data
            self.model.loadData(dataList)
            #Select first model column
            self.selectedModelColumn = 1
            #Update the column number
            self.colNumber.setText("1/" + str(self.model.columnCount()-1))
            #Refill popup menu with filenames
            for data in self.model.getData():
                self.dataSelector.addItem(os.path.basename(data.fileName))
            self.vmsTable.resizeRowsToContents() #Resize rows in table view to make space for multiline comments
    
    def appendData(self):
        """Present file dialog to append files
        """
        if self.dataSelector.currentText() == "":
            self.loadModel()
        else:
            #Present file dialog using last saved folder
            print("model column count before append " + str(self.model.columnCount()))
            print("Selected model column " + str(self.selectedModelColumn))
            fileNames = self.vmsFileSelectorDialog()
            if fileNames: #Continue if files selected
                #Deselect all
                self.paramTable.clearSelection()
                dataList = self.loadfilesIntoList(fileNames)
                #Get the number of columns before insert
                oldColumnNum = self.model.columnCount()
                for data in dataList:
                    self.dataSelector.addItem(os.path.basename(data.fileName))
                    self.model.appendData(data)
                self.selectModelColumn(oldColumnNum)
                self.vmsTable.resizeRowsToContents() #Resize rows in table view to make space for multiline comments
            
    def loadfilesIntoList(self, fileNames):
        """Load a list of vamas files into list of dataclasses to use as new model data

        ### Arguments:
            fileNames {list} -- List of filenames
        """
        data = list()   
        for filename in fileNames:
            vms = VAMAS_File(fileName=filename)
            vms.readVamasFile()
            data.append(vms)
        return data
    
    def vmsFileSelectorDialog(self):
        """ Present file dialog to select one or more vamas files
            Starting with last used folder from App preferences

        ### Returns:
            {list} -- list of selected filePaths
        """
        lastFolder = self.getLastSaveFolder()
        dialog = QFileDialog(self)
        dialog.setWindowTitle("Open vms files")
        dialog.setNameFilter("vms files (*.vms)")
        dialog.setFileMode(QFileDialog.ExistingFiles) #Select multiple existing files
        dialog.setDirectory(lastFolder)
        dialog.setAcceptMode(QFileDialog.AcceptOpen)
        if dialog.exec_() == QDialog.Accepted:
            fileNames = dialog.selectedFiles()
            #Save selected foldername for next time
            self.saveLastFolder(fileNames[0])
            return fileNames
        else:
            return None
Exemplo n.º 8
0
class MainWindow(QMainWindow):
    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)

        self.setAttribute(Qt.WA_QuitOnClose)
        self.setAttribute(Qt.WA_DeleteOnClose)

        # create instance variables
        # ui
        self.ui = uic.loadUi('mainwindow.ui', self)

        # report manager
        self._reportManager = ReportManager(parent=self)
        self._archiveManager = ArchiveManager(parent=self, arcPath=arc_path)

        # report engines
        self._xlsxEngine = XlsxEngine(parent=self)
        self._reportManager.setEngine(self._xlsxEngine)
        # self._printEngine = PrintEngine(parent=self)
        # self._reportManager.setEngine(self._printEngine)

        # persistence engine
        # self._persistenceEngine = CsvEngine(parent=self)
        # self._persistenceEngine = SqliteEngine(parent=self)
        self._persistenceEngine = MysqlEngine(parent=self,
                                              dbItemClass=BillItem)

        # facades
        self._persistenceFacade = PersistenceFacade(
            parent=self, persistenceEngine=self._persistenceEngine)
        self._uiFacade = UiFacade(parent=self,
                                  reportManager=self._reportManager,
                                  archiveManager=self._archiveManager)

        # models
        # domain
        self._modelDomain = DomainModel(
            parent=self, persistenceFacade=self._persistenceFacade)

        # bill list + search proxy
        # TODO: use settings to set icon
        self._modelBillList = BillTableModel(
            parent=self,
            domainModel=self._modelDomain,
            docIcon=QPixmap('./icons/doc.png', 'PNG').scaled(22, 22),
            rightIcon=QPixmap('./icons/right.png', 'PNG').scaled(22, 22))
        self._modelBillSearchProxy = BillSearchProxyModel(parent=self)
        self._modelBillSearchProxy.setSourceModel(self._modelBillList)

        # bill plan + search proxy
        self._modelBillPlan = BillPlanModel(parent=self,
                                            domainModel=self._modelDomain)
        self._modelPlanSearchProxy = QSortFilterProxyModel(parent=self)
        self._modelPlanSearchProxy.setSourceModel(self._modelBillPlan)

        # orders + search proxy
        self._modelOrderList = OrderTableModel(
            parent=self,
            domainModel=self._modelDomain,
            docIcon=QPixmap('./icons/doc.png', 'PNG').scaled(22, 22),
            rightIcon=QPixmap('./icons/right.png', 'PNG').scaled(22, 22))
        self._modelOrderSearchProxy = QSortFilterProxyModel(parent=self)
        self._modelOrderSearchProxy.setSourceModel(self._modelOrderList)

        # connect ui facade to models
        self._uiFacade.setDomainModel(self._modelDomain)
        self._uiFacade.setBillModel(self._modelBillSearchProxy)
        self._uiFacade.setPlanModel(self._modelPlanSearchProxy)
        self._uiFacade.setOrderModel(self._modelOrderSearchProxy)

        # actions
        self.actRefresh = QAction('Обновить', self)
        self.actAddBillRecord = QAction('Добавить счёт...', self)
        self.actEditBillRecord = QAction('Изменить счёт...', self)
        self.actDeleteBillRecord = QAction('Удалить счёт...', self)
        self.actPrint = QAction('Распечатать...', self)
        self.actOpenDictEditor = QAction('Словари', self)
        self.actMakeBillFromOrder = QAction('Создать счёт...', self)
        self.actAddOrderRecord = QAction('Добавить заказ...', self)
        self.actEditOrderRecord = QAction('Изменить заказ...', self)
        self.actViewBillStats = QAction('Статистика по счетам...', self)
        self.actDeleteOrderRecord = QAction('Удалить заказ...', self)

    def buildWeekSelectionCombo(self):
        # TODO if more settings is needed, move all settings-related code to a separate class
        year, week, day = datetime.datetime.now().isocalendar()
        week_list = list()
        for i in range(1, isoweek.Week.last_week_of_year(year).week + 1):
            w = isoweek.Week(year, i)
            week_list.append(
                str(i) + ': ' + str(w.monday().strftime('%d.%m')) + '-' +
                str(w.friday().strftime('%d.%m')))

        self.ui.comboWeek.addItems(week_list)

        # TODO read settings
        if os.path.isfile('settings.ini'):
            with open('settings.ini', mode='tr') as f:
                line = f.readline()
            index = int(line.split('=')[1])
        else:
            index = week

        self.ui.comboWeek.setCurrentIndex(index - 1)
        self._modelBillPlan.updateHeader(index)

    def initApp(self):
        # TODO: extract methods
        # init instances
        # self._persistenceEngine.initEngine(fileName='ref/1.csv')
        # self._persistenceEngine.initEngine(fileName='sqlite3.db')
        self._persistenceEngine.initEngine()
        self._persistenceFacade.initFacade()
        # self._uiFacade.initFacade()
        self._modelDomain.initModel()

        self.show()
        if not self._uiFacade.requestLogin():
            sys.exit(5)

        self.setWindowTitle('Планировщик счетов, пользователь: ' +
                            self._modelDomain.getLoggedUserName())

        self._modelDomain.buildPlanData()
        self._modelBillList.initModel()
        self._modelBillPlan.initModel()
        self._modelOrderList.initModel()

        # init UI
        # bill list table
        self.ui.tableBill: QTableView
        self.ui.tableBill.setModel(self._modelBillSearchProxy)
        self.ui.tableBill.setSelectionMode(QAbstractItemView.SingleSelection)
        self.ui.tableBill.setSelectionBehavior(QAbstractItemView.SelectRows)
        self.ui.tableBill.setEditTriggers(QAbstractItemView.NoEditTriggers)
        # draw delegates
        # self.ui.tableBill.setItemDelegateForRow(0, TableRowDelegate(self.ui.tableBill))
        # self.ui.tableBill.setHorizontalHeader(SectionHeaderView(Qt.Horizontal, parent=self.ui.tableBill))
        # formatting
        self.ui.tableBill.horizontalHeader().setDefaultAlignment(
            Qt.AlignCenter)
        self.ui.tableBill.horizontalHeader().setHighlightSections(False)
        self.ui.tableBill.horizontalHeader().setFixedHeight(24)
        self.ui.tableBill.horizontalHeader().setStretchLastSection(True)
        self.ui.tableBill.horizontalHeader().setStyleSheet(
            'QHeaderView::section {'
            '    padding: 4px;'
            '    border-style: none;'
            '    border-color: #000000;'
            '    border-bottom: 1px solid #000000;'
            '    border-right: 1px solid #000000;'
            '}'
            'QHeaderView::section:horizontal {'
            '    border-right: 1px solid #000000'
            '}')
        # self.ui.tableBill.horizontalHeader().setAutoFillBackground(False)
        self.ui.tableBill.verticalHeader().setVisible(False)
        # self.ui.tableBill.verticalHeader().setDefaultSectionSize(40)
        self.ui.tableBill.setWordWrap(True)
        self.ui.tableBill.resizeRowsToContents()
        self.ui.tableBill.setStyleSheet('QTableView { gridline-color : black}')
        self.hideBillTableColumns()
        # self.ui.tableBill.setSpan(0, 0, 1, 3)

        # bill plan table
        self.ui.tablePlan.setModel(self._modelPlanSearchProxy)
        self.ui.tablePlan.setSelectionMode(QAbstractItemView.NoSelection)
        self.ui.tablePlan.setSelectionBehavior(QAbstractItemView.SelectRows)
        self.ui.tablePlan.setEditTriggers(QAbstractItemView.SelectedClicked)
        self.ui.tablePlan.horizontalHeader().setDefaultAlignment(
            Qt.AlignCenter)
        self.ui.tablePlan.horizontalHeader().setHighlightSections(False)
        self.ui.tablePlan.horizontalHeader().setFixedHeight(24)
        self.ui.tablePlan.horizontalHeader().setStretchLastSection(True)
        self.ui.tablePlan.horizontalHeader().setStyleSheet(
            'QHeaderView::section {'
            '    padding: 4px;'
            '    border-style: none;'
            '    border-color: #000000;'
            '    border-bottom: 1px solid #000000;'
            '    border-right: 1px solid #000000;'
            '}'
            'QHeaderView::section:horizontal {'
            '    border-right: 1px solid #000000'
            '}')
        self.ui.tablePlan.verticalHeader().setVisible(False)
        self.ui.tablePlan.hideColumn(0)
        # self.ui.tablePlan.hideColumn(3)
        self.ui.tablePlan.hideColumn(4)
        # self.ui.tablePlan.verticalHeader().setDefaultSectionSize(40)
        self.ui.tablePlan.setWordWrap(True)
        self.ui.tablePlan.resizeRowsToContents()
        # self.ui.tablePlan.setSpan(0, 0, 1, 3)
        self.ui.tablePlan.setStyleSheet(
            'QTableView { gridline-color : black }')
        # self.ui.tablePlan.setItemDelegateForRow(0, TableRowDelegate(self.ui.tablePlan))

        # bill order table
        self.ui.tableOrder: QTableView
        self.ui.tableOrder.setModel(self._modelOrderSearchProxy)
        self.ui.tableOrder.setSelectionMode(QAbstractItemView.SingleSelection)
        self.ui.tableOrder.setSelectionBehavior(QAbstractItemView.SelectRows)
        self.ui.tableOrder.setEditTriggers(QAbstractItemView.NoEditTriggers)
        # formatting
        self.ui.tableOrder.horizontalHeader().setDefaultAlignment(
            Qt.AlignCenter)
        self.ui.tableOrder.horizontalHeader().setHighlightSections(False)
        self.ui.tableOrder.horizontalHeader().setFixedHeight(24)
        self.ui.tableOrder.horizontalHeader().setStretchLastSection(True)
        self.ui.tableOrder.horizontalHeader().setStyleSheet(
            'QHeaderView::section {'
            '    padding: 4px;'
            '    border-style: none;'
            '    border-color: #000000;'
            '    border-bottom: 1px solid #000000;'
            '    border-right: 1px solid #000000;'
            '}'
            'QHeaderView::section:horizontal {'
            '    border-right: 1px solid #000000'
            '}')
        self.ui.tableOrder.verticalHeader().setVisible(False)
        self.ui.tableOrder.setWordWrap(True)
        self.ui.tableOrder.resizeRowsToContents()
        self.ui.tableOrder.setStyleSheet(
            'QTableView { gridline-color : black}')

        # setup filter widgets
        self.ui.comboProjectFilter.setModel(self._modelDomain.dicts['project'])
        self.ui.comboStatusFilter.setModel(self._modelDomain.dicts['status'])
        self.ui.comboPriorityFilter.setModel(
            self._modelDomain.dicts['priority'])
        self.ui.comboShipmentFilter.setModel(
            self._modelDomain.dicts['shipment'])
        self.ui.dateFromFilter.setDate(
            QDate.fromString(self._modelDomain.getEarliestBillDate(),
                             'dd.MM.yyyy'))
        self.ui.dateUntilFilter.setDate(QDate.currentDate())

        # self.btnRefresh.setVisible(False)

        self.buildWeekSelectionCombo()

        # create actions
        self.initActions()

        # setup ui widget signals
        # buttons
        self.ui.btnRefresh.clicked.connect(self.onBtnRefreshClicked)
        self.ui.btnAddBill.clicked.connect(self.onBtnAddBillClicked)
        self.ui.btnEditBill.clicked.connect(self.onBtnEditBillClicked)
        self.ui.btnDeleteBill.clicked.connect(self.onBtnDeleteBillClicked)
        self.ui.btnPrint.clicked.connect(self.onBtnPrintClicked)
        self.ui.btnDictEditor.clicked.connect(self.onBtnDictEditorClicked)
        self.ui.btnMakeBillFromOrder.clicked.connect(
            self.onBtnMakeBillFromOrderClicked)
        self.ui.btnAddOrder.clicked.connect(self.onBtnAddOrderClicked)
        self.ui.btnEditOrder.clicked.connect(self.onBtnEditOrderClicked)
        self.ui.btnDelOrder.clicked.connect(self.onBtnDelOrderClicked)
        self.ui.btnBillStats.clicked.connect(self.onBtnBillStatsClicked)

        # table widgets
        self.ui.tableBill.doubleClicked.connect(self.onTableBillDoubleClicked)
        self.ui.tableBill.clicked.connect(self.onTableBillClicked)
        self.ui.tableOrder.doubleClicked.connect(
            self.onTableOrderDoubleClicked)
        self.ui.tableOrder.clicked.connect(self.onTableOrderClicked)
        self.ui.tabWidget.currentChanged.connect(self.onTabBarCurrentChanged)

        # model signals
        self._modelDomain.beginClearModel.connect(
            self._modelBillList.beginClearModel)
        self._modelDomain.beginClearModel.connect(
            self._modelBillPlan.beginClearModel)
        self._modelDomain.beginClearModel.connect(
            self._modelOrderList.beginClearModel)
        self._modelDomain.endClearModel.connect(
            self._modelBillList.endClearModel)
        self._modelDomain.endClearModel.connect(
            self._modelBillPlan.endClearModel)
        self._modelDomain.endClearModel.connect(
            self._modelOrderList.endClearModel)

        # totals update
        self._uiFacade.totalsChanged.connect(self.updateTotals)

        # search widgets
        self.ui.comboWeek.currentIndexChanged.connect(
            self.onComboWeekCurrentIndexChanged)
        self.ui.editSearch.textChanged.connect(self.setSearchFilter)
        self.ui.comboProjectFilter.currentIndexChanged.connect(
            self.setSearchFilter)
        self.ui.comboStatusFilter.currentIndexChanged.connect(
            self.setSearchFilter)
        self.ui.comboPriorityFilter.currentIndexChanged.connect(
            self.setSearchFilter)
        self.ui.comboShipmentFilter.currentIndexChanged.connect(
            self.setSearchFilter)
        self.ui.dateFromFilter.dateChanged.connect(self.setSearchFilter)
        self.ui.dateUntilFilter.dateChanged.connect(self.setSearchFilter)

        self.setSearchFilter()

        # widget tweaks
        # self.ui.btnRefresh.setVisible(False)
        self.updateTotals()

        self.prepareUi(self._modelDomain.getLoggedUserLevel())

    def initActions(self):
        self.actRefresh.setShortcut('Ctrl+R')
        self.actRefresh.setStatusTip('Обновить данные')
        self.actRefresh.triggered.connect(self.procActRefresh)

        self.actAddBillRecord.setShortcut('Ctrl+A')
        self.actAddBillRecord.setStatusTip('Добавить новый счёт')
        self.actAddBillRecord.triggered.connect(self.procActAddBillRecord)

        # self.actEditBillRecord.setShortcut('Ctrl+A')
        self.actEditBillRecord.setStatusTip('Добавить новый счёт')
        self.actEditBillRecord.triggered.connect(self.procActEditRecord)

        # self.actDeleteBillRecord.setShortcut('Ctrl+A')
        self.actDeleteBillRecord.setStatusTip('Добавить новый счёт')
        self.actDeleteBillRecord.triggered.connect(self.procActDeleteRecord)

        self.actPrint.setStatusTip('Напечатать текущую таблицу')
        self.actPrint.triggered.connect(self.procActPrint)

        self.actOpenDictEditor.setStatusTip('Открыть редактор словарей')
        self.actOpenDictEditor.triggered.connect(self.procActOpenDictEditor)

        self.actMakeBillFromOrder.setStatusTip('Создать счёт из заказа')
        self.actMakeBillFromOrder.triggered.connect(
            self.procActMakeBillFromOrder)

        self.actAddOrderRecord.setStatusTip('Добавить заказ')
        self.actAddOrderRecord.triggered.connect(self.procActAddOrderRecord)

        self.actEditOrderRecord.setStatusTip('Изменить заказ')
        self.actEditOrderRecord.triggered.connect(self.procActEditOrderRecord)

        self.actViewBillStats.setStatusTip(
            'Посмотреть статистику по счетам...')
        self.actViewBillStats.triggered.connect(self.procActViewBillStats)

        self.actDeleteOrderRecord.setStatusTip('Перенести заказ в архив...')
        self.actDeleteOrderRecord.triggered.connect(
            self.procActDeleteOrderRecord)

    def refreshView(self):
        screenRect = QApplication.desktop().screenGeometry()

        # if self.ui.tabWidget.currentIndex() == 0:
        tbwidth = screenRect.width() - 50
        self.ui.tableBill.setColumnWidth(0, tbwidth * 0.03)
        self.ui.tableBill.setColumnWidth(1, tbwidth * 0.05)
        self.ui.tableBill.setColumnWidth(2, tbwidth * 0.07)
        self.ui.tableBill.setColumnWidth(3, tbwidth * 0.05)  # +0.01
        self.ui.tableBill.setColumnWidth(4, tbwidth * 0.08)
        self.ui.tableBill.setColumnWidth(5, tbwidth * 0.05)
        self.ui.tableBill.setColumnWidth(6, tbwidth * 0.06)
        self.ui.tableBill.setColumnWidth(7, tbwidth * 0.20)  # +0.01
        self.ui.tableBill.setColumnWidth(8, tbwidth * 0.06)
        self.ui.tableBill.setColumnWidth(9, tbwidth * 0.06)
        self.ui.tableBill.setColumnWidth(10, tbwidth * 0.055)
        self.ui.tableBill.setColumnWidth(11, tbwidth * 0.055)
        self.ui.tableBill.setColumnWidth(12, tbwidth * 0.06)
        self.ui.tableBill.setColumnWidth(13, tbwidth * 0.035)
        # self.ui.tableBill.setColumnWidth(14, tbwidth * 0.03)
        self.ui.tableBill.setColumnWidth(15, tbwidth * 0.01)
        self.ui.tableBill.setColumnWidth(16, tbwidth * 0.01)
        self.ui.tableBill.setColumnWidth(17, tbwidth * 0.01)  # +0.01

        # elif self.ui.tabWidget.currentIndex() == 1:
        tpwidth = screenRect.width() - 45
        # 1 2 3 5 .. week count - 1
        # self.ui.tablePlan.setColumnWidth(0, tpwidth * 0.035)
        self.ui.tablePlan.setColumnWidth(1, tpwidth * 0.13)
        self.ui.tablePlan.setColumnWidth(2, tpwidth * 0.05)
        self.ui.tablePlan.setColumnWidth(3, tpwidth * 0.10)
        # self.ui.tablePlan.setColumnWidth(4, tpwidth * 0.06)
        self.ui.tablePlan.setColumnWidth(5, tpwidth * 0.09)
        self.ui.tablePlan.setColumnWidth(6, tpwidth * 0.09)
        self.ui.tablePlan.setColumnWidth(7, tpwidth * 0.09)
        self.ui.tablePlan.setColumnWidth(8, tpwidth * 0.09)
        self.ui.tablePlan.setColumnWidth(9, tpwidth * 0.09)
        self.ui.tablePlan.setColumnWidth(10, tpwidth * 0.09)
        self.ui.tablePlan.setColumnWidth(11, tpwidth * 0.09)
        self.ui.tablePlan.setColumnWidth(12, tpwidth * 0.09)

        # elif self.ui.tabWidget.currentIndex() == 2:
        towidth = screenRect.width() - 45
        self.ui.tableOrder.setColumnWidth(0, towidth * 0.02)
        self.ui.tableOrder.setColumnWidth(1, towidth * 0.30)
        self.ui.tableOrder.setColumnWidth(2, towidth * 0.20)
        self.ui.tableOrder.setColumnWidth(3, towidth * 0.06)
        self.ui.tableOrder.setColumnWidth(4, towidth * 0.05)
        self.ui.tableOrder.setColumnWidth(5, towidth * 0.06)
        self.ui.tableOrder.setColumnWidth(6, towidth * 0.06)
        self.ui.tableOrder.setColumnWidth(7, towidth * 0.07)
        self.ui.tableOrder.setColumnWidth(8, towidth * 0.07)
        self.ui.tableOrder.setColumnWidth(9, towidth * 0.07)
        self.ui.tableOrder.setColumnWidth(10, towidth * 0.02)
        self.ui.tableOrder.setColumnWidth(11, towidth * 0.01)

    def prepareUi(self, level):
        if level == 1:
            self.ui.tabWidget.setCurrentIndex(0)
            index = self._modelBillSearchProxy.mapFromSource(
                self._modelBillList.index(
                    self._modelBillList.rowCount(QModelIndex()) - 1, 0))
            self.ui.tableBill.scrollTo(index, QAbstractItemView.EnsureVisible)
            self.ui.tableBill.selectionModel().setCurrentIndex(
                index, QItemSelectionModel.Select
                | QItemSelectionModel.Rows)
        elif level == 2 or level == 3:
            print('approver + user')
            self.ui.tabWidget.setCurrentIndex(2)
            self.ui.tabWidget.removeTab(0)
            self.ui.tabWidget.removeTab(0)
            self.ui.btnMakeBillFromOrder.hide()
            self.ui.btnDictEditor.hide()
            self.ui.btnPrint.hide()
            self.ui.btnRefresh.hide()
            self.ui.btnBillStats.hide()

    def hideBillTableColumns(self):
        # self.ui.tableBill.hideColumn(11)
        self.ui.tableBill.hideColumn(14)

    def hideOrderTableColumns(self):
        self.ui.tableOrder.hideColumn(3)

    # ui events
    def onBtnRefreshClicked(self):
        self.actRefresh.trigger()

    def onBtnAddBillClicked(self):
        self.actAddBillRecord.trigger()

    def onBtnEditBillClicked(self):
        self.actEditBillRecord.trigger()

    def onBtnDeleteBillClicked(self):
        self.actDeleteBillRecord.trigger()

    def onBtnDictEditorClicked(self):
        self.actOpenDictEditor.trigger()

    def onBtnPrintClicked(self):
        self.actPrint.trigger()

    def onBtnMakeBillFromOrderClicked(self):
        self.actMakeBillFromOrder.trigger()

    def onBtnBillStatsClicked(self):
        self.actViewBillStats.trigger()

    def onBtnAddOrderClicked(self):
        self.actAddOrderRecord.trigger()

    def onBtnEditOrderClicked(self):
        self.actEditOrderRecord.trigger()

    def onBtnDelOrderClicked(self):
        self.actDeleteOrderRecord.trigger()

    def onTableBillClicked(self, index):
        col = index.column()
        if col == self._modelBillList.ColumnDoc:
            doc = index.data(Qt.EditRole)
            if doc:
                subprocess.Popen(r'explorer /select,"' +
                                 index.data(Qt.EditRole).replace("/", "\\") +
                                 '"')
        elif col == self._modelBillList.ColumnOrder:
            orderId = index.data(const.RoleOrderId)
            if not orderId:
                return
            self.ui.tabWidget.setCurrentIndex(2)
            rowToSelect = self._modelOrderList.getRowById(orderId)
            indexToSelect = self._modelOrderSearchProxy.mapFromSource(
                self._modelOrderList.index(rowToSelect, 0, QModelIndex()))
            self.ui.tableOrder.selectRow(indexToSelect.row())

    def onTableBillDoubleClicked(self, index):
        self.actEditBillRecord.trigger()

    def onTableOrderClicked(self, index):
        col = index.column()
        if col == self._modelOrderList.ColumnBill:
            billId = self._modelDomain.getBillIdForOrderId(
                index.data(const.RoleNodeId))
            if billId:
                self.ui.tabWidget.setCurrentIndex(0)
                rowToSelect = self._modelBillList.getRowById(billId)
                indexToSelect = self._modelBillSearchProxy.mapFromSource(
                    self._modelBillList.index(rowToSelect, 0, QModelIndex()))
                self.ui.tableBill.selectRow(indexToSelect.row())
        if col == self._modelOrderList.ColumnDoc:
            doc = index.data(Qt.EditRole)
            if doc:
                subprocess.Popen(r'explorer /select,"' +
                                 index.data(Qt.EditRole).replace("/", "\\") +
                                 '"')

    def onTableOrderDoubleClicked(self, index):
        self.actEditOrderRecord.trigger()

    def onTabBarCurrentChanged(self, index):
        if index == 1:
            self._modelDomain.buildPlanData()

    def onComboWeekCurrentIndexChanged(self, index):
        self._modelBillPlan.updateHeader(index + 1)

    # misc events
    def resizeEvent(self, event):
        # print('resize event')
        self.refreshView()
        # self.ui.tableBill.resizeRowsToContents()
        # self.ui.tablePlan.resizeRowsToContents()
        self.ui.tableOrder.resizeRowsToContents()

    def closeEvent(self, *args, **kwargs):
        # TODO error handling on saving before exiting
        self._uiFacade.requestExit(self.ui.comboWeek.currentIndex())
        super(MainWindow, self).closeEvent(*args, **kwargs)

    # action processing
    # send user commands to the ui facade: (command, parameters (like indexes, etc.))
    def procActRefresh(self):
        print('act refresh trigger')

        try:
            if self._modelDomain.savePlanData():
                print('plan data saved')
            else:
                QMessageBox.information(
                    self, 'Ошибка',
                    'Ошибка подключения к БД при попытке отбновления данных.\nОбратитесь к разработчику.'
                )
        except Exception as ex:
            print(ex)

        self._modelDomain.clearModel()

        self._modelDomain.initModel()
        self._modelBillList.initModel()
        self._modelBillPlan.initModel()
        self._modelOrderList.initModel()

        self._uiFacade.requestRefresh()
        self.refreshView()
        self.ui.tableBill.resizeRowsToContents()
        self.ui.tablePlan.resizeRowsToContents()
        self.ui.tableOrder.resizeRowsToContents()
        self.hideBillTableColumns()
        self.hideOrderTableColumns()

    def procActAddBillRecord(self):
        row = self._uiFacade.requestAddBillRecord()

        if row is not None:
            index = self._modelBillSearchProxy.mapFromSource(
                self._modelBillList.index(row, 0))
            self.ui.tableBill.scrollTo(index)
            self.ui.tableBill.selectionModel().setCurrentIndex(
                index, QItemSelectionModel.Select
                | QItemSelectionModel.Rows)

    def procActEditRecord(self):
        if not self.ui.tableBill.selectionModel().hasSelection():
            QMessageBox.information(self, 'Ошибка',
                                    'Изменить: пожалуйста, выберите запись.')
            return

        selectedIndex = self.ui.tableBill.selectionModel().selectedIndexes()[0]
        self._uiFacade.requestEditBillRecord(
            self._modelBillSearchProxy.mapToSource(selectedIndex))

    def procActDeleteRecord(self):
        # print('act delete record trigger')
        if not self.ui.tableBill.selectionModel().hasSelection():
            QMessageBox.information(self, 'Ошибка',
                                    'Удалить: пожалуйста, выберите запись.')
            return

        selectedIndex: QModelIndex = self.ui.tableBill.selectionModel(
        ).selectedIndexes()[0]
        # TODO: HACK -- set active to false before deleting a bill
        self._modelBillList.setData(selectedIndex, 2, Qt.CheckStateRole)
        self._uiFacade.requestDeleteRecord(
            self._modelBillSearchProxy.mapToSource(selectedIndex))

    def procActMakeBillFromOrder(self):
        if not self.ui.tableOrder.selectionModel().hasSelection():
            QMessageBox.information(
                self, 'Ошибка', 'Выберите запись о заказе для создания счёта.')
            return

        selectedIndex: QModelIndex = self.ui.tableOrder.selectionModel(
        ).selectedIndexes()[0]
        row = self._uiFacade.requestMakeBillFromOrder(
            self._modelOrderSearchProxy.mapToSource(selectedIndex))

        if row is not None:
            self.ui.tabWidget.setCurrentIndex(0)
            index = self._modelBillSearchProxy.mapFromSource(
                self._modelBillList.index(row, 0))
            self.ui.tableBill.scrollTo(index)
            self.ui.tableBill.selectionModel().setCurrentIndex(
                index, QItemSelectionModel.Select
                | QItemSelectionModel.Rows)

    def procActAddOrderRecord(self):
        row = self._uiFacade.requestAddOrderRecord()

        if row is not None:
            index = self._modelOrderSearchProxy.mapFromSource(
                self._modelOrderList.index(row, 0))
            self.ui.tableOrder.scrollTo(index)
            self.ui.tableOrder.selectionModel().setCurrentIndex(
                index, QItemSelectionModel.Select
                | QItemSelectionModel.Rows)

    def procActEditOrderRecord(self):
        if not self.ui.tableOrder.selectionModel().hasSelection():
            QMessageBox.information(self, 'Ошибка',
                                    'Изменить: пожалуйста, выберите запись.')
            return

        selectedIndex = self.ui.tableOrder.selectionModel().selectedIndexes(
        )[0]
        self._uiFacade.requestEditOrderRecord(
            self._modelOrderSearchProxy.mapToSource(selectedIndex))

    def procActDeleteOrderRecord(self):
        if not self.ui.tableOrder.selectionModel().hasSelection():
            QMessageBox.information(
                self, 'Ошибка',
                'Перенести в архив: пожалуйста, выберите запись.')
            return

        selectedIndex = self.ui.tableOrder.selectionModel().selectedIndexes(
        )[0]
        self._uiFacade.requestDeleteOrderRecord(
            self._modelOrderSearchProxy.mapToSource(selectedIndex))

    def procActPrint(self):
        self._uiFacade.requestPrint(self.ui.tabWidget.currentIndex(),
                                    self._modelDomain.getBillTotals())

    def setSearchFilter(self, dummy=0):
        self._modelBillSearchProxy.filterString = self.ui.editSearch.text()
        self._modelBillSearchProxy.filterProject = self.ui.comboProjectFilter.currentData(
            const.RoleNodeId)
        self._modelBillSearchProxy.filterStatus = self.ui.comboStatusFilter.currentData(
            const.RoleNodeId)
        self._modelBillSearchProxy.filterPriority = self.ui.comboPriorityFilter.currentData(
            const.RoleNodeId)
        self._modelBillSearchProxy.filterShipment = self.ui.comboShipmentFilter.currentData(
            const.RoleNodeId)
        self._modelBillSearchProxy.filterFromDate = self.ui.dateFromFilter.date(
        )
        self._modelBillSearchProxy.filterUntilDate = self.ui.dateUntilFilter.date(
        )
        self._modelBillSearchProxy.invalidate()

        # self._modelPlanSearchProxy.setFilterWildcard(self.ui.editSearch.text())
        # self._modelPlanSearchProxy.invalidate()
        #
        # self._modelOrderSearchProxy.setFilterWildcard(self.ui.editSearch.text())
        # self._modelPlanSearchProxy.invalidate()

        self.hideBillTableColumns()
        self.hideOrderTableColumns()
        self.refreshView()

    def procActOpenDictEditor(self):
        self._uiFacade.requestOpenDictEditor()

    def procActViewBillStats(self):
        self._uiFacade.requestViewBillStats()

    @pyqtSlot()
    def updateTotals(self):
        print("update totals")
        p, r, t = self._modelDomain.getBillTotals()
        self.ui.lblTotal.setText(
            f'Оплачено:  <span style="background-color:#92D050">{f"{p/100:,.2f}".replace(",", " ")}</span><br>'
            f'Осталось:  <span style="background-color:#FF6767">{f"{r/100:,.2f}".replace(",", " ")}</span><br>'
            f'Всего:  {f"{t/100:,.2f}".replace(",", " ")}')
Exemplo n.º 9
0
class SearchFileWidget(QWidget):

    language_filter_change = pyqtSignal(list)

    def __init__(self):
        QWidget.__init__(self)

        self._refreshing = False

        self.fileModel = None
        self.proxyFileModel = None
        self.videoModel = None

        self._state = None

        self.timeLastSearch = QTime.currentTime()

        self.ui = Ui_SearchFileWidget()
        self.setup_ui()

    def set_state(self, state):
        self._state = state
        self._state.login_status_changed.connect(self.on_login_state_changed)
        self._state.interface_language_changed.connect(
            self.on_interface_language_changed)

    def get_state(self):
        return self._state

    def setup_ui(self):
        self.ui.setupUi(self)
        settings = QSettings()

        self.ui.splitter.setSizes([600, 1000])
        self.ui.splitter.setChildrenCollapsible(False)

        # Set up folder view

        lastDir = settings.value("mainwindow/workingDirectory",
                                 QDir.homePath())
        log.debug('Current directory: {currentDir}'.format(currentDir=lastDir))

        self.fileModel = QFileSystemModel(self)
        self.fileModel.setFilter(QDir.AllDirs | QDir.Dirs | QDir.Drives
                                 | QDir.NoDotAndDotDot | QDir.Readable
                                 | QDir.Executable | QDir.Writable)
        self.fileModel.iconProvider().setOptions(
            QFileIconProvider.DontUseCustomDirectoryIcons)
        self.fileModel.setRootPath(QDir.rootPath())
        self.fileModel.directoryLoaded.connect(self.onFileModelDirectoryLoaded)

        self.proxyFileModel = QSortFilterProxyModel(self)
        self.proxyFileModel.setSortRole(Qt.DisplayRole)
        self.proxyFileModel.setSourceModel(self.fileModel)
        self.proxyFileModel.sort(0, Qt.AscendingOrder)
        self.proxyFileModel.setSortCaseSensitivity(Qt.CaseInsensitive)
        self.ui.folderView.setModel(self.proxyFileModel)

        self.ui.folderView.setHeaderHidden(True)
        self.ui.folderView.hideColumn(3)
        self.ui.folderView.hideColumn(2)
        self.ui.folderView.hideColumn(1)

        index = self.fileModel.index(lastDir)
        proxyIndex = self.proxyFileModel.mapFromSource(index)
        self.ui.folderView.scrollTo(proxyIndex)

        self.ui.folderView.expanded.connect(self.onFolderViewExpanded)
        self.ui.folderView.clicked.connect(self.onFolderTreeClicked)
        self.ui.buttonFind.clicked.connect(self.onButtonFind)
        self.ui.buttonRefresh.clicked.connect(self.onButtonRefresh)

        # Set up introduction
        self.showInstructions()

        # Set up video view
        self.ui.filterLanguageForVideo.set_unknown_text(_('All languages'))
        self.ui.filterLanguageForVideo.selected_language_changed.connect(
            self.on_language_combobox_filter_change)
        # self.ui.filterLanguageForVideo.selected_language_changed.connect(self.onFilterLanguageVideo)

        self.videoModel = VideoModel(self)
        self.ui.videoView.setHeaderHidden(True)
        self.ui.videoView.setModel(self.videoModel)
        self.ui.videoView.activated.connect(self.onClickVideoTreeView)
        self.ui.videoView.clicked.connect(self.onClickVideoTreeView)
        self.ui.videoView.customContextMenuRequested.connect(self.onContext)
        self.videoModel.dataChanged.connect(self.subtitlesCheckedChanged)
        self.language_filter_change.connect(
            self.videoModel.on_filter_languages_change)

        self.ui.buttonSearchSelectVideos.clicked.connect(
            self.onButtonSearchSelectVideos)
        self.ui.buttonSearchSelectFolder.clicked.connect(
            self.onButtonSearchSelectFolder)
        self.ui.buttonDownload.clicked.connect(self.onButtonDownload)
        self.ui.buttonPlay.clicked.connect(self.onButtonPlay)
        self.ui.buttonIMDB.clicked.connect(self.onViewOnlineInfo)
        self.ui.videoView.setContextMenuPolicy(Qt.CustomContextMenu)

        # Drag and Drop files to the videoView enabled
        self.ui.videoView.__class__.dragEnterEvent = self.dragEnterEvent
        self.ui.videoView.__class__.dragMoveEvent = self.dragEnterEvent
        self.ui.videoView.__class__.dropEvent = self.dropEvent
        self.ui.videoView.setAcceptDrops(1)

        # FIXME: ok to drop this connect?
        # self.ui.videoView.clicked.connect(self.onClickMovieTreeView)

        self.retranslate()

    def retranslate(self):
        introduction = '<p align="center"><h2>{title}</h2></p>' \
            '<p><b>{tab1header}</b><br/>{tab1content}</p>' \
            '<p><b>{tab2header}</b><br/>{tab2content}</p>'\
            '<p><b>{tab3header}</b><br/>{tab3content}</p>'.format(
                title=_('How To Use {title}').format(title=PROJECT_TITLE),
                tab1header=_('1st Tab:'),
                tab2header=_('2nd Tab:'),
                tab3header=_('3rd Tab:'),
                tab1content=_('Select, from the Folder Tree on the left, the folder which contains the videos '
                              'that need subtitles. {project} will then try to automatically find available '
                              'subtitles.').format(project=PROJECT_TITLE),
                tab2content=_('If you don\'t have the videos in your machine, you can search subtitles by '
                              'introducing the title/name of the video.').format(project=PROJECT_TITLE),
                tab3content=_('If you have found some subtitle somewhere else that is not in {project}\'s database, '
                              'please upload those subtitles so next users will be able to '
                              'find them more easily.').format(project=PROJECT_TITLE))
        self.ui.introductionHelp.setHtml(introduction)

    @pyqtSlot(Language)
    def on_interface_language_changed(self, language):
        self.ui.retranslateUi(self)
        self.retranslate()

    @pyqtSlot(str)
    def onFileModelDirectoryLoaded(self, path):
        settings = QSettings()
        lastDir = settings.value('mainwindow/workingDirectory',
                                 QDir.homePath())
        qDirLastDir = QDir(lastDir)
        qDirLastDir.cdUp()
        if qDirLastDir.path() == path:
            index = self.fileModel.index(lastDir)
            proxyIndex = self.proxyFileModel.mapFromSource(index)
            self.ui.folderView.scrollTo(proxyIndex)
            self.ui.folderView.setCurrentIndex(proxyIndex)

    @pyqtSlot(int, str)
    def on_login_state_changed(self, state, message):
        log.debug(
            'on_login_state_changed(state={state}, message={message}'.format(
                state=state, message=message))
        if state in (State.LOGIN_STATUS_LOGGED_OUT, State.LOGIN_STATUS_BUSY):
            self.ui.buttonSearchSelectFolder.setEnabled(False)
            self.ui.buttonSearchSelectVideos.setEnabled(False)
            self.ui.buttonFind.setEnabled(False)
        elif state == State.LOGIN_STATUS_LOGGED_IN:
            self.ui.buttonSearchSelectFolder.setEnabled(True)
            self.ui.buttonSearchSelectVideos.setEnabled(True)
            self.ui.buttonFind.setEnabled(
                self.get_current_selected_folder() is not None)
        else:
            log.warning('unknown state')

    @pyqtSlot(Language)
    def on_language_combobox_filter_change(self, language):
        if language.is_generic():
            self.language_filter_change.emit(
                self.get_state().get_permanent_language_filter())
        else:
            self.language_filter_change.emit([language])

    def on_permanent_language_filter_change(self, languages):
        selected_language = self.ui.filterLanguageForVideo.get_selected_language(
        )
        if selected_language.is_generic():
            self.language_filter_change.emit(languages)

    @pyqtSlot()
    def subtitlesCheckedChanged(self):
        subs = self.videoModel.get_checked_subtitles()
        if subs:
            self.ui.buttonDownload.setEnabled(True)
        else:
            self.ui.buttonDownload.setEnabled(False)

    def showInstructions(self):
        self.ui.stackedSearchResult.setCurrentWidget(self.ui.pageIntroduction)

    def hideInstructions(self):
        self.ui.stackedSearchResult.setCurrentWidget(self.ui.pageSearchResult)

    @pyqtSlot(QModelIndex)
    def onFolderTreeClicked(self, proxyIndex):
        """What to do when a Folder in the tree is clicked"""
        if not proxyIndex.isValid():
            return

        index = self.proxyFileModel.mapToSource(proxyIndex)
        settings = QSettings()
        folder_path = self.fileModel.filePath(index)
        settings.setValue('mainwindow/workingDirectory', folder_path)
        # self.ui.buttonFind.setEnabled(self.get_state().)

    def get_current_selected_folder(self):
        proxyIndex = self.ui.folderView.currentIndex()
        index = self.proxyFileModel.mapToSource(proxyIndex)
        folder_path = self.fileModel.filePath(index)
        if not folder_path:
            return None
        return folder_path

    def get_current_selected_item_videomodel(self):
        current_index = self.ui.videoView.currentIndex()
        return self.videoModel.getSelectedItem(current_index)

    @pyqtSlot()
    def onButtonFind(self):
        now = QTime.currentTime()
        if now < self.timeLastSearch.addMSecs(500):
            return
        folder_path = self.get_current_selected_folder()

        settings = QSettings()
        settings.setValue('mainwindow/workingDirectory', folder_path)
        self.search_videos([folder_path])

        self.timeLastSearch = QTime.currentTime()

    @pyqtSlot()
    def onButtonRefresh(self):
        currentPath = self.get_current_selected_folder()
        if not currentPath:
            settings = QSettings()
            currentPath = settings.value('mainwindow/workingDirectory',
                                         QDir.homePath())

        self._refreshing = True

        self.ui.folderView.collapseAll()

        currentPath = self.get_current_selected_folder()
        if not currentPath:
            settings = QSettings()
            currentPath = settings.value('mainwindow/workingDirectory',
                                         QDir.homePath())

        index = self.fileModel.index(currentPath)

        self.ui.folderView.scrollTo(self.proxyFileModel.mapFromSource(index))

    @pyqtSlot(QModelIndex)
    def onFolderViewExpanded(self, proxyIndex):
        if self._refreshing:
            expandedPath = self.fileModel.filePath(
                self.proxyFileModel.mapToSource(proxyIndex))
            if expandedPath == QDir.rootPath():
                currentPath = self.get_current_selected_folder()
                if not currentPath:
                    settings = QSettings()
                    currentPath = settings.value('mainwindow/workingDirectory',
                                                 QDir.homePath())

                index = self.fileModel.index(currentPath)

                self.ui.folderView.scrollTo(
                    self.proxyFileModel.mapFromSource(index))
                self._refreshing = False

    @pyqtSlot()
    def onButtonSearchSelectFolder(self):
        settings = QSettings()
        path = settings.value('mainwindow/workingDirectory', QDir.homePath())
        folder_path = QFileDialog.getExistingDirectory(
            self, _('Select the directory that contains your videos'), path)
        if folder_path:
            settings.setValue('mainwindow/workingDirectory', folder_path)
            self.search_videos([folder_path])

    @pyqtSlot()
    def onButtonSearchSelectVideos(self):
        settings = QSettings()
        currentDir = settings.value('mainwindow/workingDirectory',
                                    QDir.homePath())
        fileNames, t = QFileDialog.getOpenFileNames(
            self, _('Select the video(s) that need subtitles'), currentDir,
            SELECT_VIDEOS)
        if fileNames:
            settings.setValue('mainwindow/workingDirectory',
                              QFileInfo(fileNames[0]).absolutePath())
            self.search_videos(fileNames)

    def search_videos(self, paths):
        if not self.get_state().connected():
            QMessageBox.about(
                self, _("Error"),
                _('You are not connected to the server. Please reconnect first.'
                  ))
            return
        self.ui.buttonFind.setEnabled(False)
        self._search_videos_raw(paths)
        self.ui.buttonFind.setEnabled(True)

    def _search_videos_raw(self, paths):
        # FIXME: must pass mainwindow as argument to ProgressCallbackWidget
        callback = ProgressCallbackWidget(self)
        callback.set_title_text(_("Scanning..."))
        callback.set_label_text(_("Scanning files"))
        callback.set_finished_text(_("Scanning finished"))
        callback.set_block(True)

        try:
            local_videos, local_subs = scan_videopaths(paths,
                                                       callback=callback,
                                                       recursive=True)
        except OSError:
            callback.cancel()
            QMessageBox.warning(self, _('Error'),
                                _('Some directories are not accessible.'))

        if callback.canceled():
            return

        callback.finish()

        log.debug("Videos found: %s" % local_videos)
        log.debug("Subtitles found: %s" % local_subs)
        self.hideInstructions()

        QCoreApplication.processEvents()

        if not local_videos:
            QMessageBox.about(self, _("Scan Results"),
                              _("No video has been found!"))
            return

        total = len(local_videos)

        # FIXME: must pass mainwindow as argument to ProgressCallbackWidget
        # callback = ProgressCallbackWidget(self)
        # callback.set_title_text(_("Asking Server..."))
        # callback.set_label_text(_("Searching subtitles..."))
        # callback.set_updated_text(_("Searching subtitles ( %d / %d )"))
        # callback.set_finished_text(_("Search finished"))
        callback.set_block(True)
        callback.set_range(0, total)

        callback.show()

        callback.set_range(0, 2)

        download_callback = callback.get_child_progress(0, 1)
        # videoSearchResults = self.get_state().get_OSDBServer().SearchSubtitles("", videos_piece)
        remote_subs = self.get_state().get_OSDBServer().search_videos(
            videos=local_videos, callback=download_callback)

        self.videoModel.set_videos(local_videos)
        # self.onFilterLanguageVideo(self.ui.filterLanguageForVideo.get_selected_language())

        if remote_subs is None:
            QMessageBox.about(
                self, _("Error"),
                _("Error contacting the server. Please try again later"))
        callback.finish()

        # TODO: CHECK if our local subtitles are already in the server, otherwise suggest to upload
        # self.OSDBServer.CheckSubHash(sub_hashes)

    @pyqtSlot()
    def onButtonPlay(self):
        settings = QSettings()
        programPath = settings.value('options/VideoPlayerPath', '')
        parameters = settings.value('options/VideoPlayerParameters', '')
        if programPath == '':
            QMessageBox.about(
                self, _('Error'),
                _('No default video player has been defined in Settings.'))
            return

        selected_subtitle = self.get_current_selected_item_videomodel()
        if isinstance(selected_subtitle, SubtitleFileNetwork):
            selected_subtitle = selected_subtitle.get_subtitles()[0]

        if isinstance(selected_subtitle, LocalSubtitleFile):
            subtitle_file_path = selected_subtitle.get_filepath()
        elif isinstance(selected_subtitle, RemoteSubtitleFile):
            subtitle_file_path = QDir.temp().absoluteFilePath(
                'subdownloader.tmp.srt')
            log.debug(
                'Temporary subtitle will be downloaded into: {temp_path}'.
                format(temp_path=subtitle_file_path))
            # FIXME: must pass mainwindow as argument to ProgressCallbackWidget
            callback = ProgressCallbackWidget(self)
            callback.set_title_text(_('Playing video + sub'))
            callback.set_label_text(_('Downloading files...'))
            callback.set_finished_text(_('Downloading finished'))
            callback.set_block(True)
            callback.set_range(0, 100)
            callback.show()
            try:
                subtitle_stream = selected_subtitle.download(
                    self.get_state().get_OSDBServer(), callback=callback)
            except ProviderConnectionError:
                log.debug('Unable to download subtitle "{}"'.format(
                    selected_subtitle.get_filename()),
                          exc_info=True)
                QMessageBox.about(
                    self, _('Error'),
                    _('Unable to download subtitle "{subtitle}"').format(
                        subtitle=selected_subtitle.get_filename()))
                callback.finish()
                return
            callback.finish()
            write_stream(subtitle_stream, subtitle_file_path)

        video = selected_subtitle.get_parent().get_parent().get_parent()

        def windows_escape(text):
            return '"{text}"'.format(text=text.replace('"', '\\"'))

        params = [windows_escape(programPath)]

        for param in parameters.split(' '):
            param = param.format(video.get_filepath(), subtitle_file_path)
            if platform.system() in ('Windows', 'Microsoft'):
                param = windows_escape(param)
            params.append(param)

        pid = None
        log.info('Running this command: {params}'.format(params=params))
        try:
            log.debug('Trying os.spawnvpe ...')
            pid = os.spawnvpe(os.P_NOWAIT, programPath, params, os.environ)
            log.debug('... SUCCESS. pid={pid}'.format(pid=pid))
        except AttributeError:
            log.debug('... FAILED', exc_info=True)
        except Exception as e:
            log.debug('... FAILED', exc_info=True)
        if pid is None:
            try:
                log.debug('Trying os.fork ...')
                pid = os.fork()
                if not pid:
                    log.debug('... SUCCESS. pid={pid}'.format(pid=pid))
                    os.execvpe(os.P_NOWAIT, programPath, params, os.environ)
            except:
                log.debug('... FAIL', exc_info=True)
        if pid is None:
            QMessageBox.about(self, _('Error'),
                              _('Unable to launch videoplayer'))

    @pyqtSlot(QModelIndex)
    def onClickVideoTreeView(self, index):
        data_item = self.videoModel.getSelectedItem(index)

        if isinstance(data_item, SubtitleFile):
            self.ui.buttonPlay.setEnabled(True)
        else:
            self.ui.buttonPlay.setEnabled(False)

        if isinstance(data_item, VideoFile):
            video = data_item
            if True:  # video.getMovieInfo():
                self.ui.buttonIMDB.setEnabled(True)
                self.ui.buttonIMDB.setIcon(QIcon(':/images/info.png'))
                self.ui.buttonIMDB.setText(_('Movie Info'))
        elif isinstance(data_item, RemoteSubtitleFile):
            self.ui.buttonIMDB.setEnabled(True)
            self.ui.buttonIMDB.setIcon(
                QIcon(':/images/sites/opensubtitles.png'))
            self.ui.buttonIMDB.setText(_('Subtitle Info'))
        else:
            self.ui.buttonIMDB.setEnabled(False)

    def onContext(self, point):
        # FIXME: code duplication with Main.onContext and/or SearchNameWidget and/or SearchFileWidget
        menu = QMenu('Menu', self)

        listview = self.ui.videoView

        index = listview.currentIndex()
        data_item = listview.model().getSelectedItem(index)
        if data_item is not None:
            if isinstance(data_item, VideoFile):
                video = data_item
                movie_info = video.getMovieInfo()
                if movie_info:
                    subWebsiteAction = QAction(QIcon(":/images/info.png"),
                                               _("View IMDB info"), self)
                    subWebsiteAction.triggered.connect(self.onViewOnlineInfo)
                else:
                    subWebsiteAction = QAction(QIcon(":/images/info.png"),
                                               _("Set IMDB info..."), self)
                    subWebsiteAction.triggered.connect(self.onSetIMDBInfo)
                menu.addAction(subWebsiteAction)
            elif isinstance(data_item, SubtitleFile):
                downloadAction = QAction(QIcon(":/images/download.png"),
                                         _("Download"), self)
                # Video tab, TODO:Replace me with a enum

                downloadAction.triggered.connect(self.onButtonDownload)
                playAction = QAction(QIcon(":/images/play.png"),
                                     _("Play video + subtitle"), self)
                playAction.triggered.connect(self.onButtonPlay)
                menu.addAction(playAction)

                subWebsiteAction = QAction(
                    QIcon(":/images/sites/opensubtitles.png"),
                    _("View online info"), self)

                menu.addAction(downloadAction)
                subWebsiteAction.triggered.connect(self.onViewOnlineInfo)
                menu.addAction(subWebsiteAction)
            elif isinstance(data_item, Movie):
                subWebsiteAction = QAction(QIcon(":/images/info.png"),
                                           _("View IMDB info"), self)
                subWebsiteAction.triggered.connect(self.onViewOnlineInfo)
                menu.addAction(subWebsiteAction)

        # Show the context menu.
        menu.exec_(listview.mapToGlobal(point))

    def onButtonDownload(self):
        # We download the subtitle in the same folder than the video
        subs = self.videoModel.get_checked_subtitles()
        replace_all = False
        skip_all = False
        if not subs:
            QMessageBox.about(self, _("Error"),
                              _("No subtitles selected to be downloaded"))
            return
        total_subs = len(subs)
        answer = None
        success_downloaded = 0

        # FIXME: must pass mainwindow as argument to ProgressCallbackWidget
        callback = ProgressCallbackWidget(self)
        callback.set_title_text(_("Downloading..."))
        callback.set_label_text(_("Downloading files..."))
        callback.set_updated_text(_("Downloading subtitle {0} ({1}/{2})"))
        callback.set_finished_text(
            _("{0} from {1} subtitles downloaded successfully"))
        callback.set_block(True)
        callback.set_range(0, total_subs)

        callback.show()

        for i, sub in enumerate(subs):
            if callback.canceled():
                break
            destinationPath = self.get_state().getDownloadPath(self, sub)
            if not destinationPath:
                break
            log.debug("Trying to download subtitle '%s'" % destinationPath)
            callback.update(i,
                            QFileInfo(destinationPath).fileName(), i + 1,
                            total_subs)

            # Check if we have write permissions, otherwise show warning window
            while True:
                # If the file and the folder don't have write access.
                if not QFileInfo(
                        destinationPath).isWritable() and not QFileInfo(
                            QFileInfo(destinationPath).absoluteDir().path()
                        ).isWritable():
                    warningBox = QMessageBox(
                        _("Error write permission"),
                        _("%s cannot be saved.\nCheck that the folder exists and you have write-access permissions."
                          ) % destinationPath, QMessageBox.Warning,
                        QMessageBox.Retry | QMessageBox.Default,
                        QMessageBox.Discard | QMessageBox.Escape,
                        QMessageBox.NoButton, self)

                    saveAsButton = warningBox.addButton(
                        _("Save as..."), QMessageBox.ActionRole)
                    answer = warningBox.exec_()
                    if answer == QMessageBox.Retry:
                        continue
                    elif answer == QMessageBox.Discard:
                        break  # Let's get out from the While true
                    # If we choose the SAVE AS
                    elif answer == QMessageBox.NoButton:
                        fileName, t = QFileDialog.getSaveFileName(
                            self, _("Save subtitle as..."), destinationPath,
                            'All (*.*)')
                        if fileName:
                            destinationPath = fileName
                else:  # If we have write access we leave the while loop.
                    break

            # If we have chosen Discard subtitle button.
            if answer == QMessageBox.Discard:
                continue  # Continue the next subtitle

            optionWhereToDownload = QSettings().value(
                "options/whereToDownload", "SAME_FOLDER")
            # Check if doesn't exists already, otherwise show fileExistsBox
            # dialog
            if QFileInfo(destinationPath).exists(
            ) and not replace_all and not skip_all and optionWhereToDownload != "ASK_FOLDER":
                # The "remote filename" below is actually not the real filename. Real name could be confusing
                # since we always rename downloaded sub to match movie
                # filename.
                fileExistsBox = QMessageBox(
                    QMessageBox.Warning, _("File already exists"),
                    _("Local: {local}\n\nRemote: {remote}\n\nHow would you like to proceed?"
                      ).format(local=destinationPath,
                               remote=QFileInfo(destinationPath).fileName()),
                    QMessageBox.NoButton, self)
                skipButton = fileExistsBox.addButton(_("Skip"),
                                                     QMessageBox.ActionRole)
                #                    skipAllButton = fileExistsBox.addButton(_("Skip all"), QMessageBox.ActionRole)
                replaceButton = fileExistsBox.addButton(
                    _("Replace"), QMessageBox.ActionRole)
                replaceAllButton = fileExistsBox.addButton(
                    _("Replace all"), QMessageBox.ActionRole)
                saveAsButton = fileExistsBox.addButton(_("Save as..."),
                                                       QMessageBox.ActionRole)
                cancelButton = fileExistsBox.addButton(_("Cancel"),
                                                       QMessageBox.ActionRole)
                fileExistsBox.exec_()
                answer = fileExistsBox.clickedButton()
                if answer == replaceAllButton:
                    # Don't ask us again (for this batch of files)
                    replace_all = True
                elif answer == saveAsButton:
                    # We will find a uniqiue filename and suggest this to user.
                    # add .<lang> to (inside) the filename. If that is not enough, start adding numbers.
                    # There should also be a preferences setting "Autorename
                    # files" or similar ( =never ask) FIXME
                    suggBaseName, suggFileExt = os.path.splitext(
                        destinationPath)
                    fNameCtr = 0  # Counter used to generate a unique filename
                    suggestedFileName = suggBaseName + '.' + \
                        sub.get_language().xxx() + suggFileExt
                    while (os.path.exists(suggestedFileName)):
                        fNameCtr += 1
                        suggestedFileName = suggBaseName + '.' + \
                            sub.get_language().xxx() + '-' + \
                            str(fNameCtr) + suggFileExt
                    fileName, t = QFileDialog.getSaveFileName(
                        None, _("Save subtitle as..."), suggestedFileName,
                        'All (*.*)')
                    if fileName:
                        destinationPath = fileName
                    else:
                        # Skip this particular file if no filename chosen
                        continue
                elif answer == skipButton:
                    continue  # Skip this particular file
#                    elif answer == skipAllButton:
#                        count += percentage
#                        skip_all = True # Skip all files already downloaded
#                        continue
                elif answer == cancelButton:
                    break  # Break out of DL loop - cancel was pushed
            QCoreApplication.processEvents()
            # FIXME: redundant update?
            callback.update(i,
                            QFileInfo(destinationPath).fileName(), i + 1,
                            total_subs)
            try:
                if not skip_all:
                    log.debug("Downloading subtitle '%s'" % destinationPath)
                    download_callback = ProgressCallback()  # FIXME
                    data_stream = sub.download(
                        provider_instance=self.get_state().get_OSDBServer(),
                        callback=download_callback,
                    )
                    write_stream(data_stream, destinationPath)
            except Exception as e:
                log.exception('Unable to Download subtitle {}'.format(
                    sub.get_filename()))
                QMessageBox.about(
                    self, _("Error"),
                    _("Unable to download subtitle %s") % sub.get_filename())
        callback.finish(success_downloaded, total_subs)

    def onViewOnlineInfo(self):
        # FIXME: code duplication with Main.onContext and/or SearchNameWidget and/or SearchFileWidget
        # Tab for SearchByHash TODO:replace this 0 by an ENUM value
        listview = self.ui.videoView
        index = listview.currentIndex()
        data_item = self.videoModel.getSelectedItem(index)

        if isinstance(data_item, VideoFile):
            video = data_item
            movie_info = video.getMovieInfo()
            if movie_info:
                imdb = movie_info["IDMovieImdb"]
                if imdb:
                    webbrowser.open("http://www.imdb.com/title/tt%s" % imdb,
                                    new=2,
                                    autoraise=1)

        elif isinstance(data_item, RemoteSubtitleFile):
            sub = data_item
            webbrowser.open(sub.get_link(), new=2, autoraise=1)

        elif isinstance(data_item, Movie):
            movie = data_item
            imdb = movie.IMDBId
            if imdb:
                webbrowser.open("http://www.imdb.com/title/tt%s" % imdb,
                                new=2,
                                autoraise=1)

    @pyqtSlot()
    def onSetIMDBInfo(self):
        #FIXME: DUPLICATED WITH SEARCHNAMEWIDGET
        QMessageBox.about(self, _("Info"), "Not implemented yet. Sorry...")
Exemplo n.º 10
0
class WatchPointViewer(QTreeView):
    """
    Class implementing the watch expression viewer widget.
    
    Watch expressions will be shown with all their details. They can be
    modified through the context menu of this widget.
    """
    def __init__(self, parent=None):
        """
        Constructor
        
        @param parent the parent (QWidget)
        """
        super(WatchPointViewer, self).__init__(parent)
        self.setObjectName("WatchExpressionViewer")
        
        self.__model = None
        
        self.setItemsExpandable(False)
        self.setRootIsDecorated(False)
        self.setAlternatingRowColors(True)
        self.setSelectionMode(QAbstractItemView.ExtendedSelection)
        self.setSelectionBehavior(QAbstractItemView.SelectRows)
        
        self.setWindowTitle(self.tr("Watchpoints"))
        
        self.setContextMenuPolicy(Qt.CustomContextMenu)
        self.customContextMenuRequested.connect(self.__showContextMenu)
        self.doubleClicked.connect(self.__doubleClicked)
        
        self.__createPopupMenus()
        
    def setModel(self, model):
        """
        Public slot to set the watch expression model.
        
        @param model reference to the watch expression model (WatchPointModel)
        """
        self.__model = model
        
        self.sortingModel = QSortFilterProxyModel()
        self.sortingModel.setDynamicSortFilter(True)
        self.sortingModel.setSourceModel(self.__model)
        super(WatchPointViewer, self).setModel(self.sortingModel)
        
        header = self.header()
        header.setSortIndicator(0, Qt.AscendingOrder)
        header.setSortIndicatorShown(True)
        if qVersion() >= "5.0.0":
            header.setSectionsClickable(True)
        else:
            header.setClickable(True)
        
        self.setSortingEnabled(True)
        
        self.__layoutDisplay()
        
    def __layoutDisplay(self):
        """
        Private slot to perform a layout operation.
        """
        self.__resizeColumns()
        self.__resort()
        
    def __resizeColumns(self):
        """
        Private slot to resize the view when items get added, edited or
        deleted.
        """
        self.header().resizeSections(QHeaderView.ResizeToContents)
        self.header().setStretchLastSection(True)
    
    def __resort(self):
        """
        Private slot to resort the tree.
        """
        self.model().sort(self.header().sortIndicatorSection(),
                          self.header().sortIndicatorOrder())
        
    def __toSourceIndex(self, index):
        """
        Private slot to convert an index to a source index.
        
        @param index index to be converted (QModelIndex)
        @return mapped index (QModelIndex)
        """
        return self.sortingModel.mapToSource(index)
        
    def __fromSourceIndex(self, sindex):
        """
        Private slot to convert a source index to an index.
        
        @param sindex source index to be converted (QModelIndex)
        @return mapped index (QModelIndex)
        """
        return self.sortingModel.mapFromSource(sindex)
        
    def __setRowSelected(self, index, selected=True):
        """
        Private slot to select a complete row.
        
        @param index index determining the row to be selected (QModelIndex)
        @param selected flag indicating the action (bool)
        """
        if not index.isValid():
            return
        
        if selected:
            flags = QItemSelectionModel.SelectionFlags(
                QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows)
        else:
            flags = QItemSelectionModel.SelectionFlags(
                QItemSelectionModel.Deselect | QItemSelectionModel.Rows)
        self.selectionModel().select(index, flags)
        
    def __createPopupMenus(self):
        """
        Private method to generate the popup menus.
        """
        self.menu = QMenu()
        self.menu.addAction(self.tr("Add"), self.__addWatchPoint)
        self.menu.addAction(self.tr("Edit..."), self.__editWatchPoint)
        self.menu.addSeparator()
        self.menu.addAction(self.tr("Enable"), self.__enableWatchPoint)
        self.menu.addAction(self.tr("Enable all"),
                            self.__enableAllWatchPoints)
        self.menu.addSeparator()
        self.menu.addAction(self.tr("Disable"), self.__disableWatchPoint)
        self.menu.addAction(self.tr("Disable all"),
                            self.__disableAllWatchPoints)
        self.menu.addSeparator()
        self.menu.addAction(self.tr("Delete"), self.__deleteWatchPoint)
        self.menu.addAction(self.tr("Delete all"),
                            self.__deleteAllWatchPoints)
        self.menu.addSeparator()
        self.menu.addAction(self.tr("Configure..."), self.__configure)

        self.backMenuActions = {}
        self.backMenu = QMenu()
        self.backMenu.addAction(self.tr("Add"), self.__addWatchPoint)
        self.backMenuActions["EnableAll"] = \
            self.backMenu.addAction(self.tr("Enable all"),
                                    self.__enableAllWatchPoints)
        self.backMenuActions["DisableAll"] = \
            self.backMenu.addAction(self.tr("Disable all"),
                                    self.__disableAllWatchPoints)
        self.backMenuActions["DeleteAll"] = \
            self.backMenu.addAction(self.tr("Delete all"),
                                    self.__deleteAllWatchPoints)
        self.backMenu.addSeparator()
        self.backMenu.addAction(self.tr("Configure..."), self.__configure)
        self.backMenu.aboutToShow.connect(self.__showBackMenu)

        self.multiMenu = QMenu()
        self.multiMenu.addAction(self.tr("Add"), self.__addWatchPoint)
        self.multiMenu.addSeparator()
        self.multiMenu.addAction(self.tr("Enable selected"),
                                 self.__enableSelectedWatchPoints)
        self.multiMenu.addAction(self.tr("Enable all"),
                                 self.__enableAllWatchPoints)
        self.multiMenu.addSeparator()
        self.multiMenu.addAction(self.tr("Disable selected"),
                                 self.__disableSelectedWatchPoints)
        self.multiMenu.addAction(self.tr("Disable all"),
                                 self.__disableAllWatchPoints)
        self.multiMenu.addSeparator()
        self.multiMenu.addAction(self.tr("Delete selected"),
                                 self.__deleteSelectedWatchPoints)
        self.multiMenu.addAction(self.tr("Delete all"),
                                 self.__deleteAllWatchPoints)
        self.multiMenu.addSeparator()
        self.multiMenu.addAction(self.tr("Configure..."), self.__configure)
    
    def __showContextMenu(self, coord):
        """
        Private slot to show the context menu.
        
        @param coord the position of the mouse pointer (QPoint)
        """
        cnt = self.__getSelectedItemsCount()
        if cnt <= 1:
            index = self.indexAt(coord)
            if index.isValid():
                cnt = 1
                self.__setRowSelected(index)
        coord = self.mapToGlobal(coord)
        if cnt > 1:
            self.multiMenu.popup(coord)
        elif cnt == 1:
            self.menu.popup(coord)
        else:
            self.backMenu.popup(coord)
    
    def __clearSelection(self):
        """
        Private slot to clear the selection.
        """
        for index in self.selectedIndexes():
            self.__setRowSelected(index, False)
    
    def __findDuplicates(self, cond, special, showMessage=False,
                         index=QModelIndex()):
        """
        Private method to check, if an entry already exists.
        
        @param cond condition to check (string)
        @param special special condition to check (string)
        @param showMessage flag indicating a message should be shown,
            if a duplicate entry is found (boolean)
        @param index index that should not be considered duplicate
            (QModelIndex)
        @return flag indicating a duplicate entry (boolean)
        """
        idx = self.__model.getWatchPointIndex(cond, special)
        duplicate = idx.isValid() and \
            idx.internalPointer() != index.internalPointer()
        if showMessage and duplicate:
            if not special:
                msg = self.tr("""<p>A watch expression '<b>{0}</b>'"""
                              """ already exists.</p>""")\
                    .format(Utilities.html_encode(cond))
            else:
                msg = self.tr(
                    """<p>A watch expression '<b>{0}</b>'"""
                    """ for the variable <b>{1}</b> already exists.</p>""")\
                    .format(special, Utilities.html_encode(cond))
            E5MessageBox.warning(
                self,
                self.tr("Watch expression already exists"),
                msg)
        
        return duplicate
    
    def __addWatchPoint(self):
        """
        Private slot to handle the add watch expression context menu entry.
        """
        from .EditWatchpointDialog import EditWatchpointDialog
        dlg = EditWatchpointDialog(("", False, True, 0, ""), self)
        if dlg.exec_() == QDialog.Accepted:
            cond, temp, enabled, ignorecount, special = dlg.getData()
            if not self.__findDuplicates(cond, special, True):
                self.__model.addWatchPoint(cond, special,
                                           (temp, enabled, ignorecount))
                self.__resizeColumns()
                self.__resort()

    def __doubleClicked(self, index):
        """
        Private slot to handle the double clicked signal.
        
        @param index index of the entry that was double clicked (QModelIndex)
        """
        if index.isValid():
            self.__doEditWatchPoint(index)

    def __editWatchPoint(self):
        """
        Private slot to handle the edit watch expression context menu entry.
        """
        index = self.currentIndex()
        if index.isValid():
            self.__doEditWatchPoint(index)
    
    def __doEditWatchPoint(self, index):
        """
        Private slot to edit a watch expression.
        
        @param index index of watch expression to be edited (QModelIndex)
        """
        sindex = self.__toSourceIndex(index)
        if sindex.isValid():
            wp = self.__model.getWatchPointByIndex(sindex)
            if not wp:
                return
            
            cond, special, temp, enabled, count = wp[:5]
            
            from .EditWatchpointDialog import EditWatchpointDialog
            dlg = EditWatchpointDialog(
                (cond, temp, enabled, count, special), self)
            if dlg.exec_() == QDialog.Accepted:
                cond, temp, enabled, count, special = dlg.getData()
                if not self.__findDuplicates(cond, special, True, sindex):
                    self.__model.setWatchPointByIndex(
                        sindex, cond, special, (temp, enabled, count))
                    self.__resizeColumns()
                    self.__resort()

    def __setWpEnabled(self, index, enabled):
        """
        Private method to set the enabled status of a watch expression.
        
        @param index index of watch expression to be enabled/disabled
            (QModelIndex)
        @param enabled flag indicating the enabled status to be set (boolean)
        """
        sindex = self.__toSourceIndex(index)
        if sindex.isValid():
            self.__model.setWatchPointEnabledByIndex(sindex, enabled)
        
    def __enableWatchPoint(self):
        """
        Private slot to handle the enable watch expression context menu entry.
        """
        index = self.currentIndex()
        self.__setWpEnabled(index, True)
        self.__resizeColumns()
        self.__resort()

    def __enableAllWatchPoints(self):
        """
        Private slot to handle the enable all watch expressions context menu
        entry.
        """
        index = self.model().index(0, 0)
        while index.isValid():
            self.__setWpEnabled(index, True)
            index = self.indexBelow(index)
        self.__resizeColumns()
        self.__resort()

    def __enableSelectedWatchPoints(self):
        """
        Private slot to handle the enable selected watch expressions context
        menu entry.
        """
        for index in self.selectedIndexes():
            if index.column() == 0:
                self.__setWpEnabled(index, True)
        self.__resizeColumns()
        self.__resort()

    def __disableWatchPoint(self):
        """
        Private slot to handle the disable watch expression context menu entry.
        """
        index = self.currentIndex()
        self.__setWpEnabled(index, False)
        self.__resizeColumns()
        self.__resort()

    def __disableAllWatchPoints(self):
        """
        Private slot to handle the disable all watch expressions context menu
        entry.
        """
        index = self.model().index(0, 0)
        while index.isValid():
            self.__setWpEnabled(index, False)
            index = self.indexBelow(index)
        self.__resizeColumns()
        self.__resort()

    def __disableSelectedWatchPoints(self):
        """
        Private slot to handle the disable selected watch expressions context
        menu entry.
        """
        for index in self.selectedIndexes():
            if index.column() == 0:
                self.__setWpEnabled(index, False)
        self.__resizeColumns()
        self.__resort()

    def __deleteWatchPoint(self):
        """
        Private slot to handle the delete watch expression context menu entry.
        """
        index = self.currentIndex()
        sindex = self.__toSourceIndex(index)
        if sindex.isValid():
            self.__model.deleteWatchPointByIndex(sindex)
        
    def __deleteAllWatchPoints(self):
        """
        Private slot to handle the delete all watch expressions context menu
        entry.
        """
        self.__model.deleteAll()

    def __deleteSelectedWatchPoints(self):
        """
        Private slot to handle the delete selected watch expressions context
        menu entry.
        """
        idxList = []
        for index in self.selectedIndexes():
            sindex = self.__toSourceIndex(index)
            if sindex.isValid() and index.column() == 0:
                idxList.append(sindex)
        self.__model.deleteWatchPoints(idxList)

    def __showBackMenu(self):
        """
        Private slot to handle the aboutToShow signal of the background menu.
        """
        if self.model().rowCount() == 0:
            self.backMenuActions["EnableAll"].setEnabled(False)
            self.backMenuActions["DisableAll"].setEnabled(False)
            self.backMenuActions["DeleteAll"].setEnabled(False)
        else:
            self.backMenuActions["EnableAll"].setEnabled(True)
            self.backMenuActions["DisableAll"].setEnabled(True)
            self.backMenuActions["DeleteAll"].setEnabled(True)

    def __getSelectedItemsCount(self):
        """
        Private method to get the count of items selected.
        
        @return count of items selected (integer)
        """
        count = len(self.selectedIndexes()) // (self.__model.columnCount() - 1)
        # column count is 1 greater than selectable
        return count
    
    def __configure(self):
        """
        Private method to open the configuration dialog.
        """
        e5App().getObject("UserInterface")\
            .showPreferences("debuggerGeneralPage")
Exemplo n.º 11
0
class MainWindow(QMainWindow, Ui_MainWindow):
	"""
	Main window for the application with groups and password lists
	"""

	KEY_IDX = 0  # column where key is shown in password table
	PASSWORD_IDX = 1  # column where password is shown in password table
	COMMENTS_IDX = 2  # column where comments is shown in password table
	NO_OF_PASSWDTABLE_COLUMNS = 3  # 3 columns: key + value/passwd/secret + comments
	CACHE_IDX = 0  # column of QWidgetItem in whose data we cache decrypted passwords+comments

	def __init__(self, pwMap, settings, dbFilename):
		"""
		@param pwMap: a PasswordMap instance with encrypted passwords
		@param dbFilename: file name for saving pwMap
		"""
		super(MainWindow, self).__init__()
		self.setupUi(self)

		self.logger = settings.logger
		self.settings = settings
		self.pwMap = pwMap
		self.selectedGroup = None
		self.modified = False  # modified flag for "Save?" question on exit
		self.dbFilename = dbFilename

		self.groupsModel = QStandardItemModel(parent=self)
		self.groupsModel.setHorizontalHeaderLabels([u"Password group"])
		self.groupsFilter = QSortFilterProxyModel(parent=self)
		self.groupsFilter.setSourceModel(self.groupsModel)

		self.groupsTree.setModel(self.groupsFilter)
		self.groupsTree.setContextMenuPolicy(Qt.CustomContextMenu)
		self.groupsTree.customContextMenuRequested.connect(self.showGroupsContextMenu)
		# Dont use e following line, it would cause loadPasswordsBySelection
		# to be called twice on mouse-click.
		# self.groupsTree.clicked.connect(self.loadPasswordsBySelection)
		self.groupsTree.selectionModel().selectionChanged.connect(self.loadPasswordsBySelection)
		self.groupsTree.setSortingEnabled(True)

		self.passwordTable.setContextMenuPolicy(Qt.CustomContextMenu)
		self.passwordTable.customContextMenuRequested.connect(self.showPasswdContextMenu)
		self.passwordTable.setSelectionBehavior(QAbstractItemView.SelectRows)
		self.passwordTable.setSelectionMode(QAbstractItemView.SingleSelection)

		shortcut = QShortcut(QKeySequence(u"Ctrl+C"), self.passwordTable, self.copyPasswordFromSelection)
		shortcut.setContext(Qt.WidgetShortcut)

		self.actionQuit.triggered.connect(self.close)
		self.actionQuit.setShortcut(QKeySequence(u"Ctrl+Q"))
		self.actionExport.triggered.connect(self.exportCsv)
		self.actionImport.triggered.connect(self.importCsv)
		self.actionBackup.triggered.connect(self.saveBackup)
		self.actionAbout.triggered.connect(self.printAbout)
		self.actionSave.triggered.connect(self.saveDatabase)
		self.actionSave.setShortcut(QKeySequence(u"Ctrl+S"))

		# headerKey = QTableWidgetItem(u"Key")
		# headerValue = QTableWidgetItem(u"Password/Value")
		# headerComments = QTableWidgetItem(u"Comments")
		# self.passwordTable.setColumnCount(self.NO_OF_PASSWDTABLE_COLUMNS)
		# self.passwordTable.setHorizontalHeaderItem(self.KEY_IDX, headerKey)
		# self.passwordTable.setHorizontalHeaderItem(self.PASSWORD_IDX, headerValue)
		# self.passwordTable.setHorizontalHeaderItem(self.COMMENTS_IDX, headerComments)
		#
		# self.passwordTable.resizeRowsToContents()
		# self.passwordTable.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch)
		# self.passwordTable.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeToContents)
		# self.passwordTable.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeToContents)

		self.searchEdit.textChanged.connect(self.filterGroups)

		if pwMap is not None:
			self.setPwMap(pwMap)

		self.clipboard = QApplication.clipboard()

		self.timer = QTimer(parent=self)
		self.timer.timeout.connect(self.clearClipboard)

	def setPwMap(self, pwMap):
		""" if not done in __init__ pwMap can be supplied later """
		self.pwMap = pwMap
		groupNames = self.pwMap.groups.keys()
		for groupName in groupNames:
			item = QStandardItem(groupName)
			self.groupsModel.appendRow(item)
		self.groupsTree.sortByColumn(0, Qt.AscendingOrder)
		self.settings.mlogger.log("pwMap was initialized.",
			logging.DEBUG, "GUI IO")

	def setModified(self, modified):
		"""
		Sets the modified flag so that user is notified when exiting
		with unsaved changes.
		"""
		self.modified = modified
		self.setWindowTitle("TrezorPass" + "*" * int(self.modified))

	def showGroupsContextMenu(self, point):
		"""
		Show context menu for group management.

		@param point: point in self.groupsTree where click occured
		"""
		self.addGroupMenu = QMenu(self)
		newGroupAction = QAction('Add group', self)
		editGroupAction = QAction('Rename group', self)
		deleteGroupAction = QAction('Delete group', self)
		self.addGroupMenu.addAction(newGroupAction)
		self.addGroupMenu.addAction(editGroupAction)
		self.addGroupMenu.addAction(deleteGroupAction)

		# disable deleting if no point is clicked on
		proxyIdx = self.groupsTree.indexAt(point)
		itemIdx = self.groupsFilter.mapToSource(proxyIdx)
		item = self.groupsModel.itemFromIndex(itemIdx)
		if item is None:
			deleteGroupAction.setEnabled(False)

		action = self.addGroupMenu.exec_(self.groupsTree.mapToGlobal(point))

		if action == newGroupAction:
			self.createGroupWithCheck()
		elif action == editGroupAction:
			self.editGroupWithCheck(item)
		elif action == deleteGroupAction:
			self.deleteGroupWithCheck(item)

	def showPasswdContextMenu(self, point):
		"""
		Show context menu for password management

		@param point: point in self.passwordTable where click occured
		"""
		self.passwdMenu = QMenu(self)
		showPasswordAction = QAction('Show password', self)
		copyPasswordAction = QAction('Copy password', self)
		copyPasswordAction.setShortcut(QKeySequence("Ctrl+C"))
		showCommentsAction = QAction('Show comments', self)
		copyCommentsAction = QAction('Copy comments', self)
		showAllAction = QAction('Show all of group', self)
		newItemAction = QAction('New item', self)
		deleteItemAction = QAction('Delete item', self)
		editItemAction = QAction('Edit item', self)
		self.passwdMenu.addAction(showPasswordAction)
		self.passwdMenu.addAction(copyPasswordAction)
		self.passwdMenu.addSeparator()
		self.passwdMenu.addAction(showCommentsAction)
		self.passwdMenu.addAction(copyCommentsAction)
		self.passwdMenu.addSeparator()
		self.passwdMenu.addAction(showAllAction)
		self.passwdMenu.addSeparator()
		self.passwdMenu.addAction(newItemAction)
		self.passwdMenu.addAction(deleteItemAction)
		self.passwdMenu.addAction(editItemAction)

		# disable creating if no group is selected
		if self.selectedGroup is None:
			newItemAction.setEnabled(False)
			showAllAction.setEnabled(False)

		# disable deleting if no point is clicked on
		item = self.passwordTable.itemAt(point.x(), point.y())
		if item is None:
			deleteItemAction.setEnabled(False)
			showPasswordAction.setEnabled(False)
			copyPasswordAction.setEnabled(False)
			showCommentsAction.setEnabled(False)
			copyCommentsAction.setEnabled(False)
			editItemAction.setEnabled(False)

		action = self.passwdMenu.exec_(self.passwordTable.mapToGlobal(point))
		if action == newItemAction:
			self.createPassword()
		elif action == deleteItemAction:
			self.deletePassword(item)
		elif action == editItemAction:
			self.editPassword(item)
		elif action == copyPasswordAction:
			self.copyPasswordFromItem(item)
		elif action == showPasswordAction:
			self.showPassword(item)
		elif action == copyCommentsAction:
			self.copyCommentsFromItem(item)
		elif action == showCommentsAction:
			self.showComments(item)
		elif action == showAllAction:
			self.showAll()

	def createGroup(self, groupName, group=None):
		"""
		Slot to create a password group.
		"""
		newItem = QStandardItem(groupName)
		self.groupsModel.appendRow(newItem)
		self.pwMap.addGroup(groupName)
		if group is not None:
			self.pwMap.replaceGroup(groupName, group)

		# make new item selected to save a few clicks
		itemIdx = self.groupsModel.indexFromItem(newItem)
		proxyIdx = self.groupsFilter.mapFromSource(itemIdx)
		self.groupsTree.selectionModel().select(proxyIdx,
			QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows)
		self.groupsTree.sortByColumn(0, Qt.AscendingOrder)

		# Make item's passwords loaded so new key-value entries can be created
		# right away - better from UX perspective.
		self.loadPasswords(newItem)

		self.setModified(True)
		self.settings.mlogger.log("Group '%s' was created." % (groupName),
			logging.DEBUG, "GUI IO")

	def createGroupWithCheck(self):
		"""
		Slot to create a password group.
		"""
		dialog = AddGroupDialog(self.pwMap.groups, self.settings)
		if not dialog.exec_():
			return
		groupName = dialog.newGroupName()
		self.createGroup(groupName)

	def createRenamedGroupWithCheck(self, groupNameOld, groupNameNew):
		"""
		Creates a copy of a group by name as utf-8 encoded string
		with a new group name.
		A more appropriate name for the method would be:
		createRenamedGroup().
		Since the entries inside the group are encrypted
		with the groupName, we cannot simply make a copy.
		We must decrypt with old name and afterwards encrypt
		with new name.
		If the group has many entries, each entry would require a 'Confirm'
		press on Trezor. So, to mkae it faster and more userfriendly
		we use the backup key to decrypt. This requires a single
		Trezor 'Confirm' press independent of how many entries there are
		in the group.
		@param groupNameOld: name of group to copy and rename
		@type groupNameOld: string
		@param groupNameNew: name of group to be created
		@type groupNameNew: string
		"""
		if groupNameOld not in self.pwMap.groups:
			raise KeyError("Password group does not exist")
		# with less than 3 rows dont bother the user with a pop-up
		rowCount = len(self.pwMap.groups[groupNameOld].entries)
		if rowCount < 3:
			self.pwMap.createRenamedGroupSecure(groupNameOld, groupNameNew)
			return

		msgBox = QMessageBox(parent=self)
		msgBox.setText("Do you want to use the more secure way?")
		msgBox.setIcon(QMessageBox.Question)
		msgBox.setWindowTitle("How to decrypt?")
		msgBox.setDetailedText("The more secure way requires pressing 'Confirm' "
			"on Trezor once for each entry in the group. %d presses in this "
			"case. This is recommended. "
			"Select 'Yes'.\n\n"
			"The less secure way requires only a single 'Confirm' click on the "
			"Trezor. This is not recommended. "
			"Select 'No'." % (rowCount))
		msgBox.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
		msgBox.setDefaultButton(QMessageBox.Yes)
		res = msgBox.exec_()

		if res == QMessageBox.Yes:
			moreSecure = True
		else:
			moreSecure = False
		groupNew = self.pwMap.createRenamedGroup(groupNameOld, groupNameNew, moreSecure)
		self.settings.mlogger.log("Copy of group '%s' with new name '%s' "
			"was created the %s way." %
			(groupNameOld, groupNameNew, 'secure' if moreSecure else 'fast'),
			logging.DEBUG, "GUI IO")
		return(groupNew)

	def editGroup(self, item, groupNameOld, groupNameNew):
		"""
		Slot to edit name a password group.
		"""
		groupNew = self.createRenamedGroupWithCheck(groupNameOld, groupNameNew)
		self.deleteGroup(item)
		self.createGroup(groupNameNew, groupNew)
		self.settings.mlogger.log("Group '%s' was renamed to '%s'." % (groupNameOld, groupNameNew),
			logging.DEBUG, "GUI IO")

	def editGroupWithCheck(self, item):
		"""
		Slot to edit name a password group.
		"""
		groupNameOld = encoding.normalize_nfc(item.text())
		dialog = AddGroupDialog(self.pwMap.groups, self.settings)
		dialog.setWindowTitle("Edit group name")
		dialog.groupNameLabel.setText("New name for group")
		dialog.setNewGroupName(groupNameOld)
		if not dialog.exec_():
			return

		groupNameNew = dialog.newGroupName()
		self.editGroup(item, groupNameOld, groupNameNew)

	def deleteGroup(self, item):  # without checking user
		groupName = encoding.normalize_nfc(item.text())
		self.selectedGroup = None
		del self.pwMap.groups[groupName]

		itemIdx = self.groupsModel.indexFromItem(item)
		self.groupsModel.takeRow(itemIdx.row())
		self.passwordTable.setRowCount(0)
		self.groupsTree.clearSelection()

		self.setModified(True)
		self.settings.mlogger.log("Group '%s' was deleted." % (groupName),
			logging.DEBUG, "GUI IO")

	def deleteGroupWithCheck(self, item):
		msgBox = QMessageBox(text="Are you sure about delete?", parent=self)
		msgBox.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
		res = msgBox.exec_()

		if res != QMessageBox.Yes:
			return

		self.deleteGroup(item)

	def deletePassword(self, item):
		msgBox = QMessageBox(text="Are you sure about delete?", parent=self)
		msgBox.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
		res = msgBox.exec_()

		if res != QMessageBox.Yes:
			return

		row = self.passwordTable.row(item)
		self.passwordTable.removeRow(row)
		group = self.pwMap.groups[self.selectedGroup]
		group.removeEntry(row)

		self.passwordTable.resizeRowsToContents()
		self.passwordTable.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch)
		self.passwordTable.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeToContents)
		self.passwordTable.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeToContents)
		self.setModified(True)
		self.settings.mlogger.log("Row '%d' was deleted." % (row),
			logging.DEBUG, "GUI IO")

	def logCache(self, row):
		item = self.passwordTable.item(row, self.CACHE_IDX)
		cachedTuple = item.data(Qt.UserRole)
		if cachedTuple is None:
			cachedPassword, cachedComments = (None, None)
		else:
			cachedPassword, cachedComments = cachedTuple
		if cachedPassword is not None:
			cachedPassword = u'***'
		if cachedComments is not None:
			cachedComments = cachedComments[0:3] + u'...'
		self.settings.mlogger.log("Cache holds '%s' and '%s'." %
			(cachedPassword, cachedComments), logging.DEBUG, "Cache")

	def cachePasswordComments(self, row, password, comments):
		item = self.passwordTable.item(row, self.CACHE_IDX)
		item.setData(Qt.UserRole, QVariant((password, comments)))

	def cachedPassword(self, row):
		"""
		Retrieve cached password for given row of currently selected group.
		Returns password as string or None if no password cached.
		"""
		item = self.passwordTable.item(row, self.CACHE_IDX)
		cachedTuple = item.data(Qt.UserRole)
		if cachedTuple is None:
			cachedPassword, cachedComments = (None, None)
		else:
			cachedPassword, cachedComments = cachedTuple
		return cachedPassword

	def cachedComments(self, row):
		"""
		Retrieve cached comments for given row of currently selected group.
		Returns comments as string or None if no coments cached.
		"""
		item = self.passwordTable.item(row, self.CACHE_IDX)
		cachedTuple = item.data(Qt.UserRole)
		if cachedTuple is None:
			cachedPassword, cachedComments = (None, None)
		else:
			cachedPassword, cachedComments = cachedTuple
		return cachedComments

	def cachedOrDecryptPassword(self, row):
		"""
		Try retrieving cached password for item in given row, otherwise
		decrypt with Trezor.
		"""
		cached = self.cachedPassword(row)

		if cached is not None:
			return cached
		else:  # decrypt with Trezor
			group = self.pwMap.groups[self.selectedGroup]
			pwEntry = group.entry(row)
			encPwComments = pwEntry[1]

			decryptedPwComments = self.pwMap.decryptPassword(encPwComments, self.selectedGroup)
			lngth = int(decryptedPwComments[0:4])
			decryptedPassword = decryptedPwComments[4:4+lngth]
			decryptedComments = decryptedPwComments[4+lngth:]
			# while we are at it, cache the comments too
			self.cachePasswordComments(row, decryptedPassword, decryptedComments)
			self.settings.mlogger.log("Decrypted password and comments "
				"for '%s', row '%d'." % (pwEntry[0], row),
				logging.DEBUG, "GUI IO")
		return decryptedPassword

	def cachedOrDecryptComments(self, row):
		"""
		Try retrieving cached comments for item in given row, otherwise
		decrypt with Trezor.
		"""
		cached = self.cachedComments(row)

		if cached is not None:
			return cached
		else:  # decrypt with Trezor
			group = self.pwMap.groups[self.selectedGroup]
			pwEntry = group.entry(row)
			encPwComments = pwEntry[1]

			decryptedPwComments = self.pwMap.decryptPassword(encPwComments, self.selectedGroup)
			lngth = int(decryptedPwComments[0:4])
			decryptedPassword = decryptedPwComments[4:4+lngth]
			decryptedComments = decryptedPwComments[4+lngth:]
			self.cachePasswordComments(row, decryptedPassword, decryptedComments)
			self.settings.mlogger.log("Decrypted password and comments "
				"for '%s', row '%d'." % (pwEntry[0], row),
				logging.DEBUG, "GUI IO")
		return decryptedComments

	def showPassword(self, item):
		# check if this password has been decrypted, use cached version
		row = self.passwordTable.row(item)
		self.logCache(row)
		try:
			decryptedPassword = self.cachedOrDecryptPassword(row)
		except CallException:
			return
		item = QTableWidgetItem(decryptedPassword)

		self.passwordTable.setItem(row, self.PASSWORD_IDX, item)

	def showComments(self, item):
		# check if these comments has been decrypted, use cached version
		row = self.passwordTable.row(item)
		try:
			decryptedComments = self.cachedOrDecryptComments(row)
		except CallException:
			return
		item = QTableWidgetItem(decryptedComments)

		self.passwordTable.setItem(row, self.COMMENTS_IDX, item)

	def showAllSecure(self):
		rowCount = self.passwordTable.rowCount()
		for row in range(rowCount):
			try:
				decryptedPassword = self.cachedOrDecryptPassword(row)
			except CallException:
				return
			item = QTableWidgetItem(decryptedPassword)
			self.passwordTable.setItem(row, self.PASSWORD_IDX, item)
			try:
				decryptedComments = self.cachedOrDecryptComments(row)
			except CallException:
				return
			item = QTableWidgetItem(decryptedComments)
			self.passwordTable.setItem(row, self.COMMENTS_IDX, item)

		self.settings.mlogger.log("Showed all entries for group '%s' the secure way." %
			(self.selectedGroup), logging.DEBUG, "GUI IO")

	def showAllFast(self):
		try:
			privateKey = self.pwMap.backupKey.unwrapPrivateKey()
		except CallException:
			return
		group = self.pwMap.groups[self.selectedGroup]

		row = 0
		for key, _, bkupPw in group.entries:
			decryptedPwComments = self.pwMap.backupKey.decryptPassword(bkupPw, privateKey)
			lngth = int(decryptedPwComments[0:4])
			password = decryptedPwComments[4:4+lngth]
			comments = decryptedPwComments[4+lngth:]
			item = QTableWidgetItem(key)
			pwItem = QTableWidgetItem(password)
			commentsItem = QTableWidgetItem(comments)
			self.passwordTable.setItem(row, self.KEY_IDX, item)
			self.passwordTable.setItem(row, self.PASSWORD_IDX, pwItem)
			self.passwordTable.setItem(row, self.COMMENTS_IDX, commentsItem)
			self.cachePasswordComments(row, password, comments)
			row = row+1

		self.passwordTable.resizeRowsToContents()
		self.passwordTable.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch)
		self.passwordTable.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeToContents)
		self.passwordTable.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeToContents)

		self.settings.mlogger.log("Showed all entries for group '%s' the fast way." %
			(self.selectedGroup), logging.DEBUG, "GUI IO")

	def showAll(self):
		"""
		show all passwords and comments in plaintext in GUI
		can be called without any password selectedGroup
		a group must be selected
		"""
		# with less than 3 rows dont bother the user with a pop-up
		if self.passwordTable.rowCount() < 3:
			self.showAllSecure()
			return

		msgBox = QMessageBox(parent=self)
		msgBox.setText("Do you want to use the more secure way?")
		msgBox.setIcon(QMessageBox.Question)
		msgBox.setWindowTitle("How to decrypt?")
		msgBox.setDetailedText("The more secure way requires pressing 'Confirm' "
			"on Trezor once for each entry in the group. %d presses in this "
			"case. This is recommended. "
			"Select 'Yes'.\n\n"
			"The less secure way requires only a single 'Confirm' click on the "
			"Trezor. This is not recommended. "
			"Select 'No'." % (self.passwordTable.rowCount()))
		msgBox.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
		msgBox.setDefaultButton(QMessageBox.Yes)
		res = msgBox.exec_()

		if res == QMessageBox.Yes:
			self.showAllSecure()
		else:
			self.showAllFast()

	def createPassword(self):
		"""
		Slot to create key-value password entry.
		"""
		if self.selectedGroup is None:
			return
		group = self.pwMap.groups[self.selectedGroup]
		dialog = AddPasswordDialog(self.pwMap.trezor, self.settings)
		if not dialog.exec_():
			return

		plainPw = dialog.pw1()
		plainComments = dialog.comments()
		if len(plainPw) + len(plainComments) > basics.MAX_SIZE_OF_PASSWDANDCOMMENTS:
			self.settings.mlogger.log("Password and/or comments too long. "
				"Combined they must not be larger than %d." %
				basics.MAX_SIZE_OF_PASSWDANDCOMMENTS,
				logging.CRITICAL, "User IO")
			return

		row = self.passwordTable.rowCount()
		self.passwordTable.setRowCount(row+1)
		item = QTableWidgetItem(dialog.key())
		pwItem = QTableWidgetItem("*****")
		commentsItem = QTableWidgetItem("*****")
		self.passwordTable.setItem(row, self.KEY_IDX, item)
		self.passwordTable.setItem(row, self.PASSWORD_IDX, pwItem)
		self.passwordTable.setItem(row, self.COMMENTS_IDX, commentsItem)

		plainPwComments = ("%4d" % len(plainPw)) + plainPw + plainComments
		encPw = self.pwMap.encryptPassword(plainPwComments, self.selectedGroup)
		bkupPw = self.pwMap.backupKey.encryptPassword(plainPwComments)
		group.addEntry(dialog.key(), encPw, bkupPw)

		self.cachePasswordComments(row, plainPw, plainComments)

		self.passwordTable.resizeRowsToContents()
		self.passwordTable.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch)
		self.passwordTable.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeToContents)
		self.passwordTable.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeToContents)
		self.setModified(True)
		self.settings.mlogger.log("Password and comments entry "
			"for '%s', row '%d' was created." % (dialog.key(), row),
			logging.DEBUG, "GUI IO")

	def editPassword(self, item):
		row = self.passwordTable.row(item)
		group = self.pwMap.groups[self.selectedGroup]
		try:
			decrypted = self.cachedOrDecryptPassword(row)
			decryptedComments = self.cachedOrDecryptComments(row)
		except CallException:
			return

		dialog = AddPasswordDialog(self.pwMap.trezor, self.settings)
		entry = group.entry(row)
		dialog.keyEdit.setText(encoding.normalize_nfc(entry[0]))
		dialog.pwEdit1.setText(encoding.normalize_nfc(decrypted))
		dialog.pwEdit2.setText(encoding.normalize_nfc(decrypted))
		doc = QTextDocument(encoding.normalize_nfc(decryptedComments), parent=self)
		dialog.commentsEdit.setDocument(doc)

		if not dialog.exec_():
			return

		item = QTableWidgetItem(dialog.key())
		pwItem = QTableWidgetItem("*****")
		commentsItem = QTableWidgetItem("*****")
		self.passwordTable.setItem(row, self.KEY_IDX, item)
		self.passwordTable.setItem(row, self.PASSWORD_IDX, pwItem)
		self.passwordTable.setItem(row, self.COMMENTS_IDX, commentsItem)

		plainPw = dialog.pw1()
		plainComments = dialog.comments()
		if len(plainPw) + len(plainComments) > basics.MAX_SIZE_OF_PASSWDANDCOMMENTS:
			self.settings.mlogger.log("Password and/or comments too long. "
				"Combined they must not be larger than %d." %
				basics.MAX_SIZE_OF_PASSWDANDCOMMENTS,
				logging.CRITICAL, "User IO")
			return

		plainPwComments = ("%4d" % len(plainPw)) + plainPw + plainComments

		encPw = self.pwMap.encryptPassword(plainPwComments, self.selectedGroup)
		bkupPw = self.pwMap.backupKey.encryptPassword(plainPwComments)
		group.updateEntry(row, dialog.key(), encPw, bkupPw)

		self.cachePasswordComments(row, plainPw, plainComments)

		self.setModified(True)
		self.settings.mlogger.log("Password and comments entry "
			"for '%s', row '%d' was edited." % (dialog.key(), row),
			logging.DEBUG, "GUI IO")

	def copyPasswordFromSelection(self):
		"""
		Copy selected password to clipboard. Password is decrypted if
		necessary.
		"""
		indexes = self.passwordTable.selectedIndexes()
		if not indexes:
			return

		# there will be more indexes as the selection is on a row
		row = indexes[0].row()
		item = self.passwordTable.item(row, self.PASSWORD_IDX)
		self.copyPasswordFromItem(item)

	def copyPasswordFromItem(self, item):
		row = self.passwordTable.row(item)
		try:
			decryptedPassword = self.cachedOrDecryptPassword(row)
		except CallException:
			return

		self.clipboard.setText(decryptedPassword)
		# Do not log contents of clipboard, contains secrets!
		self.settings.mlogger.log("Copied text to clipboard.", logging.DEBUG,
			"Clipboard")
		if basics.CLIPBOARD_TIMEOUT_IN_SEC > 0:
			self.timer.start(basics.CLIPBOARD_TIMEOUT_IN_SEC*1000)  # cancels previous timer

	def copyCommentsFromItem(self, item):
		row = self.passwordTable.row(item)
		try:
			decryptedComments = self.cachedOrDecryptComments(row)
		except CallException:
			return

		self.clipboard.setText(decryptedComments)
		# Do not log contents of clipboard, contains secrets!
		self.settings.mlogger.log("Copied text to clipboard.", logging.DEBUG,
			"Clipboard")
		if basics.CLIPBOARD_TIMEOUT_IN_SEC > 0:
			self.timer.start(basics.CLIPBOARD_TIMEOUT_IN_SEC*1000)  # cancels previous timer

	def clearClipboard(self):
		self.clipboard.clear()
		self.timer.stop()  # cancels previous timer
		self.settings.mlogger.log("Clipboard cleared.", logging.DEBUG,
			"Clipboard")

	def loadPasswords(self, item):
		"""
		Slot that should load items for group that has been clicked on.
		"""
		self.passwordTable.clear()  # clears cahce, but also clears the header, the 3 titles
		headerKey = QTableWidgetItem(u"Key")
		headerValue = QTableWidgetItem(u"Password/Value")
		headerComments = QTableWidgetItem(u"Comments")
		self.passwordTable.setColumnCount(self.NO_OF_PASSWDTABLE_COLUMNS)
		self.passwordTable.setHorizontalHeaderItem(self.KEY_IDX, headerKey)
		self.passwordTable.setHorizontalHeaderItem(self.PASSWORD_IDX, headerValue)
		self.passwordTable.setHorizontalHeaderItem(self.COMMENTS_IDX, headerComments)

		groupName = encoding.normalize_nfc(item.text())
		self.selectedGroup = groupName
		group = self.pwMap.groups[groupName]
		self.passwordTable.setRowCount(len(group.entries))

		i = 0
		for key, encValue, bkupValue in group.entries:
			item = QTableWidgetItem(key)
			pwItem = QTableWidgetItem("*****")
			commentsItem = QTableWidgetItem("*****")
			self.passwordTable.setItem(i, self.KEY_IDX, item)
			self.passwordTable.setItem(i, self.PASSWORD_IDX, pwItem)
			self.passwordTable.setItem(i, self.COMMENTS_IDX, commentsItem)
			i = i+1

		self.passwordTable.resizeRowsToContents()
		self.passwordTable.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch)
		self.passwordTable.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeToContents)
		self.passwordTable.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeToContents)
		self.settings.mlogger.log("Loaded password group '%s'." % (groupName),
			logging.DEBUG, "GUI IO")

	def loadPasswordsBySelection(self):
		proxyIdx = self.groupsTree.currentIndex()
		itemIdx = self.groupsFilter.mapToSource(proxyIdx)
		selectedItem = self.groupsModel.itemFromIndex(itemIdx)

		if not selectedItem:
			return

		self.loadPasswords(selectedItem)

	def filterGroups(self, substring):
		"""
		Filter groupsTree view to have items containing given substring.
		"""
		self.groupsFilter.setFilterFixedString(substring)
		self.groupsTree.sortByColumn(0, Qt.AscendingOrder)

	def printAbout(self):
		"""
		Show window with about and version information.
		"""
		msgBox = QMessageBox(QMessageBox.Information, "About",
			"About <b>TrezorPass</b>: <br><br>TrezorPass is a safe " +
			"Password Manager application for people owning a Trezor who prefer to " +
			"keep their passwords local and not on the cloud. All passwords are " +
			"stored locally in a single file.<br><br>" +
			"<b>" + basics.NAME + " Version: </b>" + basics.VERSION_STR +
			" from " + basics.VERSION_DATE_STR +
			"<br><br><b>Python Version: </b>" + sys.version.replace(" \n", "; ") +
			"<br><br><b>Qt Version: </b>" + QT_VERSION_STR +
			"<br><br><b>PyQt Version: </b>" + PYQT_VERSION_STR, parent=self)
		msgBox.setIconPixmap(QPixmap("icons/TrezorPass.svg"))
		msgBox.exec_()

	def saveBackup(self):
		"""
		First it saves any pending changes to the pwdb database file. Then it uses an operating system call
		to copy the file appending a timestamp at the end of the file name.
		"""
		if self.modified:
			self.saveDatabase()
		backupFilename = self.settings.dbFilename + u"." + time.strftime('%Y%m%d%H%M%S')
		copyfile(self.settings.dbFilename, backupFilename)
		self.settings.mlogger.log("Backup of the encrypted database file has been created "
			"and placed into file \"%s\" (%d bytes)." % (backupFilename, os.path.getsize(backupFilename)),
			logging.INFO, "User IO")

	def importCsv(self):
		"""
		Read a properly formated CSV file from disk
		and add its contents to the current entries.

		Import format in CSV should be : group, key, password, comments

		There is no error checking, so be extra careful.

		Make a backup first.

		Entries from CSV will be *added* to existing pwdb. If this is not desired
		create an empty pwdb file first.

		GroupNames are unique, so if a groupname exists then
		key-password-comments tuples are added to the already existing group.
		If a group name does not exist, a new group is created and the
		key-password-comments tuples are added to the newly created group.

		Keys are not unique. So key-password-comments are always added.
		If a key with a given name existed before and the CSV file contains a
		key with the same name, then the key-password-comments is added and
		after the import the given group has 2 keys with the same name.
		Both keys exist then, the old from before the import, and the new one from the import.

		Examples of valid CSV file format: Some example lines
		First Bank account,login,myloginname,	# no comment
		[email protected],2-factor-authentication key,abcdef12345678,seed to regenerate 2FA codes	# with comment
		[email protected],recovery phrase,"passwd with 2 commas , ,",	# with comma
		[email protected],large multi-line comments,,"first line, some comma,
		second line"
		"""
		if self.modified:
			self.saveDatabase()
		copyfile(self.settings.dbFilename, self.settings.dbFilename + ".beforeCsvImport.backup")
		self.settings.mlogger.log("WARNING: You are about to import entries from a "
			"CSV file into your current password-database file. For safety "
			"reasons please make a backup copy now.\nFurthermore, this"
			"operation can be slow, so please be patient.", logging.NOTSET,
			"CSV import")
		dialog = QFileDialog(self, "Select CSV file to import",
			"", "CSV files (*.csv)")
		dialog.setAcceptMode(QFileDialog.AcceptOpen)

		res = dialog.exec_()
		if not res:
			return

		fname = encoding.normalize_nfc(dialog.selectedFiles()[0])
		listOfAddedGroupNames = self.pwMap.importCsv(fname)
		for groupNameNew in listOfAddedGroupNames:
			item = QStandardItem(groupNameNew)
			self.groupsModel.appendRow(item)
		self.groupsTree.sortByColumn(0, Qt.AscendingOrder)
		self.setModified(True)

	def exportCsv(self):
		"""
		Uses backup key encrypted by Trezor to decrypt all passwords
		at once and export them into a single paintext CSV file.

		Export format is CSV: group, key, password, comments
		"""
		self.settings.mlogger.log("WARNING: During backup/export all passwords will be "
			"written in plaintext to disk. If possible you should consider performing this "
			"operation on an offline or air-gapped computer. Be aware of the risks.",
			logging.NOTSET,	"CSV export")
		dialog = QFileDialog(self, "Select backup export file",
			"", "CSV files (*.csv)")
		dialog.setAcceptMode(QFileDialog.AcceptSave)

		res = dialog.exec_()
		if not res:
			return

		fname = encoding.normalize_nfc(dialog.selectedFiles()[0])
		self.pwMap.exportCsv(fname)

	def saveDatabase(self):
		"""
		Save main database file.
		"""
		self.pwMap.save(self.dbFilename)
		self.setModified(False)
		self.settings.mlogger.log("TrezorPass password database file was "
			"saved to '%s'." % (self.dbFilename), logging.DEBUG, "GUI IO")

	def closeEvent(self, event):
		if self.modified:
			msgBox = QMessageBox(text="Password database is modified. Save on exit?", parent=self)
			msgBox.setStandardButtons(QMessageBox.Yes |
				QMessageBox.No | QMessageBox.Cancel)
			reply = msgBox.exec_()

			if not reply or reply == QMessageBox.Cancel:
				event.ignore()
				return
			elif reply == QMessageBox.Yes:
				self.saveDatabase()

		event.accept()
Exemplo n.º 12
0
class SearchFileWidget(QWidget):

    language_filter_change = pyqtSignal(list)

    def __init__(self):
        QWidget.__init__(self)

        self._refreshing = False

        self.fileModel = None
        self.proxyFileModel = None
        self.videoModel = None

        self._state = None

        self.timeLastSearch = QTime.currentTime()

        self.ui = Ui_SearchFileWidget()
        self.setup_ui()
        QTimer.singleShot(0, self.on_event_loop_started)

    @pyqtSlot()
    def on_event_loop_started(self):
        lastDir = self._state.get_video_path()
        index = self.fileModel.index(str(lastDir))
        proxyIndex = self.proxyFileModel.mapFromSource(index)
        self.ui.folderView.scrollTo(proxyIndex)

    def set_state(self, state):
        self._state = state
        self._state.signals.interface_language_changed.connect(
            self.on_interface_language_changed)
        self._state.signals.login_status_changed.connect(
            self.on_login_state_changed)

    def setup_ui(self):
        self.ui.setupUi(self)

        self.ui.splitter.setSizes([600, 1000])
        self.ui.splitter.setChildrenCollapsible(False)

        # Set up folder view

        self.fileModel = QFileSystemModel(self)
        self.fileModel.setFilter(QDir.AllDirs | QDir.Dirs | QDir.Drives
                                 | QDir.NoDotAndDotDot | QDir.Readable
                                 | QDir.Executable | QDir.Writable)
        self.fileModel.iconProvider().setOptions(
            QFileIconProvider.DontUseCustomDirectoryIcons)
        self.fileModel.setRootPath(QDir.rootPath())
        self.fileModel.directoryLoaded.connect(self.onFileModelDirectoryLoaded)

        self.proxyFileModel = QSortFilterProxyModel(self)
        self.proxyFileModel.setSortRole(Qt.DisplayRole)
        self.proxyFileModel.setSourceModel(self.fileModel)
        self.proxyFileModel.sort(0, Qt.AscendingOrder)
        self.proxyFileModel.setSortCaseSensitivity(Qt.CaseInsensitive)
        self.ui.folderView.setModel(self.proxyFileModel)

        self.ui.folderView.setHeaderHidden(True)
        self.ui.folderView.hideColumn(3)
        self.ui.folderView.hideColumn(2)
        self.ui.folderView.hideColumn(1)

        self.ui.folderView.expanded.connect(self.onFolderViewExpanded)
        self.ui.folderView.clicked.connect(self.onFolderTreeClicked)
        self.ui.buttonFind.clicked.connect(self.onButtonFind)
        self.ui.buttonRefresh.clicked.connect(self.onButtonRefresh)

        # Setup and disable buttons
        self.ui.buttonFind.setEnabled(False)
        self.ui.buttonSearchSelectFolder.setEnabled(False)
        self.ui.buttonSearchSelectVideos.setEnabled(False)

        # Set up introduction
        self.showInstructions()

        # Set unknown  text here instead of `retranslate()` because widget translates itself
        self.ui.filterLanguageForVideo.set_unknown_text(_('All languages'))
        self.ui.filterLanguageForVideo.set_selected_language(
            UnknownLanguage.create_generic())
        self.ui.filterLanguageForVideo.selected_language_changed.connect(
            self.on_language_combobox_filter_change)

        # Set up video view
        self.videoModel = VideoModel(self)
        self.videoModel.connect_treeview(self.ui.videoView)
        self.ui.videoView.setHeaderHidden(True)
        self.ui.videoView.clicked.connect(self.onClickVideoTreeView)
        self.ui.videoView.selectionModel().currentChanged.connect(
            self.onSelectVideoTreeView)
        self.ui.videoView.customContextMenuRequested.connect(self.onContext)
        self.ui.videoView.setUniformRowHeights(True)
        self.videoModel.dataChanged.connect(self.subtitlesCheckedChanged)
        self.language_filter_change.connect(
            self.videoModel.on_filter_languages_change)

        self.ui.buttonSearchSelectVideos.clicked.connect(
            self.onButtonSearchSelectVideos)
        self.ui.buttonSearchSelectFolder.clicked.connect(
            self.onButtonSearchSelectFolder)
        self.ui.buttonDownload.clicked.connect(self.onButtonDownload)
        self.ui.buttonPlay.clicked.connect(self.onButtonPlay)
        self.ui.buttonIMDB.clicked.connect(self.onViewOnlineInfo)
        self.ui.videoView.setContextMenuPolicy(Qt.CustomContextMenu)

        self.ui.buttonPlay.setEnabled(False)

        # Drag and Drop files to the videoView enabled
        # FIXME: enable drag events for videoView (and instructions view)
        self.ui.videoView.__class__.dragEnterEvent = self.dragEnterEvent
        self.ui.videoView.__class__.dragMoveEvent = self.dragEnterEvent
        self.ui.videoView.__class__.dropEvent = self.dropEvent
        self.ui.videoView.setAcceptDrops(1)

        self.retranslate()

    def retranslate(self):
        introduction = '<p align="center"><h2>{title}</h2></p>' \
            '<p><b>{tab1header}</b><br/>{tab1content}</p>' \
            '<p><b>{tab2header}</b><br/>{tab2content}</p>'\
            '<p><b>{tab3header}</b><br/>{tab3content}</p>'.format(
                title=_('How To Use {title}').format(title=PROJECT_TITLE),
                tab1header=_('1st Tab:'),
                tab2header=_('2nd Tab:'),
                tab3header=_('3rd Tab:'),
                tab1content=_('Select, from the Folder Tree on the left, the folder which contains the videos '
                              'that need subtitles. {project} will then try to automatically find available '
                              'subtitles.').format(project=PROJECT_TITLE),
                tab2content=_('If you don\'t have the videos in your machine, you can search subtitles by '
                              'introducing the title/name of the video.').format(project=PROJECT_TITLE),
                tab3content=_('If you have found some subtitle somewhere else that is not in {project}\'s database, '
                              'please upload those subtitles so next users will be able to '
                              'find them more easily.').format(project=PROJECT_TITLE))
        self.ui.introductionHelp.setHtml(introduction)

    @pyqtSlot(Language)
    def on_interface_language_changed(self, language):
        self.ui.retranslateUi(self)
        self.retranslate()

    @pyqtSlot(str)
    def onFileModelDirectoryLoaded(self, path):
        lastDir = str(self._state.get_video_path())
        qDirLastDir = QDir(lastDir)
        qDirLastDir.cdUp()
        if qDirLastDir.path() == path:
            index = self.fileModel.index(lastDir)
            proxyIndex = self.proxyFileModel.mapFromSource(index)
            self.ui.folderView.scrollTo(proxyIndex)
            self.ui.folderView.setCurrentIndex(proxyIndex)

    @pyqtSlot()
    def on_login_state_changed(self):
        log.debug('on_login_state_changed()')
        nb_connected = self._state.providers.get_number_connected_providers()
        if nb_connected:
            self.ui.buttonSearchSelectFolder.setEnabled(True)
            self.ui.buttonSearchSelectVideos.setEnabled(True)
            self.ui.buttonFind.setEnabled(
                self.get_current_selected_folder() is not None)
        else:
            self.ui.buttonSearchSelectFolder.setEnabled(False)
            self.ui.buttonSearchSelectVideos.setEnabled(False)
            self.ui.buttonFind.setEnabled(False)

    @pyqtSlot(Language)
    def on_language_combobox_filter_change(self, language):
        if language.is_generic():
            self.language_filter_change.emit(
                self._state.get_download_languages())
        else:
            self.language_filter_change.emit([language])

    def on_permanent_language_filter_change(self, languages):
        selected_language = self.ui.filterLanguageForVideo.get_selected_language(
        )
        if selected_language.is_generic():
            self.language_filter_change.emit(languages)

    @pyqtSlot()
    def subtitlesCheckedChanged(self):
        subs = self.videoModel.get_checked_subtitles()
        if subs:
            self.ui.buttonDownload.setEnabled(True)
        else:
            self.ui.buttonDownload.setEnabled(False)

    def showInstructions(self):
        self.ui.stackedSearchResult.setCurrentWidget(self.ui.pageIntroduction)

    def hideInstructions(self):
        self.ui.stackedSearchResult.setCurrentWidget(self.ui.pageSearchResult)

    @pyqtSlot(QModelIndex)
    def onFolderTreeClicked(self, proxyIndex):
        """What to do when a Folder in the tree is clicked"""
        if not proxyIndex.isValid():
            return

        index = self.proxyFileModel.mapToSource(proxyIndex)
        folder_path = self.fileModel.filePath(index)
        self._state.set_video_paths([folder_path])

    def get_current_selected_folder(self):
        proxyIndex = self.ui.folderView.currentIndex()
        index = self.proxyFileModel.mapToSource(proxyIndex)
        folder_path = Path(self.fileModel.filePath(index))
        if not folder_path:
            return None
        return folder_path

    def get_current_selected_item_videomodel(self):
        current_index = self.ui.videoView.currentIndex()
        return self.videoModel.getSelectedItem(current_index)

    @pyqtSlot()
    def onButtonFind(self):
        now = QTime.currentTime()
        if now < self.timeLastSearch.addMSecs(500):
            return
        folder_path = self.get_current_selected_folder()

        self._state.set_video_paths([folder_path])
        self.search_videos([folder_path])

        self.timeLastSearch = QTime.currentTime()

    @pyqtSlot()
    def onButtonRefresh(self):
        currentPath = self.get_current_selected_folder()
        self._refreshing = True

        self.ui.folderView.collapseAll()

        currentPath = self.get_current_selected_folder()
        if not currentPath:
            self._state.set_video_paths([currentPath])

        index = self.fileModel.index(str(currentPath))

        self.ui.folderView.scrollTo(self.proxyFileModel.mapFromSource(index))

    @pyqtSlot(QModelIndex)
    def onFolderViewExpanded(self, proxyIndex):
        if self._refreshing:
            expandedPath = self.fileModel.filePath(
                self.proxyFileModel.mapToSource(proxyIndex))
            if expandedPath == QDir.rootPath():
                currentPath = self.get_current_selected_folder()
                if not currentPath:
                    currentPath = self._state.get_video_path()

                index = self.fileModel.index(str(currentPath))

                self.ui.folderView.scrollTo(
                    self.proxyFileModel.mapFromSource(index))
                self._refreshing = False

    @pyqtSlot()
    def onButtonSearchSelectFolder(self):
        paths = self._state.get_video_paths()
        path = paths[0] if paths else Path()
        selected_path = QFileDialog.getExistingDirectory(
            self, _('Select the directory that contains your videos'),
            str(path))
        if selected_path:
            selected_paths = [Path(selected_path)]
            self._state.set_video_paths(selected_paths)
            self.search_videos(selected_paths)

    @pyqtSlot()
    def onButtonSearchSelectVideos(self):
        paths = self._state.get_video_paths()
        path = paths[0] if paths else Path()
        selected_files, t = QFileDialog.getOpenFileNames(
            self, _('Select the video(s) that need subtitles'), str(path),
            get_select_videos())
        if selected_files:
            selected_files = list(Path(f) for f in selected_files)
            selected_dirs = list(set(p.parent for p in selected_files))
            self._state.set_video_paths(selected_dirs)
            self.search_videos(selected_files)

    def search_videos(self, paths):
        if not self._state.providers.get_number_connected_providers():
            QMessageBox.about(
                self, _('Error'),
                _('You are not connected to a server. Please connect first.'))
            return
        self.ui.buttonFind.setEnabled(False)
        self._search_videos_raw(paths)
        self.ui.buttonFind.setEnabled(True)

    def _search_videos_raw(self, paths):
        # FIXME: must pass mainwindow as argument to ProgressCallbackWidget
        callback = ProgressCallbackWidget(self)
        callback.set_title_text(_('Scanning...'))
        callback.set_label_text(_('Scanning files'))
        callback.set_finished_text(_('Scanning finished'))
        callback.set_block(True)
        callback.show()

        try:
            local_videos, local_subs = scan_videopaths(paths,
                                                       callback=callback,
                                                       recursive=True)
        except OSError:
            callback.cancel()
            QMessageBox.warning(self, _('Error'),
                                _('Some directories are not accessible.'))

        if callback.canceled():
            return

        callback.finish()

        log.debug('Videos found: {}'.format(local_videos))
        log.debug('Subtitles found: {}'.format(local_subs))

        self.hideInstructions()

        if not local_videos:
            QMessageBox.information(self, _('Scan Results'),
                                    _('No video has been found.'))
            return

        total = len(local_videos)

        # FIXME: must pass mainwindow as argument to ProgressCallbackWidget
        # callback = ProgressCallbackWidget(self)
        # callback.set_title_text(_('Asking Server...'))
        # callback.set_label_text(_('Searching subtitles...'))
        # callback.set_updated_text(_('Searching subtitles ( %d / %d )'))
        # callback.set_finished_text(_('Search finished'))
        callback.set_block(True)
        callback.set_range(0, total)

        callback.show()

        try:
            remote_subs = self._state.search_videos(local_videos, callback)
        except ProviderConnectionError:
            log.debug(
                'Unable to search for subtitles of videos: videos={}'.format(
                    list(v.get_filename() for v in local_videos)))
            QMessageBox.about(self, _('Error'),
                              _('Unable to search for subtitles'))
            callback.finish()
            return

        self.videoModel.set_videos(local_videos)

        if remote_subs is None:
            QMessageBox.about(
                self, _('Error'),
                _('Error contacting the server. Please try again later'))
        callback.finish()

        # TODO: CHECK if our local subtitles are already in the server, otherwise suggest to upload

    @pyqtSlot()
    def onButtonPlay(self):
        selected_item = self.get_current_selected_item_videomodel()
        log.debug('Trying to play selected item: {}'.format(selected_item))

        if selected_item is None:
            QMessageBox.warning(self, _('No subtitle selected'),
                                _('Select a subtitle and try again'))
            return

        if isinstance(selected_item, SubtitleFileNetwork):
            selected_item = selected_item.get_subtitles()[0]

        if isinstance(selected_item, VideoFile):
            subtitle_file_path = None
            video = selected_item
        elif isinstance(selected_item, LocalSubtitleFile):
            subtitle_file_path = selected_item.get_filepath()
            video = selected_item.get_super_parent(VideoFile)
        elif isinstance(selected_item, RemoteSubtitleFile):
            video = selected_item.get_super_parent(VideoFile)
            subtitle_file_path = Path(
                tempfile.gettempdir()) / 'subdownloader.tmp.srt'
            log.debug('tmp path is {}'.format(subtitle_file_path))
            log.debug(
                'Temporary subtitle will be downloaded into: {temp_path}'.
                format(temp_path=subtitle_file_path))
            # FIXME: must pass mainwindow as argument to ProgressCallbackWidget
            callback = ProgressCallbackWidget(self)
            callback.set_title_text(_('Playing video + sub'))
            callback.set_label_text(_('Downloading files...'))
            callback.set_finished_text(_('Downloading finished'))
            callback.set_block(True)
            callback.set_range(0, 100)
            callback.show()

            try:
                selected_item.download(
                    subtitle_file_path,
                    self._state.providers.get(selected_item.get_provider),
                    callback)
            except ProviderConnectionError:
                log.debug('Unable to download subtitle "{}"'.format(
                    selected_item.get_filename()),
                          exc_info=sys.exc_info())
                QMessageBox.about(
                    self, _('Error'),
                    _('Unable to download subtitle "{subtitle}"').format(
                        subtitle=selected_item.get_filename()))
                callback.finish()
                return
            callback.finish()
        else:
            QMessageBox.about(
                self, _('Error'),
                '{}\n{}'.format(_('Unknown Error'),
                                _('Please submit bug report')))
            return

        # video = selected_item.get_parent().get_parent().get_parent()
        # FIXME: download subtitle with provider + use returned localSubtitleFile instead of creating one here
        if subtitle_file_path:
            local_subtitle = LocalSubtitleFile(subtitle_file_path)
        else:
            local_subtitle = None
        try:
            player = self._state.get_videoplayer()
            player.play_video(video, local_subtitle)
        except RuntimeError as e:
            QMessageBox.about(self, _('Error'), e.args[0])

    @pyqtSlot(QModelIndex)
    def onClickVideoTreeView(self, index):
        data_item = self.videoModel.getSelectedItem(index)

        if isinstance(data_item, VideoFile):
            video = data_item
            if True:  # video.getMovieInfo():
                self.ui.buttonIMDB.setEnabled(True)
                self.ui.buttonIMDB.setIcon(QIcon(':/images/info.png'))
                self.ui.buttonIMDB.setText(_('Movie Info'))
        elif isinstance(data_item, RemoteSubtitleFile):
            self.ui.buttonIMDB.setEnabled(True)
            self.ui.buttonIMDB.setIcon(
                QIcon(':/images/sites/opensubtitles.png'))
            self.ui.buttonIMDB.setText(_('Subtitle Info'))
        else:
            self.ui.buttonIMDB.setEnabled(False)

    @pyqtSlot(QModelIndex)
    def onSelectVideoTreeView(self, index):
        data_item = self.videoModel.getSelectedItem(index)

        self.ui.buttonPlay.setEnabled(True)
        # if isinstance(data_item, SubtitleFile):
        #     self.ui.buttonPlay.setEnabled(True)
        # else:
        #     self.ui.buttonPlay.setEnabled(False)

    def onContext(self, point):
        # FIXME: code duplication with Main.onContext and/or SearchNameWidget and/or SearchFileWidget
        menu = QMenu('Menu', self)

        listview = self.ui.videoView

        index = listview.currentIndex()
        data_item = listview.model().getSelectedItem(index)
        if data_item is not None:
            if isinstance(data_item, VideoFile):
                video = data_item
                video_identities = video.get_identities()
                if any(video_identities.iter_imdb_identity()):
                    online_action = QAction(QIcon(":/images/info.png"),
                                            _("View IMDb info"), self)
                    online_action.triggered.connect(self.onViewOnlineInfo)
                else:
                    online_action = QAction(QIcon(":/images/info.png"),
                                            _("Set IMDb info..."), self)
                    online_action.triggered.connect(self.on_set_imdb_info)
                menu.addAction(online_action)
            elif isinstance(data_item, SubtitleFile):
                play_action = QAction(QIcon(":/images/play.png"),
                                      _("Play video + subtitle"), self)
                play_action.triggered.connect(self.onButtonPlay)
                menu.addAction(play_action)

                if isinstance(data_item, RemoteSubtitleFile):
                    download_action = QAction(QIcon(":/images/download.png"),
                                              _("Download"), self)
                    download_action.triggered.connect(self.onButtonDownload)
                    menu.addAction(download_action)

                    online_action = QAction(
                        QIcon(":/images/sites/opensubtitles.png"),
                        _("View online info"), self)
                    online_action.triggered.connect(self.onViewOnlineInfo)
                    menu.addAction(online_action)

        # Show the context menu.
        menu.exec_(listview.mapToGlobal(point))

    def _create_choose_target_subtitle_path_cb(self):
        def callback(path, filename):
            selected_path = QFileDialog.getSaveFileName(
                self, _('Choose the target filename'), str(path / filename))
            return selected_path

        return callback

    def onButtonDownload(self):
        # We download the subtitle in the same folder than the video
        rsubs = self.videoModel.get_checked_subtitles()

        sub_downloader = SubtitleDownloadProcess(parent=self.parent(),
                                                 rsubtitles=rsubs,
                                                 state=self._state,
                                                 parent_add=True)
        sub_downloader.download_all()
        new_subs = sub_downloader.downloaded_subtitles()
        self.videoModel.uncheck_subtitles(new_subs)

    def onViewOnlineInfo(self):
        # FIXME: code duplication with Main.onContext and/or SearchNameWidget and/or SearchFileWidget
        # Tab for SearchByHash TODO:replace this 0 by an ENUM value
        listview = self.ui.videoView
        index = listview.currentIndex()
        data_item = self.videoModel.getSelectedItem(index)

        if isinstance(data_item, VideoFile):
            video = data_item
            video_identities = video.get_identities()
            if any(video_identities.iter_imdb_identity()):
                imdb_identity = next(video_identities.iter_imdb_identity())
                webbrowser.open(imdb_identity.get_imdb_url(),
                                new=2,
                                autoraise=1)
            else:
                QMessageBox.information(self.parent(), _('imdb unknown'),
                                        _('imdb is unknown'))

        elif isinstance(data_item, RemoteSubtitleFile):
            sub = data_item
            webbrowser.open(sub.get_link(), new=2, autoraise=1)

    @pyqtSlot()
    def on_set_imdb_info(self):
        # FIXME: DUPLICATED WITH SEARCHNAMEWIDGET
        QMessageBox.about(self, _("Info"), "Not implemented yet. Sorry...")
Exemplo n.º 13
0
class MainWindow(QMainWindow):
    """
    The main window class
    """
    def __init__(self, parent=None):
        QMainWindow.__init__(self, parent)
        self.ui = uic.loadUi("gui/main_window.ui")

        self.timestamp_filename = None
        self.video_filename = None
        self.media_start_time = None
        self.media_end_time = None
        self.restart_needed = False
        self.timer_period = 100
        self.is_full_screen = False
        self.media_started_playing = False
        self.media_is_playing = False
        self.original_geometry = None
        self.mute = False

        self.timestamp_model = TimestampModel(None, self)
        self.proxy_model = QSortFilterProxyModel(self)
        self.ui.list_timestamp.setModel(self.timestamp_model)
        self.ui.list_timestamp.doubleClicked.connect(
            lambda event: self.ui.list_timestamp.indexAt(event.pos()).isValid()
            and self.run()
        )

        self.timer = QTimer()
        self.timer.timeout.connect(self.update_ui)
        self.timer.timeout.connect(self.timer_handler)
        self.timer.start(self.timer_period)

        self.vlc_instance = vlc.Instance()
        self.media_player = self.vlc_instance.media_player_new()
        # if sys.platform == "darwin":  # for MacOS
        #     self.ui.frame_video = QMacCocoaViewContainer(0)

        self.ui.frame_video.doubleClicked.connect(self.toggle_full_screen)
        self.ui.frame_video.wheel.connect(self.wheel_handler)
        self.ui.frame_video.keyPressed.connect(self.key_handler)

        # Set up buttons
        self.ui.button_run.clicked.connect(self.run)
        self.ui.button_timestamp_browse.clicked.connect(
            self.browse_timestamp_handler
        )
        self.ui.button_video_browse.clicked.connect(
            self.browse_video_handler
        )

        self.play_pause_model = ToggleButtonModel(None, self)
        self.play_pause_model.setStateMap(
            {
                True: {
                    "text": "",
                    "icon": qta.icon("fa.play", scale_factor=0.7)
                },
                False: {
                    "text": "",
                    "icon": qta.icon("fa.pause", scale_factor=0.7)
                }
            }
        )
        self.ui.button_play_pause.setModel(self.play_pause_model)
        self.ui.button_play_pause.clicked.connect(self.play_pause)

        self.mute_model = ToggleButtonModel(None, self)
        self.mute_model.setStateMap(
            {
                True: {
                    "text": "",
                    "icon": qta.icon("fa.volume-up", scale_factor=0.8)
                },
                False: {
                    "text": "",
                    "icon": qta.icon("fa.volume-off", scale_factor=0.8)
                }
            }
        )
        self.ui.button_mute_toggle.setModel(self.mute_model)
        self.ui.button_mute_toggle.clicked.connect(self.toggle_mute)

        self.ui.button_full_screen.setIcon(
            qta.icon("ei.fullscreen", scale_factor=0.6)
        )
        self.ui.button_full_screen.setText("")
        self.ui.button_full_screen.clicked.connect(self.toggle_full_screen)
        self.ui.button_speed_up.clicked.connect(self.speed_up_handler)
        self.ui.button_speed_up.setIcon(
            qta.icon("fa.arrow-circle-o-up", scale_factor=0.8)
        )
        self.ui.button_speed_up.setText("")
        self.ui.button_slow_down.clicked.connect(self.slow_down_handler)
        self.ui.button_slow_down.setIcon(
            qta.icon("fa.arrow-circle-o-down", scale_factor=0.8)
        )
        self.ui.button_slow_down.setText("")
        self.ui.button_mark_start.setIcon(
            qta.icon("fa.quote-left", scale_factor=0.7)
        )
        self.ui.button_mark_start.setText("")
        self.ui.button_mark_end.setIcon(
            qta.icon("fa.quote-right", scale_factor=0.7)
        )
        self.ui.button_mark_end.setText("")
        self.ui.button_add_entry.clicked.connect(self.add_entry)
        self.ui.button_remove_entry.clicked.connect(self.remove_entry)

        self.ui.button_mark_start.clicked.connect(
            lambda: self.set_mark(start_time=int(
                self.media_player.get_position() *
                self.media_player.get_media().get_duration()))
        )
        self.ui.button_mark_end.clicked.connect(
            lambda: self.set_mark(end_time=int(
                self.media_player.get_position() *
                self.media_player.get_media().get_duration()))
        )

        self.ui.slider_progress.setTracking(False)
        self.ui.slider_progress.valueChanged.connect(self.set_media_position)
        self.ui.slider_volume.valueChanged.connect(self.set_volume)
        self.ui.entry_description.setReadOnly(True)

        # Mapper between the table and the entry detail
        self.mapper = QDataWidgetMapper()
        self.mapper.setSubmitPolicy(QDataWidgetMapper.ManualSubmit)
        self.ui.button_save.clicked.connect(self.mapper.submit)

        # Set up default volume
        self.set_volume(self.ui.slider_volume.value())

        self.vlc_events = self.media_player.event_manager()
        self.vlc_events.event_attach(
            vlc.EventType.MediaPlayerTimeChanged, self.media_time_change_handler
        )

        # Let our application handle mouse and key input instead of VLC
        self.media_player.video_set_mouse_input(False)
        self.media_player.video_set_key_input(False)

        self.ui.show()


    def add_entry(self):
        if not self.timestamp_filename:
            self._show_error("You haven't chosen a timestamp file yet")
        row_num = self.timestamp_model.rowCount()
        self.timestamp_model.insertRow(row_num)
        start_cell = self.timestamp_model.index(row_num, 0)
        end_cell = self.timestamp_model.index(row_num, 1)
        self.timestamp_model.setData(start_cell, TimestampDelta.from_string(""))
        self.timestamp_model.setData(end_cell, TimestampDelta.from_string(""))

    def remove_entry(self):
        if not self.timestamp_filename:
            self._show_error("You haven't chosen a timestamp file yet")
        selected = self.ui.list_timestamp.selectionModel().selectedIndexes()
        if len(selected) == 0:
            return
        self.proxy_model.removeRow(selected[0].row()) and self.mapper.submit()


    def set_media_position(self, position):
        percentage = position / 10000.0
        self.media_player.set_position(percentage)
        absolute_position = percentage * \
            self.media_player.get_media().get_duration()
        if absolute_position > self.media_end_time:
            self.media_end_time = -1

    def set_mark(self, start_time=None, end_time=None):
        if len(self.ui.list_timestamp.selectedIndexes()) == 0:
            blankRowIndex = self.timestamp_model.blankRowIndex()
            if not blankRowIndex.isValid():
                self.add_entry()
            else:
                index = self.proxy_model.mapFromSource(blankRowIndex)
                self.ui.list_timestamp.selectRow(index.row())
        selectedIndexes = self.ui.list_timestamp.selectedIndexes()
        if start_time:
            self.proxy_model.setData(selectedIndexes[0],
                                     TimestampDelta.string_from_int(
                                         start_time))
        if end_time:
            self.proxy_model.setData(selectedIndexes[1],
                                     TimestampDelta.string_from_int(
                                         end_time))

    def update_ui(self):
        self.ui.slider_progress.blockSignals(True)
        self.ui.slider_progress.setValue(
            self.media_player.get_position() * 10000
        )
        # When the video finishes
        self.ui.slider_progress.blockSignals(False)
        if self.media_started_playing and \
           self.media_player.get_media().get_state() == vlc.State.Ended:
            self.play_pause_model.setState(True)
            # Apparently we need to reset the media, otherwise the player
            # won't play at all
            self.media_player.set_media(self.media_player.get_media())
            self.set_volume(self.ui.slider_volume.value())
            self.media_is_playing = False
            self.media_started_playing = False
            self.run()

    def timer_handler(self):
        """
        This is a workaround, because for some reason we can't call set_time()
        inside the MediaPlayerTimeChanged handler (as the video just stops
        playing)
        """
        if self.restart_needed:
            self.media_player.set_time(self.media_start_time)
            self.restart_needed = False

    def key_handler(self, event):
        if event.key() == Qt.Key_Escape and self.is_full_screen:
            self.toggle_full_screen()
        if event.key() == Qt.Key_F:
            self.toggle_full_screen()
        if event.key() == Qt.Key_Space:
            self.play_pause()

    def wheel_handler(self, event):
        self.modify_volume(1 if event.angleDelta().y() > 0 else -1)

    def toggle_mute(self):
        self.media_player.audio_set_mute(not self.media_player.audio_get_mute())
        self.mute = not self.mute
        self.mute_model.setState(not self.mute)

    def modify_volume(self, delta_percent):
        new_volume = self.media_player.audio_get_volume() + delta_percent
        if new_volume < 0:
            new_volume = 0
        elif new_volume > 40:
            new_volume = 40
        self.media_player.audio_set_volume(new_volume)
        self.ui.slider_volume.setValue(self.media_player.audio_get_volume())

    def set_volume(self, new_volume):
        self.media_player.audio_set_volume(new_volume)

    def speed_up_handler(self):
        self.modify_rate(0.1)

    def slow_down_handler(self):
        self.modify_rate(-0.1)

    def modify_rate(self, delta_percent):
        new_rate = self.media_player.get_rate() + delta_percent
        if new_rate < 0.2 or new_rate > 2.0:
            return
        self.media_player.set_rate(new_rate)

    def media_time_change_handler(self, _):
        if self.media_end_time == -1:
            return
        if self.media_player.get_time() > self.media_end_time:
            self.restart_needed = True

    def update_slider_highlight(self):
        if self.ui.list_timestamp.selectionModel().hasSelection():
            selected_row = self.ui.list_timestamp.selectionModel(). \
                selectedRows()[0]
            self.media_start_time = self.ui.list_timestamp.model().data(
                selected_row.model().index(selected_row.row(), 0),
                Qt.UserRole
            )
            self.media_end_time = self.ui.list_timestamp.model().data(
                selected_row.model().index(selected_row.row(), 1),
                Qt.UserRole
            )
            duration = self.media_player.get_media().get_duration()
            self.media_end_time = self.media_end_time \
                if self.media_end_time != 0 else duration
            if self.media_start_time > self.media_end_time:
                raise ValueError("Start time cannot be later than end time")
            if self.media_start_time > duration:
                raise ValueError("Start time not within video duration")
            if self.media_end_time > duration:
                raise ValueError("End time not within video duration")
            slider_start_pos = (self.media_start_time / duration) * \
                               (self.ui.slider_progress.maximum() -
                                self.ui.slider_progress.minimum())
            slider_end_pos = (self.media_end_time / duration) * \
                             (self.ui.slider_progress.maximum() -
                              self.ui.slider_progress.minimum())
            self.ui.slider_progress.setHighlight(
                int(slider_start_pos), int(slider_end_pos)
            )

        else:
            self.media_start_time = 0
            self.media_end_time = -1


    def run(self):
        """
        Execute the loop
        """
        if self.timestamp_filename is None:
            self._show_error("No timestamp file chosen")
            return
        if self.video_filename is None:
            self._show_error("No video file chosen")
            return
        try:
            self.update_slider_highlight()
            self.media_player.play()
            self.media_player.set_time(self.media_start_time)
            self.media_started_playing = True
            self.media_is_playing = True
            self.play_pause_model.setState(False)
        except Exception as ex:
            self._show_error(str(ex))
            print(traceback.format_exc())

    def play_pause(self):
        """Toggle play/pause status
        """
        if not self.media_started_playing:
            self.run()
            return
        if self.media_is_playing:
            self.media_player.pause()
        else:
            self.media_player.play()
        self.media_is_playing = not self.media_is_playing
        self.play_pause_model.setState(not self.media_is_playing)

    def toggle_full_screen(self):
        if self.is_full_screen:
            # TODO Artifacts still happen some time when exiting full screen
            # in X11
            self.ui.frame_media.showNormal()
            self.ui.frame_media.restoreGeometry(self.original_geometry)
            self.ui.frame_media.setParent(self.ui.widget_central)
            self.ui.layout_main.addWidget(self.ui.frame_media, 2, 3, 3, 1)
            # self.ui.frame_media.ensurePolished()
        else:
            self.ui.frame_media.setParent(None)
            self.ui.frame_media.setWindowFlags(Qt.FramelessWindowHint |
                                               Qt.CustomizeWindowHint)
            self.original_geometry = self.ui.frame_media.saveGeometry()
            desktop = QApplication.desktop()
            rect = desktop.screenGeometry(desktop.screenNumber(QCursor.pos()))
            self.ui.frame_media.setGeometry(rect)
            self.ui.frame_media.showFullScreen()
            self.ui.frame_media.show()
        self.ui.frame_video.setFocus()
        self.is_full_screen = not self.is_full_screen

    def browse_timestamp_handler(self):
        """
        Handler when the timestamp browser button is clicked
        """
        tmp_name, _ = QFileDialog.getOpenFileName(
            self, "Choose Timestamp file", None,
            "Timestamp File (*.tmsp);;All Files (*)"
        )
        if not tmp_name:
            return
        self.set_timestamp_filename(QDir.toNativeSeparators(tmp_name))

    def _sort_model(self):
        self.ui.list_timestamp.sortByColumn(0, Qt.AscendingOrder)

    def _select_blank_row(self, parent, start, end):
        self.ui.list_timestamp.selectRow(start)

    def set_timestamp_filename(self, filename):
        """
        Set the timestamp file name
        """
        if not os.path.isfile(filename):
            self._show_error("Cannot access timestamp file " + filename)
            return

        try:
            self.timestamp_model = TimestampModel(filename, self)
            self.timestamp_model.timeParseError.connect(
                lambda err: self._show_error(err)
            )
            self.proxy_model.setSortRole(Qt.UserRole)
            self.proxy_model.dataChanged.connect(self._sort_model)
            self.proxy_model.dataChanged.connect(self.update_slider_highlight)
            self.proxy_model.setSourceModel(self.timestamp_model)
            self.proxy_model.rowsInserted.connect(self._sort_model)
            self.proxy_model.rowsInserted.connect(self._select_blank_row)
            self.ui.list_timestamp.setModel(self.proxy_model)

            self.timestamp_filename = filename
            self.ui.entry_timestamp.setText(self.timestamp_filename)

            self.mapper.setModel(self.proxy_model)
            self.mapper.addMapping(self.ui.entry_start_time, 0)
            self.mapper.addMapping(self.ui.entry_end_time, 1)
            self.mapper.addMapping(self.ui.entry_description, 2)
            self.ui.list_timestamp.selectionModel().selectionChanged.connect(
                self.timestamp_selection_changed)
            self._sort_model()

            directory = os.path.dirname(self.timestamp_filename)
            basename = os.path.basename(self.timestamp_filename)
            timestamp_name_without_ext = os.path.splitext(basename)[0]
            for file_in_dir in os.listdir(directory):
                current_filename = os.path.splitext(file_in_dir)[0]
                found_video = (current_filename == timestamp_name_without_ext
                               and file_in_dir != basename)
                if found_video:
                    found_video_file = os.path.join(directory, file_in_dir)
                    self.set_video_filename(found_video_file)
                    break
        except ValueError as err:
            self._show_error("Timestamp file is invalid")

    def timestamp_selection_changed(self, selected, deselected):
        if len(selected) > 0:
            self.mapper.setCurrentModelIndex(selected.indexes()[0])
            self.ui.button_save.setEnabled(True)
            self.ui.button_remove_entry.setEnabled(True)
            self.ui.entry_start_time.setReadOnly(False)
            self.ui.entry_end_time.setReadOnly(False)
            self.ui.entry_description.setReadOnly(False)
        else:
            self.mapper.setCurrentModelIndex(QModelIndex())
            self.ui.button_save.setEnabled(False)
            self.ui.button_remove_entry.setEnabled(False)
            self.ui.entry_start_time.clear()
            self.ui.entry_end_time.clear()
            self.ui.entry_description.clear()
            self.ui.entry_start_time.setReadOnly(True)
            self.ui.entry_end_time.setReadOnly(True)
            self.ui.entry_description.setReadOnly(True)

    def set_video_filename(self, filename):
        """
        Set the video filename
        """
        if not os.path.isfile(filename):
            self._show_error("Cannot access video file " + filename)
            return

        self.video_filename = filename

        media = self.vlc_instance.media_new(self.video_filename)
        media.parse()
        if not media.get_duration():
            self._show_error("Cannot play this media file")
            self.media_player.set_media(None)
            self.video_filename = None
        else:
            self.media_player.set_media(media)
            if sys.platform.startswith('linux'): # for Linux using the X Server
                self.media_player.set_xwindow(self.ui.frame_video.winId())
            elif sys.platform == "win32": # for Windows
                self.media_player.set_hwnd(self.ui.frame_video.winId())
            elif sys.platform == "darwin": # for MacOS
                self.media_player.set_nsobject(self.ui.frame_video.winId())
            self.ui.entry_video.setText(self.video_filename)
            self.media_started_playing = False
            self.media_is_playing = False
            self.set_volume(self.ui.slider_volume.value())
            self.play_pause_model.setState(True)

    def browse_video_handler(self):
        """
        Handler when the video browse button is clicked
        """
        tmp_name, _ = QFileDialog.getOpenFileName(
            self, "Choose Video file", None,
            "All Files (*)"
        )
        if not tmp_name:
            return
        self.set_video_filename(QDir.toNativeSeparators(tmp_name))

    def _show_error(self, message, title="Error"):
        QMessageBox.warning(self, title, message)
Exemplo n.º 14
0
class TileStampsDock(QDockWidget):
    setStamp = pyqtSignal(TileStamp)
    
    def __init__(self, stampManager, parent = None):
        super().__init__(parent)
        
        self.mTileStampManager = stampManager
        self.mTileStampModel = stampManager.tileStampModel()
        self.mProxyModel = QSortFilterProxyModel(self.mTileStampModel)
        self.mFilterEdit = QLineEdit(self)
        self.mNewStamp = QAction(self)
        self.mAddVariation = QAction(self)
        self.mDuplicate = QAction(self)
        self.mDelete = QAction(self)
        self.mChooseFolder = QAction(self)

        self.setObjectName("TileStampsDock")
        self.mProxyModel.setSortLocaleAware(True)
        self.mProxyModel.setSortCaseSensitivity(Qt.CaseInsensitive)
        self.mProxyModel.setFilterCaseSensitivity(Qt.CaseInsensitive)
        self.mProxyModel.setSourceModel(self.mTileStampModel)
        self.mProxyModel.sort(0)
        self.mTileStampView = TileStampView(self)
        self.mTileStampView.setModel(self.mProxyModel)
        self.mTileStampView.setVerticalScrollMode(QAbstractItemView.ScrollPerPixel)
        self.mTileStampView.header().setStretchLastSection(False)
        self.mTileStampView.header().setSectionResizeMode(0, QHeaderView.Stretch)
        self.mTileStampView.header().setSectionResizeMode(1, QHeaderView.ResizeToContents)
        self.mTileStampView.setContextMenuPolicy(Qt.CustomContextMenu)
        self.mTileStampView.customContextMenuRequested.connect(self.showContextMenu)
        self.mNewStamp.setIcon(QIcon(":images/16x16/document-new.png"))
        self.mAddVariation.setIcon(QIcon(":/images/16x16/add.png"))
        self.mDuplicate.setIcon(QIcon(":/images/16x16/stock-duplicate-16.png"))
        self.mDelete.setIcon(QIcon(":images/16x16/edit-delete.png"))
        self.mChooseFolder.setIcon(QIcon(":images/16x16/document-open.png"))
        Utils.setThemeIcon(self.mNewStamp, "document-new")
        Utils.setThemeIcon(self.mAddVariation, "add")
        Utils.setThemeIcon(self.mDelete, "edit-delete")
        Utils.setThemeIcon(self.mChooseFolder, "document-open")

        self.mFilterEdit.setClearButtonEnabled(True)
        self.mFilterEdit.textChanged.connect(self.mProxyModel.setFilterFixedString)
        self.mTileStampModel.stampRenamed.connect(self.ensureStampVisible)
        self.mNewStamp.triggered.connect(self.newStamp)
        self.mAddVariation.triggered.connect(self.addVariation)
        self.mDuplicate.triggered.connect(self.duplicate)
        self.mDelete.triggered.connect(self.delete_)
        self.mChooseFolder.triggered.connect(self.chooseFolder)
        self.mDuplicate.setEnabled(False)
        self.mDelete.setEnabled(False)
        self.mAddVariation.setEnabled(False)
        widget = QWidget(self)
        layout = QVBoxLayout(widget)
        layout.setContentsMargins(5, 5, 5, 5)
        
        buttonContainer = QToolBar()
        buttonContainer.setFloatable(False)
        buttonContainer.setMovable(False)
        buttonContainer.setIconSize(QSize(16, 16))
        buttonContainer.addAction(self.mNewStamp)
        buttonContainer.addAction(self.mAddVariation)
        buttonContainer.addAction(self.mDuplicate)
        buttonContainer.addAction(self.mDelete)
        stretch = QWidget()
        stretch.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)
        buttonContainer.addWidget(stretch)
        buttonContainer.addAction(self.mChooseFolder)
        listAndToolBar = QVBoxLayout()
        listAndToolBar.setSpacing(0)
        listAndToolBar.addWidget(self.mFilterEdit)
        listAndToolBar.addWidget(self.mTileStampView)
        listAndToolBar.addWidget(buttonContainer)
        layout.addLayout(listAndToolBar)
        selectionModel = self.mTileStampView.selectionModel()
        selectionModel.currentRowChanged.connect(self.currentRowChanged)
        self.setWidget(widget)
        self.retranslateUi()
        
    def changeEvent(self, e):
        super().changeEvent(e)
        x = e.type()
        if x==QEvent.LanguageChange:
            self.retranslateUi()
        else:
            pass
            
    def keyPressEvent(self, event):
        x = event.key()
        if x==Qt.Key_Delete or x==Qt.Key_Backspace:
            self.delete_()
            return

        super().keyPressEvent(event)

    def currentRowChanged(self, index):
        sourceIndex = self.mProxyModel.mapToSource(index)
        isStamp = self.mTileStampModel.isStamp(sourceIndex)
        self.mDuplicate.setEnabled(isStamp)
        self.mDelete.setEnabled(sourceIndex.isValid())
        self.mAddVariation.setEnabled(isStamp)
        if (isStamp):
            self.setStamp.emit(self.mTileStampModel.stampAt(sourceIndex))
        else:
            variation = self.mTileStampModel.variationAt(sourceIndex)
            if variation:
                # single variation clicked, use it specifically
                self.setStamp.emit(TileStamp(Map(variation.map)))

    def showContextMenu(self, pos):
        index = self.mTileStampView.indexAt(pos)
        if (not index.isValid()):
            return
        menu = QMenu()
        sourceIndex = self.mProxyModel.mapToSource(index)
        if (self.mTileStampModel.isStamp(sourceIndex)):
            addStampVariation = QAction(self.mAddVariation.icon(), self.mAddVariation.text(), menu)
            deleteStamp = QAction(self.mDelete.icon(), self.tr("Delete Stamp"), menu)
            deleteStamp.triggered.connect(self.delete_)
            addStampVariation.triggered.connect(self.addVariation)
            menu.addAction(addStampVariation)
            menu.addSeparator()
            menu.addAction(deleteStamp)
        else :
            removeVariation = QAction(QIcon(":/images/16x16/remove.png"),
                                                   self.tr("Remove Variation"),
                                                   menu)
            Utils.setThemeIcon(removeVariation, "remove")
            removeVariation.triggered.connect(self.delete_)
            menu.addAction(removeVariation)
        
        menu.exec(self.mTileStampView.viewport().mapToGlobal(pos))
    
    def newStamp(self):
        stamp = self.mTileStampManager.createStamp()
        if (self.isVisible() and not stamp.isEmpty()):
            stampIndex = self.mTileStampModel.index(stamp)
            if (stampIndex.isValid()):
                viewIndex = self.mProxyModel.mapFromSource(stampIndex)
                self.mTileStampView.setCurrentIndex(viewIndex)
                self.mTileStampView.edit(viewIndex)

    
    def delete_(self):
        index = self.mTileStampView.currentIndex()
        if (not index.isValid()):
            return
        sourceIndex = self.mProxyModel.mapToSource(index)
        self.mTileStampModel.removeRow(sourceIndex.row(), sourceIndex.parent())
    
    def duplicate(self):
        index = self.mTileStampView.currentIndex()
        if (not index.isValid()):
            return
        sourceIndex = self.mProxyModel.mapToSource(index)
        if (not self.mTileStampModel.isStamp(sourceIndex)):
            return
        stamp = self.mTileStampModel.stampAt = TileStamp(sourceIndex)
        self.mTileStampModel.addStamp(stamp.clone())

    def addVariation(self):
        index = self.mTileStampView.currentIndex()
        if (not index.isValid()):
            return
        sourceIndex = self.mProxyModel.mapToSource(index)
        if (not self.mTileStampModel.isStamp(sourceIndex)):
            return
        stamp = self.mTileStampModel.stampAt(sourceIndex)
        self.mTileStampManager.addVariation(stamp)
    
    def chooseFolder(self):
        prefs = Preferences.instance()
        stampsDirectory = prefs.stampsDirectory()
        stampsDirectory = QFileDialog.getExistingDirectory(self.window(),
                                                            self.tr("Choose the Stamps Folder"),
                                                            stampsDirectory)
        if (not stampsDirectory.isEmpty()):
            prefs.setStampsDirectory(stampsDirectory)
    
    def ensureStampVisible(self, stamp):
        stampIndex = self.mTileStampModel.index(stamp)
        if (stampIndex.isValid()):
            self.mTileStampView.scrollTo(self.mProxyModel.mapFromSource(stampIndex))

    def retranslateUi(self):
        self.setWindowTitle(self.tr("Tile Stamps"))
        self.mNewStamp.setText(self.tr("Add New Stamp"))
        self.mAddVariation.setText(self.tr("Add Variation"))
        self.mDuplicate.setText(self.tr("Duplicate Stamp"))
        self.mDelete.setText(self.tr("Delete Selected"))
        self.mChooseFolder.setText(self.tr("Set Stamps Folder"))
        self.mFilterEdit.setPlaceholderText(self.tr("Filter"))
Exemplo n.º 15
0
class MainWindow(QMainWindow):
    """
    The main window class
    """
    def __init__(self, parent=None):
        QMainWindow.__init__(self, parent)
        self.ui = uic.loadUi("gui/main_window.ui")

        self.timestamp_filename = None
        self.video_filename = None
        self.media_start_time = None
        self.media_end_time = None
        self.restart_needed = False
        self.timer_period = 100
        self.is_full_screen = False
        self.media_started_playing = False
        self.media_is_playing = False
        self.original_geometry = None
        self.mute = False

        self.timestamp_model = TimestampModel(None, self)
        self.proxy_model = QSortFilterProxyModel(self)
        self.ui.list_timestamp.setModel(self.timestamp_model)
        self.ui.list_timestamp.doubleClicked.connect(
            lambda event: self.ui.list_timestamp.indexAt(event.pos()).isValid()
            and self.run()
        )

        self.timer = QTimer()
        self.timer.timeout.connect(self.update_ui)
        self.timer.timeout.connect(self.timer_handler)
        self.timer.start(self.timer_period)

        self.vlc_instance = vlc.Instance()
        self.media_player = self.vlc_instance.media_player_new()
        # if sys.platform == "darwin":  # for MacOS
        #     self.ui.frame_video = QMacCocoaViewContainer(0)

        self.ui.frame_video.doubleClicked.connect(self.toggle_full_screen)
        self.ui.frame_video.wheel.connect(self.wheel_handler)
        self.ui.frame_video.keyPressed.connect(self.key_handler)

        # Set up Labels:
        # self.ui.lblVideoName.

        # self.displayed_video_title = bind("self.ui.lblVideoName", "text", str)
        self.ui.lblVideoName.setText(self.video_filename)
        self.ui.lblVideoSubtitle.setText("")
        self.ui.dateTimeEdit.setHidden(True)
        self.ui.lblCurrentFrame.setText("")
        self.ui.lblTotalFrames.setText("")

        self.ui.lblCurrentTime.setText("")
        self.ui.lblTotalDuration.setText("")

        self.ui.lblFileFPS.setText("")

        self.ui.spinBoxFrameJumpMultiplier.value = 1



        # Set up buttons
        self.ui.button_run.clicked.connect(self.run)
        self.ui.button_timestamp_browse.clicked.connect(
            self.browse_timestamp_handler
        )
        self.ui.button_timestamp_create.clicked.connect(
            self.create_timestamp_file_handler
        )
        self.ui.button_video_browse.clicked.connect(
            self.browse_video_handler
        )


        # Set up directional buttons
        self.ui.btnSkipLeft.clicked.connect(self.skip_left_handler)
        self.ui.btnSkipRight.clicked.connect(self.skip_right_handler)
        self.ui.btnLeft.clicked.connect(self.seek_left_handler)
        self.ui.btnRight.clicked.connect(self.seek_right_handler)

        self.play_pause_model = ToggleButtonModel(None, self)
        self.play_pause_model.setStateMap(
            {
                True: {
                    "text": "",
                    "icon": qta.icon("fa.play", scale_factor=0.7)
                },
                False: {
                    "text": "",
                    "icon": qta.icon("fa.pause", scale_factor=0.7)
                }
            }
        )
        self.ui.button_play_pause.setModel(self.play_pause_model)
        self.ui.button_play_pause.clicked.connect(self.play_pause)

        self.mute_model = ToggleButtonModel(None, self)
        self.mute_model.setStateMap(
            {
                True: {
                    "text": "",
                    "icon": qta.icon("fa.volume-up", scale_factor=0.8)
                },
                False: {
                    "text": "",
                    "icon": qta.icon("fa.volume-off", scale_factor=0.8)
                }
            }
        )
        self.ui.button_mute_toggle.setModel(self.mute_model)
        self.ui.button_mute_toggle.clicked.connect(self.toggle_mute)

        self.ui.button_full_screen.setIcon(
            qta.icon("ei.fullscreen", scale_factor=0.6)
        )
        self.ui.button_full_screen.setText("")
        self.ui.button_full_screen.clicked.connect(self.toggle_full_screen)
        self.ui.button_speed_up.clicked.connect(self.speed_up_handler)
        self.ui.button_speed_up.setIcon(
            qta.icon("fa.arrow-circle-o-up", scale_factor=0.8)
        )
        self.ui.button_speed_up.setText("")
        self.ui.button_slow_down.clicked.connect(self.slow_down_handler)
        self.ui.button_slow_down.setIcon(
            qta.icon("fa.arrow-circle-o-down", scale_factor=0.8)
        )
        self.ui.button_slow_down.setText("")
        self.ui.button_mark_start.setIcon(
            qta.icon("fa.quote-left", scale_factor=0.7)
        )
        self.ui.button_mark_start.setText("")
        self.ui.button_mark_end.setIcon(
            qta.icon("fa.quote-right", scale_factor=0.7)
        )
        self.ui.button_mark_end.setText("")
        self.ui.button_add_entry.clicked.connect(self.add_entry)
        self.ui.button_remove_entry.clicked.connect(self.remove_entry)

        self.ui.button_mark_start.clicked.connect(
            lambda: self.set_mark(start_time=int(
                self.media_player.get_position() *
                self.media_player.get_media().get_duration()))
        )
        self.ui.button_mark_end.clicked.connect(
            lambda: self.set_mark(end_time=int(
                self.media_player.get_position() *
                self.media_player.get_media().get_duration()))
        )

        self.ui.slider_progress.setTracking(False)
        self.ui.slider_progress.valueChanged.connect(self.set_media_position)
        self.ui.slider_volume.valueChanged.connect(self.set_volume)
        self.ui.entry_description.setReadOnly(True)

        # Mapper between the table and the entry detail
        self.mapper = QDataWidgetMapper()
        self.mapper.setSubmitPolicy(QDataWidgetMapper.ManualSubmit)
        self.ui.button_save.clicked.connect(self.mapper.submit)

        # Set up default volume
        self.set_volume(self.ui.slider_volume.value())

        self.vlc_events = self.media_player.event_manager()
        self.vlc_events.event_attach(
            vlc.EventType.MediaPlayerTimeChanged, self.media_time_change_handler
        )

        # Let our application handle mouse and key input instead of VLC
        self.media_player.video_set_mouse_input(False)
        self.media_player.video_set_key_input(False)

        self.ui.show()


    def add_entry(self):
        if not self.timestamp_filename:
            self._show_error("You haven't chosen a timestamp file yet")
        row_num = self.timestamp_model.rowCount()
        self.timestamp_model.insertRow(row_num)
        start_cell = self.timestamp_model.index(row_num, 0)
        end_cell = self.timestamp_model.index(row_num, 1)
        self.timestamp_model.setData(start_cell, TimestampDelta.from_string(""))
        self.timestamp_model.setData(end_cell, TimestampDelta.from_string(""))

    def remove_entry(self):
        if not self.timestamp_filename:
            self._show_error("You haven't chosen a timestamp file yet")
        selected = self.ui.list_timestamp.selectionModel().selectedIndexes()
        if len(selected) == 0:
            return
        self.proxy_model.removeRow(selected[0].row()) and self.mapper.submit()


    def set_media_position(self, position):
        percentage = position / 10000.0
        self.media_player.set_position(percentage)
        absolute_position = percentage * \
            self.media_player.get_media().get_duration()
        if absolute_position > self.media_end_time:
            self.media_end_time = -1

    def set_mark(self, start_time=None, end_time=None):
        if len(self.ui.list_timestamp.selectedIndexes()) == 0:
            blankRowIndex = self.timestamp_model.blankRowIndex()
            if not blankRowIndex.isValid():
                self.add_entry()
            else:
                index = self.proxy_model.mapFromSource(blankRowIndex)
                self.ui.list_timestamp.selectRow(index.row())
        selectedIndexes = self.ui.list_timestamp.selectedIndexes()
        if start_time:
            self.proxy_model.setData(selectedIndexes[0],
                                     TimestampDelta.string_from_int(
                                         start_time))
        if end_time:
            self.proxy_model.setData(selectedIndexes[1],
                                     TimestampDelta.string_from_int(
                                         end_time))

    def update_ui(self):
        self.ui.slider_progress.blockSignals(True)
        self.ui.slider_progress.setValue(
            self.media_player.get_position() * 10000
        )
        #print(self.media_player.get_position() * 10000)

        self.update_video_file_play_labels()

        # When the video finishes
        self.ui.slider_progress.blockSignals(False)
        if self.media_started_playing and \
           self.media_player.get_media().get_state() == vlc.State.Ended:
            self.play_pause_model.setState(True)
            # Apparently we need to reset the media, otherwise the player
            # won't play at all
            self.media_player.set_media(self.media_player.get_media())
            self.set_volume(self.ui.slider_volume.value())
            self.media_is_playing = False
            self.media_started_playing = False
            self.run()

    def timer_handler(self):
        """
        This is a workaround, because for some reason we can't call set_time()
        inside the MediaPlayerTimeChanged handler (as the video just stops
        playing)
        """
        if self.restart_needed:
            self.media_player.set_time(self.media_start_time)
            self.restart_needed = False

    def key_handler(self, event):
        if event.key() == Qt.Key_Escape and self.is_full_screen:
            self.toggle_full_screen()
        if event.key() == Qt.Key_F:
            self.toggle_full_screen()
        if event.key() == Qt.Key_Space:
            self.play_pause()

    def wheel_handler(self, event):
        self.modify_volume(1 if event.angleDelta().y() > 0 else -1)

    def toggle_mute(self):
        self.media_player.audio_set_mute(not self.media_player.audio_get_mute())
        self.mute = not self.mute
        self.mute_model.setState(not self.mute)

    def modify_volume(self, delta_percent):
        new_volume = self.media_player.audio_get_volume() + delta_percent
        if new_volume < 0:
            new_volume = 0
        elif new_volume > 40:
            new_volume = 40
        self.media_player.audio_set_volume(new_volume)
        self.ui.slider_volume.setValue(self.media_player.audio_get_volume())

    def set_volume(self, new_volume):
        self.media_player.audio_set_volume(new_volume)

    def speed_up_handler(self):
        self.modify_rate(0.1)

    def slow_down_handler(self):
        self.modify_rate(-0.1)

    def modify_rate(self, delta_percent):
        new_rate = self.media_player.get_rate() + delta_percent
        if new_rate < 0.2 or new_rate > 2.0:
            return
        self.media_player.set_rate(new_rate)

    def media_time_change_handler(self, _):
        if self.media_end_time == -1:
            return
        if self.media_player.get_time() > self.media_end_time:
            self.restart_needed = True

    def update_slider_highlight(self):
        if self.ui.list_timestamp.selectionModel().hasSelection():
            selected_row = self.ui.list_timestamp.selectionModel(). \
                selectedRows()[0]
            self.media_start_time = self.ui.list_timestamp.model().data(
                selected_row.model().index(selected_row.row(), 0),
                Qt.UserRole
            )
            self.media_end_time = self.ui.list_timestamp.model().data(
                selected_row.model().index(selected_row.row(), 1),
                Qt.UserRole
            )
            duration = self.media_player.get_media().get_duration()
            self.media_end_time = self.media_end_time \
                if self.media_end_time != 0 else duration
            if self.media_start_time > self.media_end_time:
                raise ValueError("Start time cannot be later than end time")
            if self.media_start_time > duration:
                raise ValueError("Start time not within video duration")
            if self.media_end_time > duration:
                raise ValueError("End time not within video duration")
            slider_start_pos = (self.media_start_time / duration) * \
                               (self.ui.slider_progress.maximum() -
                                self.ui.slider_progress.minimum())
            slider_end_pos = (self.media_end_time / duration) * \
                             (self.ui.slider_progress.maximum() -
                              self.ui.slider_progress.minimum())
            self.ui.slider_progress.setHighlight(
                int(slider_start_pos), int(slider_end_pos)
            )

        else:
            self.media_start_time = 0
            self.media_end_time = -1


    def run(self):
        """
        Execute the loop
        """
        if self.timestamp_filename is None:
            self._show_error("No timestamp file chosen")
            return
        if self.video_filename is None:
            self._show_error("No video file chosen")
            return
        try:
            self.update_slider_highlight()
            self.media_player.play()
            self.media_player.set_time(self.media_start_time)
            self.media_started_playing = True
            self.media_is_playing = True
            self.play_pause_model.setState(False)
        except Exception as ex:
            self._show_error(str(ex))
            print(traceback.format_exc())

    def play_pause(self):
        """Toggle play/pause status
        """
        if not self.media_started_playing:
            self.run()
            return
        if self.media_is_playing:
            self.media_player.pause()
        else:
            self.media_player.play()
        self.media_is_playing = not self.media_is_playing
        self.play_pause_model.setState(not self.media_is_playing)

    def update_video_file_play_labels(self):
        curr_total_fps = self.media_player.get_fps()
        curr_total_duration = self.media_player.get_length()
        totalNumFrames = int(curr_total_duration * curr_total_fps)
        if totalNumFrames > 0:
            self.ui.lblTotalFrames.setText(str(totalNumFrames))
        else:
            self.ui.lblTotalFrames.setText("--")
        if curr_total_duration > 0:
            self.ui.lblTotalDuration.setText(str(curr_total_duration))  # Gets duration in [ms]
        else:
            self.ui.lblTotalDuration.setText("--")

        # Changing Values: Dynamically updated each time the playhead changes
        curr_percent_complete = self.media_player.get_position()  # Current percent complete between 0.0 and 1.0

        if curr_percent_complete >= 0:
            self.ui.lblPlaybackPercent.setText(str(curr_percent_complete))
        else:
            self.ui.lblPlaybackPercent.setText("--")

        curr_frame = int(round(curr_percent_complete * totalNumFrames))

        if curr_frame >= 0:
            self.ui.lblCurrentFrame.setText(str(curr_frame))
        else:
            self.ui.lblCurrentFrame.setText("--")

        if self.media_player.get_time() >= 0:
            self.ui.lblCurrentTime.setText(str(self.media_player.get_time()) + "[ms]")  # Gets time in [ms]
        else:
            self.ui.lblCurrentTime.setText("-- [ms]")  # Gets time in [ms]






    # Called only when the video file changes:
    def update_video_file_labels_on_file_change(self):
        if self.video_filename is None:
            self.ui.lblVideoName.setText("")
        else:
            self.ui.lblVideoName.setText(self.video_filename)
            # Only updated when the video file is changed:
            curr_total_fps = self.media_player.get_fps()
            self.ui.lblFileFPS.setText(str(curr_total_fps))

            curr_total_duration = self.media_player.get_length()
            totalNumFrames = int(curr_total_duration * curr_total_fps)
            if totalNumFrames > 0:
                self.ui.lblTotalFrames.setText(str(totalNumFrames))
            else:
                self.ui.lblTotalFrames.setText("--")

            if curr_total_duration > 0:
                self.ui.lblTotalDuration.setText(str(curr_total_duration)) # Gets duration in [ms]
            else:
                self.ui.lblTotalDuration.setText("--")

            self.update_video_file_play_labels()

    def get_frame_multipler(self):
        return self.ui.spinBoxFrameJumpMultiplier.value

    # def compute_total_number_frames(self):
    #     self.media_player.get_length()


    def seek_left_handler(self):
        print('seek: left')
        self.seek_frames(-10 * self.get_frame_multipler())

    def skip_left_handler(self):
        print('skip: left')
        self.seek_frames(-1 * self.get_frame_multipler())

    def seek_right_handler(self):
        print('seek: right')
        self.seek_frames(10 * self.get_frame_multipler())

    def skip_right_handler(self):
        print('skip: right')
        self.seek_frames(1 * self.get_frame_multipler())

    def seek_frames(self, relativeFrameOffset):
        """Jump a certain number of frames forward or back
        """
        if self.video_filename is None:
            self._show_error("No video file chosen")
            return
        # if self.media_end_time == -1:
        #     return

        curr_total_fps = self.media_player.get_fps()
        relativeSecondsOffset = relativeFrameOffset / curr_total_fps # Desired offset in seconds
        curr_total_duration = self.media_player.get_length()
        relative_percent_offset = relativeSecondsOffset / curr_total_duration # percent of the whole that we want to skip




        totalNumFrames = int(curr_total_duration * curr_total_fps)

        try:
            didPauseMedia = False
            if self.media_is_playing:
                self.media_player.pause()
                didPauseMedia = True

            newPosition = self.media_player.get_position() + relative_percent_offset
            # newTime = int(self.media_player.get_time() + relativeFrameOffset)

            # self.update_slider_highlight()
            # self.media_player.set_time(newTime)
            self.media_player.set_position(newPosition)

            if (didPauseMedia):
                self.media_player.play()
            # else:
            #     # Otherwise, the media was already paused, we need to very quickly play the media to update the frame with the new time, and then immediately pause it again.
            #     self.media_player.play()
            #     self.media_player.pause()
            self.media_player.next_frame()

            print("Setting media playback time to ", newPosition)
        except Exception as ex:
            self._show_error(str(ex))
            print(traceback.format_exc())


    def toggle_full_screen(self):
        if self.is_full_screen:
            # TODO Artifacts still happen some time when exiting full screen
            # in X11
            self.ui.frame_media.showNormal()
            self.ui.frame_media.restoreGeometry(self.original_geometry)
            self.ui.frame_media.setParent(self.ui.widget_central)
            self.ui.layout_main.addWidget(self.ui.frame_media, 2, 3, 3, 1)
            # self.ui.frame_media.ensurePolished()
        else:
            self.ui.frame_media.setParent(None)
            self.ui.frame_media.setWindowFlags(Qt.FramelessWindowHint |
                                               Qt.CustomizeWindowHint)
            self.original_geometry = self.ui.frame_media.saveGeometry()
            desktop = QApplication.desktop()
            rect = desktop.screenGeometry(desktop.screenNumber(QCursor.pos()))
            self.ui.frame_media.setGeometry(rect)
            self.ui.frame_media.showFullScreen()
            self.ui.frame_media.show()
        self.ui.frame_video.setFocus()
        self.is_full_screen = not self.is_full_screen

    def browse_timestamp_handler(self):
        """
        Handler when the timestamp browser button is clicked
        """
        tmp_name, _ = QFileDialog.getOpenFileName(
            self, "Choose Timestamp file", None,
            "Timestamp File (*.tmsp);;All Files (*)"
        )
        if not tmp_name:
            return
        self.set_timestamp_filename(QDir.toNativeSeparators(tmp_name))

    def create_timestamp_file_handler(self):
        """
        Handler when the timestamp file create button is clicked
        """
        tmp_name, _ = QFileDialog.getSaveFileName(
            self, "Create New Timestamp file", None,
            "Timestamp File (*.tmsp);;All Files (*)"
        )
        if not tmp_name:
            return

        try:
            if (os.stat(QDir.toNativeSeparators(tmp_name)).st_size == 0):
                    # File is empty, create a non-empty one:
                    with open(QDir.toNativeSeparators(tmp_name), "w") as fh:
                        fh.write("[]")  # Write the minimal valid JSON string to the file to allow it to be used
            else:
                pass

            # with open(tmp_name, 'r') as fh:
            #     if fh.__sizeof__()>0:
            #         # File is not empty:
            #         pass
            #     else:
            #         # File is empty, create a non-empty one:
            #         fh.close()
            #         with open(tmp_name, "w") as fh:
            #             fh.write("[]")  # Write the minimal valid JSON string to the file to allow it to be used

        except WindowsError:
            with open(tmp_name, "w") as fh:
                fh.write("[]") # Write the minimal valid JSON string to the file to allow it to be used


        # Create new file:
        self.set_timestamp_filename(QDir.toNativeSeparators(tmp_name))

    def _sort_model(self):
        self.ui.list_timestamp.sortByColumn(0, Qt.AscendingOrder)

    def _select_blank_row(self, parent, start, end):
        self.ui.list_timestamp.selectRow(start)

    def set_timestamp_filename(self, filename):
        """
        Set the timestamp file name
        """
        if not os.path.isfile(filename):
            self._show_error("Cannot access timestamp file " + filename)
            return

        try:
            self.timestamp_model = TimestampModel(filename, self)
            self.timestamp_model.timeParseError.connect(
                lambda err: self._show_error(err)
            )
            self.proxy_model.setSortRole(Qt.UserRole)
            self.proxy_model.dataChanged.connect(self._sort_model)
            self.proxy_model.dataChanged.connect(self.update_slider_highlight)
            self.proxy_model.setSourceModel(self.timestamp_model)
            self.proxy_model.rowsInserted.connect(self._sort_model)
            self.proxy_model.rowsInserted.connect(self._select_blank_row)
            self.ui.list_timestamp.setModel(self.proxy_model)

            self.timestamp_filename = filename
            self.ui.entry_timestamp.setText(self.timestamp_filename)

            self.mapper.setModel(self.proxy_model)
            self.mapper.addMapping(self.ui.entry_start_time, 0)
            self.mapper.addMapping(self.ui.entry_end_time, 1)
            self.mapper.addMapping(self.ui.entry_description, 2)
            self.ui.list_timestamp.selectionModel().selectionChanged.connect(
                self.timestamp_selection_changed)
            self._sort_model()

            directory = os.path.dirname(self.timestamp_filename)
            basename = os.path.basename(self.timestamp_filename)
            timestamp_name_without_ext = os.path.splitext(basename)[0]
            for file_in_dir in os.listdir(directory):
                current_filename = os.path.splitext(file_in_dir)[0]
                found_video = (current_filename == timestamp_name_without_ext
                               and file_in_dir != basename)
                if found_video:
                    found_video_file = os.path.join(directory, file_in_dir)
                    self.set_video_filename(found_video_file)
                    break
        except ValueError as err:
            self._show_error("Timestamp file is invalid")

    def timestamp_selection_changed(self, selected, deselected):
        if len(selected) > 0:
            self.mapper.setCurrentModelIndex(selected.indexes()[0])
            self.ui.button_save.setEnabled(True)
            self.ui.button_remove_entry.setEnabled(True)
            self.ui.entry_start_time.setReadOnly(False)
            self.ui.entry_end_time.setReadOnly(False)
            self.ui.entry_description.setReadOnly(False)
        else:
            self.mapper.setCurrentModelIndex(QModelIndex())
            self.ui.button_save.setEnabled(False)
            self.ui.button_remove_entry.setEnabled(False)
            self.ui.entry_start_time.clear()
            self.ui.entry_end_time.clear()
            self.ui.entry_description.clear()
            self.ui.entry_start_time.setReadOnly(True)
            self.ui.entry_end_time.setReadOnly(True)
            self.ui.entry_description.setReadOnly(True)

    def set_video_filename(self, filename):
        """
        Set the video filename
        """
        if not os.path.isfile(filename):
            self._show_error("Cannot access video file " + filename)
            return

        self.video_filename = filename

        media = self.vlc_instance.media_new(self.video_filename)
        media.parse()
        if not media.get_duration():
            self._show_error("Cannot play this media file")
            self.media_player.set_media(None)
            self.video_filename = None
        else:
            self.media_player.set_media(media)
            if sys.platform.startswith('linux'): # for Linux using the X Server
                self.media_player.set_xwindow(self.ui.frame_video.winId())
            elif sys.platform == "win32": # for Windows
                self.media_player.set_hwnd(self.ui.frame_video.winId())
            elif sys.platform == "darwin": # for MacOS
                self.media_player.set_nsobject(self.ui.frame_video.winId())
            self.ui.entry_video.setText(self.video_filename)

            self.update_video_file_labels_on_file_change()
            self.media_started_playing = False
            self.media_is_playing = False
            self.set_volume(self.ui.slider_volume.value())
            self.play_pause_model.setState(True)

    def browse_video_handler(self):
        """
        Handler when the video browse button is clicked
        """
        tmp_name, _ = QFileDialog.getOpenFileName(
            self, "Choose Video file", None,
            "All Files (*)"
        )
        if not tmp_name:
            return
        self.set_video_filename(QDir.toNativeSeparators(tmp_name))

    def _show_error(self, message, title="Error"):
        QMessageBox.warning(self, title, message)