Beispiel #1
0
class GuiProjectLoad(QDialog):

    NONE_STATE = 0
    NEW_STATE  = 1
    OPEN_STATE = 2

    C_NAME  = 0
    C_COUNT = 1
    C_TIME  = 2

    def __init__(self, theParent):
        QDialog.__init__(self, theParent)

        logger.debug("Initialising GuiProjectLoad ...")
        self.setObjectName("GuiProjectLoad")

        self.mainConf  = nw.CONFIG
        self.theParent = theParent
        self.theTheme  = theParent.theTheme
        self.openState = self.NONE_STATE
        self.openPath  = None

        sPx = self.mainConf.pxInt(16)
        nPx = self.mainConf.pxInt(96)
        iPx = self.theTheme.baseIconSize

        self.outerBox = QVBoxLayout()
        self.innerBox = QHBoxLayout()
        self.outerBox.setSpacing(sPx)
        self.innerBox.setSpacing(sPx)

        self.setWindowTitle(self.tr("Open Project"))
        self.setMinimumWidth(self.mainConf.pxInt(650))
        self.setMinimumHeight(self.mainConf.pxInt(400))
        self.setModal(True)

        self.nwIcon = QLabel()
        self.nwIcon.setPixmap(self.theParent.theTheme.getPixmap("novelwriter", (nPx, nPx)))
        self.innerBox.addWidget(self.nwIcon, 0, Qt.AlignTop)

        self.projectForm = QGridLayout()
        self.projectForm.setContentsMargins(0, 0, 0, 0)

        self.listBox = QTreeWidget()
        self.listBox.setSelectionMode(QAbstractItemView.SingleSelection)
        self.listBox.setDragDropMode(QAbstractItemView.NoDragDrop)
        self.listBox.setColumnCount(3)
        self.listBox.setHeaderLabels([
            self.tr("Working Title"),
            self.tr("Words"),
            self.tr("Last Opened"),
        ])
        self.listBox.setRootIsDecorated(False)
        self.listBox.itemSelectionChanged.connect(self._doSelectRecent)
        self.listBox.itemDoubleClicked.connect(self._doOpenRecent)
        self.listBox.setIconSize(QSize(iPx, iPx))

        treeHead = self.listBox.headerItem()
        treeHead.setTextAlignment(self.C_COUNT, Qt.AlignRight)
        treeHead.setTextAlignment(self.C_TIME, Qt.AlignRight)

        self.lblRecent = QLabel("<b>%s</b>" % self.tr("Recently Opened Projects"))
        self.lblPath   = QLabel("<b>%s</b>" % self.tr("Path"))
        self.selPath   = QLineEdit("")
        self.selPath.setReadOnly(True)

        self.browseButton = QPushButton("...")
        self.browseButton.setMaximumWidth(int(2.5*self.theTheme.getTextWidth("...")))
        self.browseButton.clicked.connect(self._doBrowse)

        self.projectForm.addWidget(self.lblRecent,    0, 0, 1, 3)
        self.projectForm.addWidget(self.listBox,      1, 0, 1, 3)
        self.projectForm.addWidget(self.lblPath,      2, 0, 1, 1)
        self.projectForm.addWidget(self.selPath,      2, 1, 1, 1)
        self.projectForm.addWidget(self.browseButton, 2, 2, 1, 1)
        self.projectForm.setColumnStretch(0, 0)
        self.projectForm.setColumnStretch(1, 1)
        self.projectForm.setColumnStretch(2, 0)
        self.projectForm.setVerticalSpacing(self.mainConf.pxInt(4))
        self.projectForm.setHorizontalSpacing(self.mainConf.pxInt(8))

        self.innerBox.addLayout(self.projectForm)

        self.buttonBox = QDialogButtonBox(QDialogButtonBox.Open | QDialogButtonBox.Cancel)
        self.buttonBox.accepted.connect(self._doOpenRecent)
        self.buttonBox.rejected.connect(self._doCancel)

        self.newButton = self.buttonBox.addButton(self.tr("New"), QDialogButtonBox.ActionRole)
        self.newButton.clicked.connect(self._doNewProject)

        self.delButton = self.buttonBox.addButton(self.tr("Remove"), QDialogButtonBox.ActionRole)
        self.delButton.clicked.connect(self._doDeleteRecent)

        self.outerBox.addLayout(self.innerBox)
        self.outerBox.addWidget(self.buttonBox)
        self.setLayout(self.outerBox)

        self._populateList()
        self._doSelectRecent()

        keyDelete = QShortcut(self.listBox)
        keyDelete.setKey(QKeySequence(Qt.Key_Delete))
        keyDelete.activated.connect(self._doDeleteRecent)

        logger.debug("GuiProjectLoad initialisation complete")

        return

    ##
    #  Buttons
    ##

    def _doOpenRecent(self):
        """Close the dialog window with a recent project selected.
        """
        logger.verbose("GuiProjectLoad open button clicked")
        self._saveSettings()

        self.openPath = None
        self.openState = self.NONE_STATE

        selItems = self.listBox.selectedItems()
        if selItems:
            self.openPath = selItems[0].data(self.C_NAME, Qt.UserRole)
            self.openState = self.OPEN_STATE
            self.accept()

        return

    def _doSelectRecent(self):
        """A recent item has been selected.
        """
        selList = self.listBox.selectedItems()
        if selList:
            self.selPath.setText(selList[0].data(self.C_NAME, Qt.UserRole))
        return

    def _doBrowse(self):
        """Browse for a folder path.
        """
        logger.verbose("GuiProjectLoad browse button clicked")
        extFilter = [
            self.tr("novelWriter Project File ({0})").format(nwFiles.PROJ_FILE),
            self.tr("All files ({0})").format("*"),
        ]
        projFile, _ = QFileDialog.getOpenFileName(
            self, self.tr("Open Project"), "", filter=";;".join(extFilter)
        )
        if projFile:
            thePath = os.path.abspath(os.path.dirname(projFile))
            self.selPath.setText(thePath)
            self.openPath = thePath
            self.openState = self.OPEN_STATE
            self.accept()

        return

    def _doCancel(self):
        """Close the dialog window without doing anything.
        """
        logger.verbose("GuiProjectLoad close button clicked")
        self.openPath = None
        self.openState = self.NONE_STATE
        self.close()
        return

    def _doNewProject(self):
        """Create a new project.
        """
        logger.verbose("GuiProjectLoad new project button clicked")
        self._saveSettings()
        self.openPath = None
        self.openState = self.NEW_STATE
        self.accept()
        return

    def _doDeleteRecent(self):
        """Remove an entry from the recent projects list.
        """
        selList = self.listBox.selectedItems()
        if selList:
            projName = selList[0].text(self.C_NAME)
            msgYes = self.theParent.askQuestion(
                self.tr("Remove Entry"),
                self.tr(
                    "Remove '{0}' from the recent projects list? "
                    "The project files will not be deleted."
                ).format(projName)
            )
            if msgYes:
                self.mainConf.removeFromRecentCache(
                    selList[0].data(self.C_NAME, Qt.UserRole)
                )
                self._populateList()

        return

    ##
    #  Events
    ##

    def closeEvent(self, theEvent):
        """Capture the user closing the dialog so we can save settings.
        """
        self._saveSettings()
        theEvent.accept()
        return

    ##
    #  Internal Functions
    ##

    def _saveSettings(self):
        """Save the changes made to the dialog.
        """
        colWidths = [0, 0, 0]
        colWidths[self.C_NAME]  = self.listBox.columnWidth(self.C_NAME)
        colWidths[self.C_COUNT] = self.listBox.columnWidth(self.C_COUNT)
        colWidths[self.C_TIME]  = self.listBox.columnWidth(self.C_TIME)
        self.mainConf.setProjColWidths(colWidths)
        return

    def _populateList(self):
        """Populate the list box with recent project data.
        """
        dataList = []
        for projPath in self.mainConf.recentProj:
            theEntry = self.mainConf.recentProj[projPath]
            theTitle = theEntry.get("title", "")
            theTime  = theEntry.get("time", 0)
            theWords = theEntry.get("words", 0)
            dataList.append([theTitle, theTime, theWords, projPath])

        self.listBox.clear()
        sortList = sorted(dataList, key=lambda x: x[1], reverse=True)
        for theTitle, theTime, theWords, projPath in sortList:
            newItem = QTreeWidgetItem([""]*4)
            newItem.setIcon(self.C_NAME,  self.theParent.theTheme.getIcon("proj_nwx"))
            newItem.setText(self.C_NAME,  theTitle)
            newItem.setData(self.C_NAME,  Qt.UserRole, projPath)
            newItem.setText(self.C_COUNT, formatInt(theWords))
            newItem.setText(self.C_TIME,  datetime.fromtimestamp(theTime).strftime("%x %X"))
            newItem.setTextAlignment(self.C_NAME,  Qt.AlignLeft  | Qt.AlignVCenter)
            newItem.setTextAlignment(self.C_COUNT, Qt.AlignRight | Qt.AlignVCenter)
            newItem.setTextAlignment(self.C_TIME,  Qt.AlignRight | Qt.AlignVCenter)
            newItem.setFont(self.C_TIME, self.theTheme.guiFontFixed)
            self.listBox.addTopLevelItem(newItem)

        if self.listBox.topLevelItemCount() > 0:
            self.listBox.topLevelItem(0).setSelected(True)

        projColWidth = self.mainConf.getProjColWidths()
        if len(projColWidth) == 3:
            self.listBox.setColumnWidth(self.C_NAME,  projColWidth[self.C_NAME])
            self.listBox.setColumnWidth(self.C_COUNT, projColWidth[self.C_COUNT])
            self.listBox.setColumnWidth(self.C_TIME,  projColWidth[self.C_TIME])

        return
class PythonAstViewer(QWidget):
    """
    Class implementing a widget to visualize the Python AST for some Python
    sources.
    """
    StartLineRole = Qt.UserRole
    StartIndexRole = Qt.UserRole + 1
    EndLineRole = Qt.UserRole + 2
    EndIndexRole = Qt.UserRole + 3
    
    def __init__(self, viewmanager, parent=None):
        """
        Constructor
        
        @param viewmanager reference to the viewmanager object
        @type ViewManager
        @param parent reference to the parent widget
        @type QWidget
        """
        super(PythonAstViewer, self).__init__(parent)
        
        self.__layout = QVBoxLayout(self)
        self.setLayout(self.__layout)
        self.__astWidget = QTreeWidget(self)
        self.__layout.addWidget(self.__astWidget)
        self.__layout.setContentsMargins(0, 0, 0, 0)
        
        self.__vm = viewmanager
        self.__vmConnected = False
        
        self.__editor = None
        self.__source = ""
        
        self.__astWidget.setHeaderLabels([self.tr("Node"),
                                          self.tr("Code Range")])
        self.__astWidget.setSortingEnabled(False)
        self.__astWidget.setSelectionBehavior(QAbstractItemView.SelectRows)
        self.__astWidget.setSelectionMode(QAbstractItemView.SingleSelection)
        self.__astWidget.setAlternatingRowColors(True)
        
        self.__astWidget.itemClicked.connect(self.__astItemClicked)
        
        self.__vm.astViewerStateChanged.connect(self.__astViewerStateChanged)
        
        self.hide()
    
    def __editorChanged(self, editor):
        """
        Private slot to handle a change of the current editor.
        
        @param editor reference to the current editor
        @type Editor
        """
        if editor is not self.__editor:
            if self.__editor:
                self.__editor.clearAllHighlights()
            self.__editor = editor
            if self.__editor:
                self.__loadAST()
    
    def __editorSaved(self, editor):
        """
        Private slot to reload the AST after the connected editor was saved.
        
        @param editor reference to the editor that performed a save action
        @type Editor
        """
        if editor and editor is self.__editor:
            self.__loadAST()
    
    def __editorDoubleClicked(self, editor, pos, buttons):
        """
        Private slot to handle a mouse button double click in the editor.
        
        @param editor reference to the editor, that emitted the signal
        @type Editor
        @param pos position of the double click
        @type QPoint
        @param buttons mouse buttons that were double clicked
        @type Qt.MouseButtons
        """
        if editor is self.__editor and buttons == Qt.LeftButton:
            if editor.isModified():
                # reload the source
                QTimer.singleShot(0, self.__loadAST)
            else:
                # highlight the corresponding entry
                QTimer.singleShot(0, self.__selectItemForEditorSelection)
                QTimer.singleShot(0, self.__grabFocus)
    
    def __lastEditorClosed(self):
        """
        Private slot to handle the last editor closed signal of the view
        manager.
        """
        self.hide()
    
    def show(self):
        """
        Public slot to show the AST viewer.
        """
        super(PythonAstViewer, self).show()
        
        if not self.__vmConnected:
            self.__vm.editorChangedEd.connect(self.__editorChanged)
            self.__vm.editorSavedEd.connect(self.__editorSaved)
            self.__vm.editorDoubleClickedEd.connect(self.__editorDoubleClicked)
            self.__vmConnected = True
    
    def hide(self):
        """
        Public slot to hide the AST viewer.
        """
        super(PythonAstViewer, self).hide()
        
        if self.__editor:
            self.__editor.clearAllHighlights()
        
        if self.__vmConnected:
            self.__vm.editorChangedEd.disconnect(self.__editorChanged)
            self.__vm.editorSavedEd.disconnect(self.__editorSaved)
            self.__vm.editorDoubleClickedEd.disconnect(
                self.__editorDoubleClicked)
            self.__vmConnected = False
    
    def shutdown(self):
        """
        Public method to perform shutdown actions.
        """
        self.__editor = None
    
    def __astViewerStateChanged(self, on):
        """
        Private slot to toggle the display of the AST viewer.
        
        @param on flag indicating to show the AST
        @type bool
        """
        editor = self.__vm.activeWindow()
        if on and editor and editor.isPyFile():
            if editor is not self.__editor:
                self.__editor = editor
            self.show()
            self.__loadAST()
        else:
            self.hide()
            self.__editor = None
    
    def __createErrorItem(self, error):
        """
        Private method to create a top level error item.
        
        @param error error message
        @type str
        @return generated item
        @rtype QTreeWidgetItem
        """
        itm = QTreeWidgetItem(self.__astWidget, [error])
        itm.setFirstColumnSpanned(True)
        itm.setForeground(0, QBrush(Qt.red))
        return itm
    
    def __loadAST(self):
        """
        Private method to generate the AST from the source of the current
        editor and visualize it.
        """
        if not self.__editor:
            return
        
        self.__astWidget.clear()
        self.__editor.clearAllHighlights()
        
        if not self.__editor.isPyFile():
            self.__createErrorItem(self.tr(
                "The current editor text does not contain Python source."
            ))
            return
        
        source = self.__editor.text()
        if not source.strip():
            # empty editor or white space only
            return
        
        QApplication.setOverrideCursor(QCursor(Qt.WaitCursor))
        try:
            # generate the AST
            root = ast.parse(source, self.__editor.getFileName(), "exec")
            self.__markTextRanges(root, source)
            astValid = True
        except Exception as exc:
            self.__createErrorItem(str(exc))
            astValid = False
        
        if astValid:
            self.setUpdatesEnabled(False)
            
            # populate the AST tree
            self.__populateNode(self.tr("Module"), root, self.__astWidget)
            self.__selectItemForEditorSelection()
            QTimer.singleShot(0, self.__resizeColumns)
            
            self.setUpdatesEnabled(True)
        
        QApplication.restoreOverrideCursor()
        
        self.__grabFocus()
    
    def __populateNode(self, name, nodeOrFields, parent):
        """
        Private method to populate the tree view with a node.
        
        @param name name of the node
        @type str
        @param nodeOrFields reference to the node or a list node fields
        @type ast.AST or list
        @param parent reference to the parent item
        @type QTreeWidget or QTreeWidgetItem
        """
        if isinstance(nodeOrFields, ast.AST):
            fields = [(key, val) for key, val in ast.iter_fields(nodeOrFields)]
            value = nodeOrFields.__class__.__name__
        elif isinstance(nodeOrFields, list):
            fields = list(enumerate(nodeOrFields))
            if len(nodeOrFields) == 0:
                value = "[]"
            else:
                value = "[...]"
        else:
            fields = []
            value = repr(nodeOrFields)
        
        text = self.tr("{0}: {1}").format(name, value)
        itm = QTreeWidgetItem(parent, [text])
        itm.setExpanded(True)
        
        if (
            hasattr(nodeOrFields, "lineno") and
            hasattr(nodeOrFields, "col_offset")
        ):
            itm.setData(0, self.StartLineRole, nodeOrFields.lineno)
            itm.setData(0, self.StartIndexRole, nodeOrFields.col_offset)
            startStr = self.tr("{0},{1}").format(
                nodeOrFields.lineno, nodeOrFields.col_offset)
            endStr = ""
            
            if (
                hasattr(nodeOrFields, "end_lineno") and
                hasattr(nodeOrFields, "end_col_offset")
            ):
                itm.setData(0, self.EndLineRole, nodeOrFields.end_lineno)
                itm.setData(0, self.EndIndexRole,
                            nodeOrFields.end_col_offset)
                endStr = self.tr("{0},{1}").format(
                    nodeOrFields.end_lineno, nodeOrFields.end_col_offset)
            else:
                itm.setData(0, self.EndLineRole, nodeOrFields.lineno)
                itm.setData(0, self.EndIndexRole,
                            nodeOrFields.col_offset + 1)
            if endStr:
                rangeStr = self.tr("{0}  -  {1}").format(startStr, endStr)
            else:
                rangeStr = startStr
            
            itm.setText(1, rangeStr)
        
        for fieldName, fieldValue in fields:
            self.__populateNode(fieldName, fieldValue, itm)
    
    def __markTextRanges(self, tree, source):
        """
        Private method to modify the AST nodes with end_lineno and
        end_col_offset information.
        
        Note: The modifications are only done for nodes containing lineno and
        col_offset attributes.
        
        @param tree reference to the AST to be modified
        @type ast.AST
        @param source source code the AST was created from
        @type str
        """
        ASTTokens(source, tree=tree)
        for child in ast.walk(tree):
            if hasattr(child, 'last_token'):
                child.end_lineno, child.end_col_offset = child.last_token.end
                if hasattr(child, 'lineno'):
                    # Fixes problems with some nodes like binop
                    child.lineno, child.col_offset = child.first_token.start
    
    def __findClosestContainingNode(self, node, textRange):
        """
        Private method to search for the AST node that contains a range
        closest.
        
        @param node AST node to start searching at
        @type ast.AST
        @param textRange tuple giving the start and end positions
        @type tuple of (int, int, int, int)
        @return best matching node
        @rtype ast.AST
        """
        if textRange in [(-1, -1, -1, -1), (0, -1, 0, -1)]:
            # no valid range, i.e. no selection
            return None
        
        # first look among children
        for child in ast.iter_child_nodes(node):
            result = self.__findClosestContainingNode(child, textRange)
            if result is not None:
                return result
        
        # no suitable child was found
        if hasattr(node, "lineno") and self.__rangeContainsSmaller(
            (node.lineno, node.col_offset, node.end_lineno,
             node.end_col_offset), textRange):
            return node
        else:
            # nope
            return None
    
    def __findClosestContainingItem(self, itm, textRange):
        """
        Private method to search for the tree item that contains a range
        closest.
        
        @param itm tree item to start searching at
        @type QTreeWidgetItem
        @param textRange tuple giving the start and end positions
        @type tuple of (int, int, int, int)
        @return best matching tree item
        @rtype QTreeWidgetItem
        """
        if textRange in [(-1, -1, -1, -1), (0, -1, 0, -1)]:
            # no valid range, i.e. no selection
            return None
        
        lineno = itm.data(0, self.StartLineRole)
        if lineno is not None and not self.__rangeContainsSmallerOrEqual(
           (itm.data(0, self.StartLineRole), itm.data(0, self.StartIndexRole),
            itm.data(0, self.EndLineRole), itm.data(0, self.EndIndexRole)),
           textRange):
            return None
        
        # first look among children
        for index in range(itm.childCount()):
            child = itm.child(index)
            result = self.__findClosestContainingItem(child, textRange)
            if result is not None:
                return result
        
        # no suitable child was found
        lineno = itm.data(0, self.StartLineRole)
        if lineno is not None and self.__rangeContainsSmallerOrEqual(
           (itm.data(0, self.StartLineRole), itm.data(0, self.StartIndexRole),
            itm.data(0, self.EndLineRole), itm.data(0, self.EndIndexRole)),
           textRange):
            return itm
        else:
            # nope
            return None
    
    def __resizeColumns(self):
        """
        Private method to resize the columns to suitable values.
        """
        for col in range(self.__astWidget.columnCount()):
            self.__astWidget.resizeColumnToContents(col)
        
        rangeSize = self.__astWidget.columnWidth(1) + 10
        # 10 px extra for the range
        nodeSize = max(400, self.__astWidget.viewport().width() - rangeSize)
        self.__astWidget.setColumnWidth(0, nodeSize)
        self.__astWidget.setColumnWidth(1, rangeSize)
    
    def resizeEvent(self, evt):
        """
        Protected method to handle resize events.
        
        @param evt resize event
        @type QResizeEvent
        """
        # just adjust the sizes of the columns
        self.__resizeColumns()
    
    def __rangeContainsSmaller(self, first, second):
        """
        Private method to check, if second is contained in first.
        
        @param first text range to check against
        @type tuple of (int, int, int, int)
        @param second text range to check for
        @type tuple of (int, int, int, int)
        @return flag indicating second is contained in first
        @rtype bool
        """
        firstStart = first[:2]
        firstEnd = first[2:]
        secondStart = second[:2]
        secondEnd = second[2:]

        return (
            (firstStart < secondStart and firstEnd > secondEnd) or
            (firstStart == secondStart and firstEnd > secondEnd) or
            (firstStart < secondStart and firstEnd == secondEnd)
        )
    
    def __rangeContainsSmallerOrEqual(self, first, second):
        """
        Private method to check, if second is contained in or equal to first.
        
        @param first text range to check against
        @type tuple of (int, int, int, int)
        @param second text range to check for
        @type tuple of (int, int, int, int)
        @return flag indicating second is contained in or equal to first
        @rtype bool
        """
        return first == second or self.__rangeContainsSmaller(first, second)
    
    def __clearSelection(self):
        """
        Private method to clear all selected items.
        """
        for itm in self.__astWidget.selectedItems():
            itm.setSelected(False)
    
    def __selectItemForEditorSelection(self):
        """
        Private slot to select the item corresponding to an editor selection.
        """
        # step 1: clear all selected items
        self.__clearSelection()
        
        # step 2: retrieve the editor selection
        selection = self.__editor.getSelection()
        # make the line numbers 1-based
        selection = (selection[0] + 1, selection[1],
                     selection[2] + 1, selection[3])
        
        # step 3: search the corresponding item, scroll to it and select it
        itm = self.__findClosestContainingItem(
            self.__astWidget.topLevelItem(0), selection)
        if itm:
            self.__astWidget.scrollToItem(
                itm, QAbstractItemView.PositionAtCenter)
            itm.setSelected(True)
    
    def __grabFocus(self):
        """
        Private method to grab the input focus.
        """
        self.__astWidget.setFocus(Qt.OtherFocusReason)
    
    @pyqtSlot(QTreeWidgetItem, int)
    def __astItemClicked(self, itm, column):
        """
        Private slot handling a user click on an AST node item.
        
        @param itm reference to the clicked item
        @type QTreeWidgetItem
        @param column column number of the click
        @type int
        """
        self.__editor.clearAllHighlights()
        
        if itm is not None:
            startLine = itm.data(0, self.StartLineRole)
            if startLine is not None:
                startIndex = itm.data(0, self.StartIndexRole)
                endLine = itm.data(0, self.EndLineRole)
                endIndex = itm.data(0, self.EndIndexRole)
                
                self.__editor.gotoLine(startLine, firstVisible=True,
                                       expand=True)
                self.__editor.setHighlight(startLine - 1, startIndex,
                                           endLine - 1, endIndex)
class GuiWritingStats(QDialog):

    C_TIME   = 0
    C_LENGTH = 1
    C_COUNT  = 2
    C_BAR    = 3

    FMT_JSON = 0
    FMT_CSV  = 1

    def __init__(self, theParent, theProject):
        QDialog.__init__(self, theParent)

        logger.debug("Initialising GuiWritingStats ...")
        self.setObjectName("GuiWritingStats")

        self.mainConf   = nw.CONFIG
        self.theParent  = theParent
        self.theProject = theProject
        self.theTheme   = theParent.theTheme
        self.optState   = theProject.optState

        self.logData    = []
        self.filterData = []
        self.timeFilter = 0.0
        self.wordOffset = 0

        self.setWindowTitle("Writing Statistics")
        self.setMinimumWidth(self.mainConf.pxInt(420))
        self.setMinimumHeight(self.mainConf.pxInt(400))
        self.resize(
            self.mainConf.pxInt(self.optState.getInt("GuiWritingStats", "winWidth",  550)),
            self.mainConf.pxInt(self.optState.getInt("GuiWritingStats", "winHeight", 500))
        )

        # List Box
        wCol0 = self.mainConf.pxInt(
            self.optState.getInt("GuiWritingStats", "widthCol0", 180)
        )
        wCol1 = self.mainConf.pxInt(
            self.optState.getInt("GuiWritingStats", "widthCol1", 80)
        )
        wCol2 = self.mainConf.pxInt(
            self.optState.getInt("GuiWritingStats", "widthCol2", 80)
        )

        self.listBox = QTreeWidget()
        self.listBox.setHeaderLabels(["Session Start", "Length", "Words", "Histogram"])
        self.listBox.setIndentation(0)
        self.listBox.setColumnWidth(self.C_TIME, wCol0)
        self.listBox.setColumnWidth(self.C_LENGTH, wCol1)
        self.listBox.setColumnWidth(self.C_COUNT, wCol2)

        hHeader = self.listBox.headerItem()
        hHeader.setTextAlignment(self.C_LENGTH, Qt.AlignRight)
        hHeader.setTextAlignment(self.C_COUNT, Qt.AlignRight)

        sortValid = (Qt.AscendingOrder, Qt.DescendingOrder)
        sortCol = self.optState.validIntRange(
            self.optState.getInt("GuiWritingStats", "sortCol", 0), 0, 2, 0
        )
        sortOrder = self.optState.validIntTuple(
            self.optState.getInt("GuiWritingStats", "sortOrder", Qt.DescendingOrder),
            sortValid, Qt.DescendingOrder
        )
        self.listBox.sortByColumn(sortCol, sortOrder)
        self.listBox.setSortingEnabled(True)

        # Word Bar
        self.barHeight = int(round(0.5*self.theTheme.fontPixelSize))
        self.barWidth = self.mainConf.pxInt(200)
        self.barImage = QPixmap(self.barHeight, self.barHeight)
        self.barImage.fill(self.palette().highlight().color())

        # Session Info
        self.infoBox  = QGroupBox("Sum Totals", self)
        self.infoForm = QGridLayout(self)
        self.infoBox.setLayout(self.infoForm)

        self.labelTotal = QLabel(formatTime(0))
        self.labelTotal.setFont(self.theTheme.guiFontFixed)
        self.labelTotal.setAlignment(Qt.AlignVCenter | Qt.AlignRight)

        self.labelFilter = QLabel(formatTime(0))
        self.labelFilter.setFont(self.theTheme.guiFontFixed)
        self.labelFilter.setAlignment(Qt.AlignVCenter | Qt.AlignRight)

        self.novelWords = QLabel("0")
        self.novelWords.setFont(self.theTheme.guiFontFixed)
        self.novelWords.setAlignment(Qt.AlignVCenter | Qt.AlignRight)

        self.notesWords = QLabel("0")
        self.notesWords.setFont(self.theTheme.guiFontFixed)
        self.notesWords.setAlignment(Qt.AlignVCenter | Qt.AlignRight)

        self.totalWords = QLabel("0")
        self.totalWords.setFont(self.theTheme.guiFontFixed)
        self.totalWords.setAlignment(Qt.AlignVCenter | Qt.AlignRight)

        self.infoForm.addWidget(QLabel("Total Time:"),       0, 0)
        self.infoForm.addWidget(QLabel("Filtered Time:"),    1, 0)
        self.infoForm.addWidget(QLabel("Novel Word Count:"), 2, 0)
        self.infoForm.addWidget(QLabel("Notes Word Count:"), 3, 0)
        self.infoForm.addWidget(QLabel("Total Word Count:"), 4, 0)
        self.infoForm.addWidget(self.labelTotal,  0, 1)
        self.infoForm.addWidget(self.labelFilter, 1, 1)
        self.infoForm.addWidget(self.novelWords,  2, 1)
        self.infoForm.addWidget(self.notesWords,  3, 1)
        self.infoForm.addWidget(self.totalWords,  4, 1)
        self.infoForm.setRowStretch(5, 1)

        # Filter Options
        sPx = self.theTheme.baseIconSize

        self.filterBox  = QGroupBox("Filters", self)
        self.filterForm = QGridLayout(self)
        self.filterBox.setLayout(self.filterForm)

        self.incNovel = QSwitch(width=2*sPx, height=sPx)
        self.incNovel.setChecked(
            self.optState.getBool("GuiWritingStats", "incNovel", True)
        )
        self.incNovel.clicked.connect(self._updateListBox)

        self.incNotes = QSwitch(width=2*sPx, height=sPx)
        self.incNotes.setChecked(
            self.optState.getBool("GuiWritingStats", "incNotes", True)
        )
        self.incNotes.clicked.connect(self._updateListBox)

        self.hideZeros = QSwitch(width=2*sPx, height=sPx)
        self.hideZeros.setChecked(
            self.optState.getBool("GuiWritingStats", "hideZeros", True)
        )
        self.hideZeros.clicked.connect(self._updateListBox)

        self.hideNegative = QSwitch(width=2*sPx, height=sPx)
        self.hideNegative.setChecked(
            self.optState.getBool("GuiWritingStats", "hideNegative", False)
        )
        self.hideNegative.clicked.connect(self._updateListBox)

        self.groupByDay = QSwitch(width=2*sPx, height=sPx)
        self.groupByDay.setChecked(
            self.optState.getBool("GuiWritingStats", "groupByDay", False)
        )
        self.groupByDay.clicked.connect(self._updateListBox)

        self.filterForm.addWidget(QLabel("Count novel files"),        0, 0)
        self.filterForm.addWidget(QLabel("Count note files"),         1, 0)
        self.filterForm.addWidget(QLabel("Hide zero word count"),     2, 0)
        self.filterForm.addWidget(QLabel("Hide negative word count"), 3, 0)
        self.filterForm.addWidget(QLabel("Group entries by day"),     4, 0)
        self.filterForm.addWidget(self.incNovel,     0, 1)
        self.filterForm.addWidget(self.incNotes,     1, 1)
        self.filterForm.addWidget(self.hideZeros,    2, 1)
        self.filterForm.addWidget(self.hideNegative, 3, 1)
        self.filterForm.addWidget(self.groupByDay,   4, 1)
        self.filterForm.setRowStretch(5, 1)

        # Settings
        self.histMax = QSpinBox(self)
        self.histMax.setMinimum(100)
        self.histMax.setMaximum(100000)
        self.histMax.setSingleStep(100)
        self.histMax.setValue(
            self.optState.getInt("GuiWritingStats", "histMax", 2000)
        )
        self.histMax.valueChanged.connect(self._updateListBox)

        self.optsBox = QHBoxLayout()
        self.optsBox.addStretch(1)
        self.optsBox.addWidget(QLabel("Word count cap for the histogram"), 0)
        self.optsBox.addWidget(self.histMax, 0)

        # Buttons
        self.buttonBox = QDialogButtonBox()
        self.buttonBox.rejected.connect(self._doClose)

        self.btnClose = self.buttonBox.addButton(QDialogButtonBox.Close)
        self.btnClose.setAutoDefault(False)

        self.btnSave = self.buttonBox.addButton("Save As", QDialogButtonBox.ActionRole)
        self.btnSave.setAutoDefault(False)

        self.saveMenu = QMenu(self)
        self.btnSave.setMenu(self.saveMenu)

        self.saveJSON = QAction("JSON Data File (.json)", self)
        self.saveJSON.triggered.connect(lambda: self._saveData(self.FMT_JSON))
        self.saveMenu.addAction(self.saveJSON)

        self.saveCSV = QAction("CSV Data File (.csv)", self)
        self.saveCSV.triggered.connect(lambda: self._saveData(self.FMT_CSV))
        self.saveMenu.addAction(self.saveCSV)

        # Assemble
        self.outerBox = QGridLayout()
        self.outerBox.addWidget(self.listBox,   0, 0, 1, 2)
        self.outerBox.addLayout(self.optsBox,   1, 0, 1, 2)
        self.outerBox.addWidget(self.infoBox,   2, 0)
        self.outerBox.addWidget(self.filterBox, 2, 1)
        self.outerBox.addWidget(self.buttonBox, 3, 0, 1, 2)
        self.outerBox.setRowStretch(0, 1)

        self.setLayout(self.outerBox)

        logger.debug("GuiWritingStats initialisation complete")

        return

    def populateGUI(self):
        """Populate list box with data from the log file.
        """
        qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
        self._loadLogFile()
        self._updateListBox()
        qApp.restoreOverrideCursor()
        return

    ##
    #  Slots
    ##

    def _doClose(self):
        """Save the state of the window, clear cache, end close.
        """
        self.logData = []

        winWidth     = self.mainConf.rpxInt(self.width())
        winHeight    = self.mainConf.rpxInt(self.height())
        widthCol0    = self.mainConf.rpxInt(self.listBox.columnWidth(0))
        widthCol1    = self.mainConf.rpxInt(self.listBox.columnWidth(1))
        widthCol2    = self.mainConf.rpxInt(self.listBox.columnWidth(2))
        sortCol      = self.listBox.sortColumn()
        sortOrder    = self.listBox.header().sortIndicatorOrder()
        incNovel     = self.incNovel.isChecked()
        incNotes     = self.incNotes.isChecked()
        hideZeros    = self.hideZeros.isChecked()
        hideNegative = self.hideNegative.isChecked()
        groupByDay   = self.groupByDay.isChecked()
        histMax      = self.histMax.value()

        self.optState.setValue("GuiWritingStats", "winWidth",     winWidth)
        self.optState.setValue("GuiWritingStats", "winHeight",    winHeight)
        self.optState.setValue("GuiWritingStats", "widthCol0",    widthCol0)
        self.optState.setValue("GuiWritingStats", "widthCol1",    widthCol1)
        self.optState.setValue("GuiWritingStats", "widthCol2",    widthCol2)
        self.optState.setValue("GuiWritingStats", "sortCol",      sortCol)
        self.optState.setValue("GuiWritingStats", "sortOrder",    sortOrder)
        self.optState.setValue("GuiWritingStats", "incNovel",     incNovel)
        self.optState.setValue("GuiWritingStats", "incNotes",     incNotes)
        self.optState.setValue("GuiWritingStats", "hideZeros",    hideZeros)
        self.optState.setValue("GuiWritingStats", "hideNegative", hideNegative)
        self.optState.setValue("GuiWritingStats", "groupByDay",   groupByDay)
        self.optState.setValue("GuiWritingStats", "histMax",      histMax)

        self.optState.saveSettings()
        self.close()

        return

    def _saveData(self, dataFmt):
        """Save the content of the list box to a file.
        """
        fileExt = ""
        textFmt = ""

        if dataFmt == self.FMT_JSON:
            fileExt = "json"
            textFmt = "JSON Data File"
        elif dataFmt == self.FMT_CSV:
            fileExt = "csv"
            textFmt = "CSV Data File"
        else:
            return False

        # Generate the file name
        saveDir = self.mainConf.lastPath
        if not os.path.isdir(saveDir):
            saveDir = os.path.expanduser("~")

        fileName = "sessionStats.%s" % fileExt
        savePath = os.path.join(saveDir, fileName)

        dlgOpt  = QFileDialog.Options()
        dlgOpt |= QFileDialog.DontUseNativeDialog
        savePath, _ = QFileDialog.getSaveFileName(
            self, "Save Document As", savePath, options=dlgOpt
        )
        if not savePath:
            return False

        self.mainConf.setLastPath(savePath)

        # Do the actual writing
        wSuccess = False
        errMsg = ""

        try:
            with open(savePath, mode="w", encoding="utf8") as outFile:
                if dataFmt == self.FMT_JSON:
                    jsonData = []
                    for _, sD, tT, wD, wA, wB in self.filterData:
                        jsonData.append({
                            "date": sD,
                            "length": tT,
                            "newWords": wD,
                            "novelWords": wA,
                            "noteWords": wB,
                        })
                    json.dump(jsonData, outFile, indent=2)
                    wSuccess = True

                if dataFmt == self.FMT_CSV:
                    outFile.write(
                        '"Date","Length (sec)","Words Changed","Novel Words","Note Words"\n'
                    )
                    for _, sD, tT, wD, wA, wB in self.filterData:
                        outFile.write(f'"{sD}",{tT:.0f},{wD},{wA},{wB}\n')
                    wSuccess = True

        except Exception as e:
            errMsg = str(e)
            wSuccess = False

        # Report to user
        if wSuccess:
            self.theParent.makeAlert(
                "%s file successfully written to:<br>%s" % (
                    textFmt, savePath
                ), nwAlert.INFO
            )
        else:
            self.theParent.makeAlert(
                "Failed to write %s file.<br>%s" % (
                    textFmt, errMsg
                ), nwAlert.ERROR
            )

        return wSuccess

    ##
    #  Internal Functions
    ##

    def _loadLogFile(self):
        """Load the content of the log file into a buffer.
        """
        logger.debug("Loading session log file")

        self.logData = []
        self.wordOffset = 0

        ttNovel = 0
        ttNotes = 0
        ttTime  = 0

        logFile = os.path.join(self.theProject.projMeta, nwFiles.SESS_STATS)
        if not os.path.isfile(logFile):
            logger.info("This project has no writing stats logfile")
            return False

        try:
            with open(logFile, mode="r", encoding="utf8") as inFile:
                for inLine in inFile:
                    if inLine.startswith("#"):
                        if inLine.startswith("# Offset"):
                            self.wordOffset = checkInt(inLine[9:].strip(), 0)
                            logger.verbose(
                                "Initial word count when log was started is %d" % self.wordOffset
                            )
                        continue

                    inData = inLine.split()
                    if len(inData) != 6:
                        continue

                    dStart = datetime.strptime(
                        "%s %s" % (inData[0], inData[1]), nwConst.FMT_TSTAMP
                    )
                    dEnd = datetime.strptime(
                        "%s %s" % (inData[2], inData[3]), nwConst.FMT_TSTAMP
                    )

                    tDiff = dEnd - dStart
                    sDiff = tDiff.total_seconds()
                    ttTime += sDiff

                    wcNovel = int(inData[4])
                    wcNotes = int(inData[5])
                    ttNovel = wcNovel
                    ttNotes = wcNotes

                    self.logData.append((dStart, sDiff, wcNovel, wcNotes))

        except Exception as e:
            self.theParent.makeAlert(
                ["Failed to read session log file.", str(e)], nwAlert.ERROR
            )
            return False

        ttWords = ttNovel + ttNotes
        self.labelTotal.setText(formatTime(round(ttTime)))
        self.novelWords.setText(f"{ttNovel:n}")
        self.notesWords.setText(f"{ttNotes:n}")
        self.totalWords.setText(f"{ttWords:n}")

        return True

    def _updateListBox(self, dummyVar=None):
        """Load/reload the content of the list box. The dummyVar
        variable captures the variable sent from the widgets connecting
        to it and discards it.
        """
        self.listBox.clear()
        self.timeFilter = 0.0

        incNovel     = self.incNovel.isChecked()
        incNotes     = self.incNotes.isChecked()
        hideZeros    = self.hideZeros.isChecked()
        hideNegative = self.hideNegative.isChecked()
        groupByDay   = self.groupByDay.isChecked()
        histMax      = self.histMax.value()

        # Group the data
        if groupByDay:
            tempData = []
            sessDate = None
            sessTime = 0
            lstNovel = 0
            lstNotes = 0

            for n, (dStart, sDiff, wcNovel, wcNotes) in enumerate(self.logData):
                if n == 0:
                    sessDate = dStart.date()
                if sessDate != dStart.date():
                    tempData.append((sessDate, sessTime, lstNovel, lstNotes))
                    sessDate = dStart.date()
                    sessTime = sDiff
                    lstNovel = wcNovel
                    lstNotes = wcNotes
                else:
                    sessTime += sDiff
                    lstNovel = wcNovel
                    lstNotes = wcNotes

            if sessDate is not None:
                tempData.append((sessDate, sessTime, lstNovel, lstNotes))

        else:
            tempData = self.logData

        # Calculate Word Diff
        self.filterData = []
        pcTotal = 0
        listMax = 0
        isFirst = True
        for dStart, sDiff, wcNovel, wcNotes in tempData:

            wcTotal = 0
            if incNovel:
                wcTotal += wcNovel
            if incNotes:
                wcTotal += wcNotes

            dwTotal = wcTotal - pcTotal
            if hideZeros and dwTotal == 0:
                continue
            if hideNegative and dwTotal < 0:
                pcTotal = wcTotal
                continue

            if isFirst:
                # Subtract the offset from the first list entry
                dwTotal -= self.wordOffset
                dwTotal = max(dwTotal, 1) # Don't go zero or negative
                isFirst = False

            if groupByDay:
                sStart = dStart.strftime(nwConst.FMT_DSTAMP)
            else:
                sStart = dStart.strftime(nwConst.FMT_TSTAMP)

            self.filterData.append((dStart, sStart, sDiff, dwTotal, wcNovel, wcNotes))
            listMax = min(max(listMax, dwTotal), histMax)
            pcTotal = wcTotal

        # Populate the list
        for _, sStart, sDiff, nWords, _, _ in self.filterData:

            newItem = QTreeWidgetItem()
            newItem.setText(self.C_TIME, sStart)
            newItem.setText(self.C_LENGTH, formatTime(round(sDiff)))
            newItem.setText(self.C_COUNT, f"{nWords:n}")

            if nWords > 0 and listMax > 0:
                theBar = self.barImage.scaled(
                    int(200*min(nWords, histMax)/listMax),
                    self.barHeight,
                    Qt.IgnoreAspectRatio,
                    Qt.FastTransformation
                )
                newItem.setData(self.C_BAR, Qt.DecorationRole, theBar)

            newItem.setTextAlignment(self.C_LENGTH, Qt.AlignRight)
            newItem.setTextAlignment(self.C_COUNT, Qt.AlignRight)
            newItem.setTextAlignment(self.C_BAR, Qt.AlignLeft | Qt.AlignVCenter)

            newItem.setFont(self.C_TIME, self.theTheme.guiFontFixed)
            newItem.setFont(self.C_LENGTH, self.theTheme.guiFontFixed)
            newItem.setFont(self.C_COUNT, self.theTheme.guiFontFixed)

            self.listBox.addTopLevelItem(newItem)
            self.timeFilter += sDiff

        self.labelFilter.setText(formatTime(round(self.timeFilter)))

        return True
Beispiel #4
0
class SnapshotRestoreFileSelector(QWidget):
    """
    Widget for visual representation (and selection) of existing saved_value
    files.
    """

    files_selected = QtCore.pyqtSignal(list)
    files_updated = QtCore.pyqtSignal(dict)

    def __init__(self, snapshot, common_settings, parent=None, **kw):
        QWidget.__init__(self, parent, **kw)

        self.snapshot = snapshot
        self.selected_files = list()
        self.common_settings = common_settings

        self.file_list = dict()
        self.pvs = dict()

        # Filter handling
        self.file_filter = dict()
        self.file_filter["keys"] = list()
        self.file_filter["comment"] = ""

        self.filter_input = SnapshotFileFilterWidget(self.common_settings,
                                                     self)

        self.filter_input.file_filter_updated.connect(
            self.filter_file_list_selector)

        # Create list with: file names, comment, labels, machine params.
        # This is done with a single-level QTreeWidget instead of QTableWidget
        # because it is line-oriented whereas a table is cell-oriented.
        self.file_selector = QTreeWidget(self)
        self.file_selector.setRootIsDecorated(False)
        self.file_selector.setUniformRowHeights(True)
        self.file_selector.setIndentation(0)
        self.file_selector.setColumnCount(FileSelectorColumns.params)
        self.column_labels = ["File name", "Comment", "Labels"]
        self.file_selector.setHeaderLabels(self.column_labels)
        self.file_selector.setAllColumnsShowFocus(True)
        self.file_selector.setSortingEnabled(True)
        # Sort by file name (alphabetical order)
        self.file_selector.sortItems(FileSelectorColumns.filename,
                                     Qt.DescendingOrder)

        self.file_selector.itemSelectionChanged.connect(self.select_files)
        self.file_selector.setContextMenuPolicy(Qt.CustomContextMenu)
        self.file_selector.customContextMenuRequested.connect(self.open_menu)

        # Set column sizes
        self.file_selector.resizeColumnToContents(FileSelectorColumns.filename)
        self.file_selector.setColumnWidth(FileSelectorColumns.comment, 350)

        # Applies following behavior for multi select:
        #   click            selects only current file
        #   Ctrl + click     adds current file to selected files
        #   Shift + click    adds all files between last selected and current
        #                    to selected
        self.file_selector.setSelectionMode(QTreeWidget.ExtendedSelection)

        self.filter_file_list_selector()

        # Add to main layout
        layout = QVBoxLayout(self)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.addWidget(self.filter_input)
        layout.addWidget(self.file_selector)

    def handle_new_snapshot_instance(self, snapshot):
        self.clear_file_selector()
        self.filter_input.clear()
        self.snapshot = snapshot

    def rebuild_file_list(self, already_parsed_files=None):
        background_workers.suspend()
        self.clear_file_selector()
        self.file_selector.setSortingEnabled(False)
        if already_parsed_files:
            save_files, err_to_report = already_parsed_files
        else:
            save_dir = self.common_settings["save_dir"]
            req_file_path = self.common_settings["req_file_path"]
            save_files, err_to_report = get_save_files(save_dir, req_file_path)

        self._update_file_list_selector(save_files)
        self.filter_file_list_selector()

        # Report any errors with snapshot files to the user
        if err_to_report:
            show_snapshot_parse_errors(self, err_to_report)

        self.file_selector.setSortingEnabled(True)
        self.files_updated.emit(save_files)
        background_workers.resume()

    def _update_file_list_selector(self, file_list):
        new_labels = set()
        new_params = set()
        for new_file, new_data in file_list.items():
            meta_data = new_data["meta_data"]
            labels = meta_data.get("labels", [])
            params = meta_data.get("machine_params", {})

            assert (new_file not in self.file_list)
            new_labels.update(labels)
            new_params.update(params.keys())

        new_labels = list(new_labels)
        new_params = list(new_params)
        defined_params = list(self.common_settings['machine_params'].keys())
        all_params = defined_params + \
            [p for p in new_params if p not in defined_params]

        for new_file, new_data in file_list.items():
            meta_data = new_data["meta_data"]
            labels = meta_data.get("labels", [])
            params = meta_data.get("machine_params", {})
            comment = meta_data.get("comment", "")

            row = [new_file, comment, " ".join(labels)]
            assert (len(row) == FileSelectorColumns.params)
            param_vals = [None] * len(all_params)
            for p, v in params.items():
                string = SnapshotPv.value_to_display_str(
                    v['value'],
                    v['precision'] if v['precision'] is not None else 0)
                idx = all_params.index(p)
                param_vals[idx] = string
            selector_item = QTreeWidgetItem(row + param_vals)
            self.file_selector.addTopLevelItem(selector_item)
            self.file_list[new_file] = new_data
            self.file_list[new_file]["file_selector"] = selector_item

        self.common_settings["existing_labels"] = new_labels
        self.common_settings["existing_params"] = new_params
        self.filter_input.update_params()

        # Add units to column headers; get units from the latest file that has
        # them.
        params_mtimes = [(data['meta_data']['machine_params'],
                          data['modif_time']) for data in file_list.values()]
        params_mtimes.sort(key=lambda d: d[1], reverse=True)
        for i, p in enumerate(all_params):
            for file_params, _ in params_mtimes:
                if file_params.get(p, {}).get('units', None):
                    all_params[i] += f" ({file_params[p]['units']})"
                    break

        self.file_selector.setHeaderLabels(self.column_labels + all_params)
        for col in range(self.file_selector.columnCount()):
            self.file_selector.resizeColumnToContents(col)

        # There can be some rather long comments in the snapshots, so let's
        # make sure that they don't push out more useful stuff.
        if self.file_selector.columnWidth(FileSelectorColumns.comment) \
           > self.file_selector.columnWidth(FileSelectorColumns.filename):
            self.file_selector.setColumnWidth(
                FileSelectorColumns.comment,
                self.file_selector.columnWidth(FileSelectorColumns.filename))

    def filter_file_list_selector(self):
        file_filter = self.filter_input.file_filter

        def ensure_nums_or_strings(*vals):
            """Variables have to be all numbers or all strings. If this is not
            the case, convert everything to strings."""
            if not all((isinstance(x, (int, float)) for x in vals)):
                return tuple((str(x) for x in vals))
            return vals

        def check_params(params_filter, file_params):
            """
            file_params is a dict of machine params and their data (being a
            dict containing 'value' and 'precision').
            params_filter is a dict of machine params and corresponding lists.
            These lists have either one or two elements, causing either an
            equality or in-range check.

            Returns True if all checks pass.
            """
            for p, vals in params_filter.items():
                if p not in file_params:
                    return False
                if len(vals) == 1:
                    v1 = vals[0]
                    v2 = file_params[p]['value']
                    v1, v2 = ensure_nums_or_strings(v1, v2)
                    if isinstance(v2, float):
                        # If precision is defined, compare with tolerance.
                        # The default precision is 6, which matches string
                        # formatting behaviour. It makes no sense to do
                        # comparison to a higher precision than what the user
                        # can see.
                        prec = file_params[p]['precision']
                        tol = 10**(-prec) if (prec and prec > 0) else 10**-6
                        if abs(v1 - v2) > tol:
                            return False
                    else:
                        if v1 != v2:
                            return False

                elif len(vals) == 2:
                    vals = ensure_nums_or_strings(*vals)
                    low = min(vals)
                    high = max(vals)
                    v = file_params[p]['value']
                    v, low, high = ensure_nums_or_strings(v, low, high)
                    if not (v >= low and v <= high):
                        return False
            return True

        for file_name in self.file_list:
            file_line = self.file_list[file_name]["file_selector"]
            file_to_filter = self.file_list.get(file_name)

            if not file_filter:
                file_line.setHidden(False)
            else:
                keys_filter = file_filter.get("keys")
                comment_filter = file_filter.get("comment")
                name_filter = file_filter.get("name")
                params_filter = file_filter.get("params")

                if keys_filter:
                    keys_status = False
                    for key in file_to_filter["meta_data"]["labels"]:
                        # Break when first found
                        if key and (key in keys_filter):
                            keys_status = True
                            break
                else:
                    keys_status = True

                if comment_filter:
                    comment_status = comment_filter in file_to_filter[
                        "meta_data"]["comment"]
                else:
                    comment_status = True

                if name_filter:
                    name_status = name_filter in file_name
                else:
                    name_status = True

                params_status = True
                if params_filter:
                    params_status = check_params(
                        params_filter,
                        file_to_filter['meta_data']['machine_params'])

                # Set visibility if any of the filters conditions met
                file_line.setHidden(not (name_status and keys_status
                                         and comment_status and params_status))

    def open_menu(self, point):
        item_idx = self.file_selector.indexAt(point)
        if not item_idx.isValid():
            return

        text = item_idx.data()
        field = self.file_selector.model().headerData(item_idx.column(),
                                                      Qt.Horizontal)
        clipboard = QGuiApplication.clipboard()

        menu = QMenu(self)
        if item_idx.column() < FileSelectorColumns.params:
            menu.addAction(f"Copy {field.lower()}",
                           lambda: clipboard.setText(text))
        else:
            # Machine param fields end with the unit in parentheses which needs
            # to be stripped to recognize them.
            try:
                param_name = field[:field.rindex('(')].rstrip()
            except ValueError:
                param_name = field

            menu.addAction(f"Copy {param_name} name",
                           lambda: clipboard.setText(param_name))
            menu.addAction(f"Copy {param_name} value",
                           lambda: clipboard.setText(text))
            if param_name in self.common_settings['machine_params']:
                pv_name = self.common_settings['machine_params'][param_name]
                menu.addAction(f"Copy {param_name} PV name",
                               lambda: clipboard.setText(pv_name))

        menu.addAction("Delete selected files", self.delete_files)
        menu.addAction("Edit file meta-data", self.update_file_metadata)

        menu.exec(QCursor.pos())
        menu.deleteLater()

    def select_files(self):
        # Pre-process selected items, to a list of files
        self.selected_files = list()
        if self.file_selector.selectedItems():
            for item in self.file_selector.selectedItems():
                self.selected_files.append(
                    item.text(FileSelectorColumns.filename))

        self.files_selected.emit(self.selected_files)

    def delete_files(self):
        if self.selected_files:
            msg = "Do you want to delete selected files?"
            reply = QMessageBox.question(self, 'Message', msg, QMessageBox.Yes,
                                         QMessageBox.No)
            if reply == QMessageBox.Yes:
                background_workers.suspend()
                symlink_file = self.common_settings["save_file_prefix"] \
                    + 'latest' + save_file_suffix
                symlink_path = os.path.join(self.common_settings["save_dir"],
                                            symlink_file)
                symlink_target = os.path.realpath(symlink_path)

                files = self.selected_files[:]
                paths = [
                    os.path.join(self.common_settings["save_dir"],
                                 selected_file)
                    for selected_file in self.selected_files
                ]

                if any((path == symlink_target for path in paths)) \
                   and symlink_file not in files:
                    files.append(symlink_file)
                    paths.append(symlink_path)

                for selected_file, file_path in zip(files, paths):
                    try:
                        os.remove(file_path)
                        self.file_list.pop(selected_file)
                        self.pvs = dict()
                        items = self.file_selector.findItems(
                            selected_file, Qt.MatchCaseSensitive,
                            FileSelectorColumns.filename)
                        self.file_selector.takeTopLevelItem(
                            self.file_selector.indexOfTopLevelItem(items[0]))

                    except OSError as e:
                        warn = "Problem deleting file:\n" + str(e)
                        QMessageBox.warning(self, "Warning", warn,
                                            QMessageBox.Ok,
                                            QMessageBox.NoButton)
                self.files_updated.emit(self.file_list)
                background_workers.resume()

    def update_file_metadata(self):
        if self.selected_files:
            if len(self.selected_files) == 1:
                settings_window = SnapshotEditMetadataDialog(
                    self.file_list.get(self.selected_files[0])["meta_data"],
                    self.common_settings, self)
                settings_window.resize(800, 200)
                # if OK was pressed, update actual file and reflect changes in the list
                if settings_window.exec_():
                    background_workers.suspend()
                    file_data = self.file_list.get(self.selected_files[0])
                    try:
                        self.snapshot.replace_metadata(file_data['file_path'],
                                                       file_data['meta_data'])
                    except OSError as e:
                        warn = "Problem modifying file:\n" + str(e)
                        QMessageBox.warning(self, "Warning", warn,
                                            QMessageBox.Ok,
                                            QMessageBox.NoButton)

                    self.rebuild_file_list()
                    background_workers.resume()
            else:
                QMessageBox.information(self, "Information",
                                        "Please select one file only",
                                        QMessageBox.Ok, QMessageBox.NoButton)

    def clear_file_selector(self):
        self.file_selector.clear(
        )  # Clears and "deselects" itmes on file selector
        self.select_files()  # Process new,empty list of selected files
        self.pvs = dict()
        self.file_list = dict()
Beispiel #5
0
class GuiSessionLogView(QDialog):

    def __init__(self, theParent, theProject):
        QDialog.__init__(self, theParent)

        logger.debug("Initialising SessionLogView ...")

        self.mainConf   = nw.CONFIG
        self.theProject = theProject
        self.theParent  = theParent
        self.optState   = SessionLogLastState(self.theProject,nwFiles.SLOG_OPT)
        self.optState.loadSettings()

        self.timeFilter = 0.0
        self.timeTotal  = 0.0

        self.outerBox  = QGridLayout()
        self.bottomBox = QHBoxLayout()

        self.setWindowTitle("Session Log")
        self.setMinimumWidth(420)
        self.setMinimumHeight(400)

        widthCol0 = self.optState.validIntRange(
            self.optState.getSetting("widthCol0"), 30, 999, 180
        )
        widthCol1 = self.optState.validIntRange(
            self.optState.getSetting("widthCol1"), 30, 999, 80
        )
        widthCol2 = self.optState.validIntRange(
            self.optState.getSetting("widthCol2"), 30, 999, 80
        )

        self.listBox = QTreeWidget()
        self.listBox.setHeaderLabels(["Session Start","Length","Words",""])
        self.listBox.setIndentation(0)
        self.listBox.setColumnWidth(0,widthCol0)
        self.listBox.setColumnWidth(1,widthCol1)
        self.listBox.setColumnWidth(2,widthCol2)
        self.listBox.setColumnWidth(3,0)

        hHeader = self.listBox.headerItem()
        hHeader.setTextAlignment(1,Qt.AlignRight)
        hHeader.setTextAlignment(2,Qt.AlignRight)

        self.monoFont = QFont("Monospace",10)

        sortValid = (Qt.AscendingOrder, Qt.DescendingOrder)
        sortCol = self.optState.validIntRange(
            self.optState.getSetting("sortCol"), 0, 2, 0
        )
        sortOrder = self.optState.validIntTuple(
            self.optState.getSetting("sortOrder"), sortValid, Qt.DescendingOrder
        )

        self.listBox.sortByColumn(sortCol, sortOrder)
        self.listBox.setSortingEnabled(True)

        # Session Info
        self.infoBox     = QGroupBox("Sum Total Time", self)
        self.infoBoxForm = QGridLayout(self)
        self.infoBox.setLayout(self.infoBoxForm)

        self.labelTotal = QLabel(self._formatTime(0))
        self.labelTotal.setFont(self.monoFont)
        self.labelTotal.setAlignment(Qt.AlignVCenter | Qt.AlignRight)

        self.labelFilter = QLabel(self._formatTime(0))
        self.labelFilter.setFont(self.monoFont)
        self.labelFilter.setAlignment(Qt.AlignVCenter | Qt.AlignRight)

        self.infoBoxForm.addWidget(QLabel("All:"),      0, 0)
        self.infoBoxForm.addWidget(self.labelTotal,     0, 1)
        self.infoBoxForm.addWidget(QLabel("Filtered:"), 1, 0)
        self.infoBoxForm.addWidget(self.labelFilter,    1, 1)

        # Filter Options
        self.filterBox     = QGroupBox("Filters", self)
        self.filterBoxForm = QGridLayout(self)
        self.filterBox.setLayout(self.filterBoxForm)

        self.hideZeros = QCheckBox("Hide zero word count", self)
        self.hideZeros.setChecked(self.optState.getSetting("hideZeros"))
        self.hideZeros.stateChanged.connect(self._doHideZeros)

        self.hideNegative = QCheckBox("Hide negative word count", self)
        self.hideNegative.setChecked(self.optState.getSetting("hideNegative"))
        self.hideNegative.stateChanged.connect(self._doHideNegative)

        self.filterBoxForm.addWidget(self.hideZeros,    0, 0)
        self.filterBoxForm.addWidget(self.hideNegative, 1, 0)

        # Buttons
        self.buttonBox = QDialogButtonBox(QDialogButtonBox.Close)
        self.buttonBox.rejected.connect(self._doClose)

        # Assemble
        self.outerBox.addWidget(self.listBox,   0, 0, 1, 2)
        self.outerBox.addWidget(self.infoBox,   1, 0)
        self.outerBox.addWidget(self.filterBox, 1, 1)
        self.outerBox.addWidget(self.buttonBox, 2, 0, 1, 2)

        self.setLayout(self.outerBox)

        self.show()

        logger.debug("SessionLogView initialisation complete")

        self._loadSessionLog()

        return

    def _loadSessionLog(self):

        logFile = path.join(self.theProject.projMeta, nwFiles.SESS_INFO)
        if not path.isfile(logFile):
            logger.warning("No session log file found for this project.")
            return False

        self.listBox.clear()

        self.timeFilter = 0.0
        self.timeTotal  = 0.0

        hideZeros    = self.hideZeros.isChecked()
        hideNegative = self.hideNegative.isChecked()

        logger.debug("Loading session log file")
        try:
            with open(logFile,mode="r",encoding="utf8") as inFile:
                for inLine in inFile:
                    inData = inLine.split()
                    if len(inData) != 8:
                        continue
                    dStart = datetime.strptime("%s %s" % (inData[1],inData[2]),nwConst.tStampFmt)
                    dEnd   = datetime.strptime("%s %s" % (inData[4],inData[5]),nwConst.tStampFmt)
                    nWords = int(inData[7])
                    tDiff  = dEnd - dStart
                    sDiff  = tDiff.total_seconds()

                    self.timeTotal  += sDiff
                    if abs(nWords) > 0:
                        self.timeFilter += sDiff

                    if hideZeros and nWords == 0:
                        continue

                    if hideNegative and nWords < 0:
                        continue

                    newItem = QTreeWidgetItem([str(dStart),self._formatTime(sDiff),str(nWords),""])

                    newItem.setTextAlignment(1,Qt.AlignRight)
                    newItem.setTextAlignment(2,Qt.AlignRight)

                    newItem.setFont(0,self.monoFont)
                    newItem.setFont(1,self.monoFont)
                    newItem.setFont(2,self.monoFont)

                    self.listBox.addTopLevelItem(newItem)

        except Exception as e:
            self.theParent.makeAlert(["Failed to read session log file.",str(e)], nwAlert.ERROR)
            return False

        self.labelFilter.setText(self._formatTime(self.timeFilter))
        self.labelTotal.setText(self._formatTime(self.timeTotal))

        return True

    def _doClose(self):

        widthCol0    = self.listBox.columnWidth(0)
        widthCol1    = self.listBox.columnWidth(1)
        widthCol2    = self.listBox.columnWidth(2)
        sortCol      = self.listBox.sortColumn()
        sortOrder    = self.listBox.header().sortIndicatorOrder()
        hideZeros    = self.hideZeros.isChecked()
        hideNegative = self.hideNegative.isChecked()

        self.optState.setSetting("widthCol0",   widthCol0)
        self.optState.setSetting("widthCol1",   widthCol1)
        self.optState.setSetting("widthCol2",   widthCol2)
        self.optState.setSetting("sortCol",     sortCol)
        self.optState.setSetting("sortOrder",   sortOrder)
        self.optState.setSetting("hideZeros",   hideZeros)
        self.optState.setSetting("hideNegative",hideNegative)

        self.optState.saveSettings()
        self.close()

        return

    def _doHideZeros(self, newState):
        self._loadSessionLog()
        return

    def _doHideNegative(self, newState):
        self._loadSessionLog()
        return

    def _formatTime(self, tS):
        tM = int(tS/60)
        tH = int(tM/60)
        tM = tM - tH*60
        tS = tS - tM*60 - tH*3600
        return "%02d:%02d:%02d" % (tH,tM,tS)
Beispiel #6
0
class tree(QWidget):
	(FILE,FILE_PATH,H5GROUP) = range(3)
	def __init__(self):
		super().__init__()
		self.title = 'Tree of h5 data'
		self.left = 10
		self.top = 10
		self.width = 720
		self.height = 640
		
		self.setWindowTitle(self.title)
		self.setGeometry(self.left,self.top,self.width,self.height)
		
		self.datalayout= QVBoxLayout()
		self.open_button = self.add_open_button()
		self.datalayout.addWidget(self.open_button)
		self.setLayout(self.datalayout)
		self.show()
		self.tree = QTreeWidget()
		header = QTreeWidgetItem(['File','Type','H5 Keys'])
		self.tree.setHeaderItem(header)
		self.datalayout.addWidget(self.tree)
	
	def add_open_button(self):
		open_file_btn = QPushButton('Open')
		open_file_btn.clicked.connect(self.add_file)
		#button_section =  QHBoxLayout()
		#button_section.addWidget(open_file_btn)
		return open_file_btn#button_section
		
	def add_file(self):	
		h5file = QFileDialog.getOpenFileName(self, 'Open file',
		'/home/yugang/Desktop/XPCS_GUI/TestData/test.h5', filter='*.hdf5 *.h5 *.lst')[0]
		self.f = h5py.File(h5file,'r')
		self.filename = self.f.filename.split('/')[-1]
		
		#self.model = QStandardItemModel(0,3)
		#self.model.setHeaderData(0,Qt.Horizontal,"File")
		#self.model.setHeaderData(1,Qt.Horizontal,"Type")
		#self.model.setHeaderData(2,Qt.Horizontal,"H5 keys")
		
		
		self.tree_root = QTreeWidgetItem(self.tree,[self.filename,'H5 File',''])
		self.tree.setColumnWidth(0,250)
		self.tree.setColumnWidth(1,100)
		self.tree.setColumnWidth(2,100)
		print(
			  self.tree.columnWidth(0),
			  self.tree.columnWidth(1),
			  self.tree.columnWidth(2))
		self.add_branch(self.tree_root,self.f)
		#self.tree.setModel(self.model)
		self.tree.itemClicked.connect(self.onItemClicked)
		
		#print(self.tree.currentItem())
		self.setLayout(self.datalayout)
		self.show()

	def add_branch(self,tree_root,h5file):
		for _ in h5file.keys():
			#print(self.tree.currentItem())
			branch = QTreeWidgetItem([str(h5file[_].name).split('/')[-1],
									  str(type(h5file[_])),
									  str(h5file[_].name)])
			tree_root.addChild(branch)
			if 	isinstance(h5file[_],h5py.Group):
				self.add_branch(branch,h5file[_])
	
	@pyqtSlot(QTreeWidgetItem,int)
	def onItemClicked(self,item):
		print(self.filename,item.text(2))
Beispiel #7
0
class GuiProjectLoad(QDialog):

    NONE_STATE = 0
    NEW_STATE = 1
    OPEN_STATE = 2

    C_NAME = 0
    C_COUNT = 1
    C_TIME = 2

    def __init__(self, theParent):
        QDialog.__init__(self, theParent)

        logger.debug("Initialising GuiProjectLoad ...")
        self.setObjectName("GuiProjectLoad")

        self.mainConf = nw.CONFIG
        self.theParent = theParent
        self.theTheme = theParent.theTheme
        self.openState = self.NONE_STATE
        self.openPath = None

        sPx = self.mainConf.pxInt(16)
        nPx = self.mainConf.pxInt(96)
        iPx = self.theTheme.baseIconSize

        self.outerBox = QVBoxLayout()
        self.innerBox = QHBoxLayout()
        self.outerBox.setSpacing(sPx)
        self.innerBox.setSpacing(sPx)

        self.setWindowTitle("Open Project")
        self.setMinimumWidth(self.mainConf.pxInt(650))
        self.setMinimumHeight(self.mainConf.pxInt(400))
        self.setModal(True)

        self.nwIcon = QLabel()
        self.nwIcon.setPixmap(
            self.theParent.theTheme.getPixmap("novelwriter", (nPx, nPx)))
        self.innerBox.addWidget(self.nwIcon, 0, Qt.AlignTop)

        self.projectForm = QGridLayout()
        self.projectForm.setContentsMargins(0, 0, 0, 0)

        self.listBox = QTreeWidget()
        self.listBox.setSelectionMode(QAbstractItemView.SingleSelection)
        self.listBox.setDragDropMode(QAbstractItemView.NoDragDrop)
        self.listBox.setColumnCount(3)
        self.listBox.setHeaderLabels(["Working Title", "Words", "Last Opened"])
        self.listBox.setRootIsDecorated(False)
        self.listBox.itemSelectionChanged.connect(self._doSelectRecent)
        self.listBox.itemDoubleClicked.connect(self._doOpenRecent)
        self.listBox.setIconSize(QSize(iPx, iPx))

        treeHead = self.listBox.headerItem()
        treeHead.setTextAlignment(self.C_COUNT, Qt.AlignRight)
        treeHead.setTextAlignment(self.C_TIME, Qt.AlignRight)

        self.lblRecent = QLabel("<b>Recently Opened Projects</b>")
        self.lblPath = QLabel("<b>Path</b>")
        self.selPath = QLineEdit("")
        self.selPath.setReadOnly(True)

        self.browseButton = QPushButton("...")
        self.browseButton.setMaximumWidth(
            int(2.5 * self.theTheme.getTextWidth("...")))
        self.browseButton.clicked.connect(self._doBrowse)

        self.projectForm.addWidget(self.lblRecent, 0, 0, 1, 3)
        self.projectForm.addWidget(self.listBox, 1, 0, 1, 3)
        self.projectForm.addWidget(self.lblPath, 2, 0, 1, 1)
        self.projectForm.addWidget(self.selPath, 2, 1, 1, 1)
        self.projectForm.addWidget(self.browseButton, 2, 2, 1, 1)
        self.projectForm.setColumnStretch(0, 0)
        self.projectForm.setColumnStretch(1, 1)
        self.projectForm.setColumnStretch(2, 0)
        self.projectForm.setVerticalSpacing(self.mainConf.pxInt(4))
        self.projectForm.setHorizontalSpacing(self.mainConf.pxInt(8))

        self.innerBox.addLayout(self.projectForm)

        self.buttonBox = QDialogButtonBox(QDialogButtonBox.Open
                                          | QDialogButtonBox.Cancel)
        self.buttonBox.accepted.connect(self._doOpenRecent)
        self.buttonBox.rejected.connect(self._doCancel)

        self.newButton = self.buttonBox.addButton("New",
                                                  QDialogButtonBox.ActionRole)
        self.newButton.clicked.connect(self._doNewProject)

        self.outerBox.addLayout(self.innerBox)
        self.outerBox.addWidget(self.buttonBox)
        self.setLayout(self.outerBox)

        self._populateList()
        self._doSelectRecent()

        keyDelete = QShortcut(self.listBox)
        keyDelete.setKey(QKeySequence(Qt.Key_Delete))
        keyDelete.activated.connect(self._keyPressDelete)

        logger.debug("GuiProjectLoad initialisation complete")

        return

    ##
    #  Buttons
    ##

    def _doOpenRecent(self):
        """Close the dialog window with a recent project selected.
        """
        logger.verbose("GuiProjectLoad open button clicked")
        self._saveSettings()

        selItems = self.listBox.selectedItems()
        if selItems:
            self.openPath = selItems[0].data(self.C_NAME, Qt.UserRole)
            self.openState = self.OPEN_STATE
            self.accept()
        else:
            self.openPath = None
            self.openState = self.NONE_STATE

        return

    def _doSelectRecent(self):
        """A recent item has been selected.
        """
        selList = self.listBox.selectedItems()
        if selList:
            self.selPath.setText(selList[0].data(self.C_NAME, Qt.UserRole))
        return

    def _doBrowse(self):
        """Browse for a folder path.
        """
        logger.verbose("GuiProjectLoad browse button clicked")
        dlgOpt = QFileDialog.Options()
        dlgOpt |= QFileDialog.DontUseNativeDialog
        projFile, _ = QFileDialog.getOpenFileName(
            self,
            "Open novelWriter Project",
            "",
            "novelWriter Project File (%s);;All Files (*)" % nwFiles.PROJ_FILE,
            options=dlgOpt)
        if projFile:
            thePath = os.path.abspath(os.path.dirname(projFile))
            self.selPath.setText(thePath)
            self.openPath = thePath
            self.openState = self.OPEN_STATE
            self.accept()

        return

    def _doCancel(self):
        """Close the dialog window without doing anything.
        """
        logger.verbose("GuiProjectLoad close button clicked")
        self.openPath = None
        self.openState = self.NONE_STATE
        self.close()
        return

    def _doNewProject(self):
        """Create a new project.
        """
        logger.verbose("GuiProjectLoad new project button clicked")
        self._saveSettings()
        self.openPath = None
        self.openState = self.NEW_STATE
        self.accept()
        return

    def _keyPressDelete(self):
        """Remove an entry from the recent projects list.
        """
        selList = self.listBox.selectedItems()
        if selList:
            msgBox = QMessageBox()
            msgRes = msgBox.question(
                self, "Remove Entry",
                "Remove the selected entry from the recent projects list?")
            if msgRes == QMessageBox.Yes:
                self.mainConf.removeFromRecentCache(selList[0].data(
                    self.C_NAME, Qt.UserRole))
                self._populateList()

        return

    ##
    #  Events
    ##

    def closeEvent(self, theEvent):
        """Capture the user closing the dialog so we can save settings.
        """
        self._saveSettings()
        theEvent.accept()
        return

    ##
    #  Internal Functions
    ##

    def _saveSettings(self):
        """Save the changes made to the dialog.
        """
        colWidths = [0, 0, 0]
        colWidths[self.C_NAME] = self.listBox.columnWidth(self.C_NAME)
        colWidths[self.C_COUNT] = self.listBox.columnWidth(self.C_COUNT)
        colWidths[self.C_TIME] = self.listBox.columnWidth(self.C_TIME)
        self.mainConf.setProjColWidths(colWidths)
        return

    def _populateList(self):
        """Populate the list box with recent project data.
        """
        listOrder = []
        listData = {}
        for projPath in self.mainConf.recentProj.keys():
            theEntry = self.mainConf.recentProj[projPath]
            theTitle = ""
            theTime = 0
            theWords = 0
            if "title" in theEntry.keys():
                theTitle = theEntry["title"]
            if "time" in theEntry.keys():
                theTime = theEntry["time"]
            if "words" in theEntry.keys():
                theWords = theEntry["words"]
            if theTime > 0:
                listOrder.append(theTime)
                listData[theTime] = [theTitle, theWords, projPath]

        self.listBox.clear()
        hasSelection = False
        for timeStamp in sorted(listOrder, reverse=True):
            newItem = QTreeWidgetItem([""] * 4)
            newItem.setIcon(self.C_NAME,
                            self.theParent.theTheme.getIcon("proj_nwx"))
            newItem.setText(self.C_NAME, listData[timeStamp][0])
            newItem.setData(self.C_NAME, Qt.UserRole, listData[timeStamp][2])
            newItem.setText(self.C_COUNT, formatInt(listData[timeStamp][1]))
            newItem.setText(
                self.C_TIME,
                datetime.fromtimestamp(timeStamp).strftime("%x %X"))
            newItem.setTextAlignment(self.C_NAME,
                                     Qt.AlignLeft | Qt.AlignVCenter)
            newItem.setTextAlignment(self.C_COUNT,
                                     Qt.AlignRight | Qt.AlignVCenter)
            newItem.setTextAlignment(self.C_TIME,
                                     Qt.AlignRight | Qt.AlignVCenter)
            newItem.setFont(self.C_TIME, self.theTheme.guiFontFixed)
            self.listBox.addTopLevelItem(newItem)
            if not hasSelection:
                newItem.setSelected(True)
                hasSelection = True

        projColWidth = self.mainConf.getProjColWidths()
        if len(projColWidth) == 3:
            self.listBox.setColumnWidth(self.C_NAME, projColWidth[self.C_NAME])
            self.listBox.setColumnWidth(self.C_COUNT,
                                        projColWidth[self.C_COUNT])
            self.listBox.setColumnWidth(self.C_TIME, projColWidth[self.C_TIME])

        return
Beispiel #8
0
class GuiProjectDetailsContents(QWidget):

    C_TITLE = 0
    C_WORDS = 1
    C_PAGES = 2
    C_PAGE = 3
    C_PROG = 4

    def __init__(self, theParent, theProject):
        QWidget.__init__(self, theParent)

        self.mainConf = nw.CONFIG
        self.theParent = theParent
        self.theProject = theProject
        self.theTheme = theParent.theTheme
        self.theIndex = theParent.theIndex
        self.optState = theProject.optState

        # Internal
        self._theToC = []

        iPx = self.theTheme.baseIconSize
        hPx = self.mainConf.pxInt(12)
        vPx = self.mainConf.pxInt(4)

        # Contents Tree
        # =============

        self.tocTree = QTreeWidget()
        self.tocTree.setIconSize(QSize(iPx, iPx))
        self.tocTree.setIndentation(0)
        self.tocTree.setColumnCount(6)
        self.tocTree.setSelectionMode(QAbstractItemView.NoSelection)
        self.tocTree.setHeaderLabels([
            self.tr("Title"),
            self.tr("Words"),
            self.tr("Pages"),
            self.tr("Page"),
            self.tr("Progress"), ""
        ])

        treeHeadItem = self.tocTree.headerItem()
        treeHeadItem.setTextAlignment(self.C_WORDS, Qt.AlignRight)
        treeHeadItem.setTextAlignment(self.C_PAGES, Qt.AlignRight)
        treeHeadItem.setTextAlignment(self.C_PAGE, Qt.AlignRight)
        treeHeadItem.setTextAlignment(self.C_PROG, Qt.AlignRight)

        treeHeader = self.tocTree.header()
        treeHeader.setStretchLastSection(True)
        treeHeader.setMinimumSectionSize(hPx)

        wCol0 = self.mainConf.pxInt(
            self.optState.getInt("GuiProjectDetails", "widthCol0", 200))
        wCol1 = self.mainConf.pxInt(
            self.optState.getInt("GuiProjectDetails", "widthCol1", 60))
        wCol2 = self.mainConf.pxInt(
            self.optState.getInt("GuiProjectDetails", "widthCol2", 60))
        wCol3 = self.mainConf.pxInt(
            self.optState.getInt("GuiProjectDetails", "widthCol3", 60))
        wCol4 = self.mainConf.pxInt(
            self.optState.getInt("GuiProjectDetails", "widthCol4", 90))

        self.tocTree.setColumnWidth(0, wCol0)
        self.tocTree.setColumnWidth(1, wCol1)
        self.tocTree.setColumnWidth(2, wCol2)
        self.tocTree.setColumnWidth(3, wCol3)
        self.tocTree.setColumnWidth(4, wCol4)
        self.tocTree.setColumnWidth(5, hPx)

        # Options
        # =======

        wordsPerPage = self.optState.getInt("GuiProjectDetails",
                                            "wordsPerPage", 350)
        countFrom = self.optState.getInt("GuiProjectDetails", "countFrom", 1)
        clearDouble = self.optState.getInt("GuiProjectDetails", "clearDouble",
                                           True)

        wordsHelp = (self.tr(
            "Typical word count for a 5 by 8 inch book page with 11 pt font is 350."
        ))
        offsetHelp = (self.tr("Start counting page numbers from this page."))
        dblHelp = (self.tr(
            "Assume a new chapter or partition always start on an odd numbered page."
        ))

        self.wpLabel = QLabel(self.tr("Words per page"))
        self.wpLabel.setToolTip(wordsHelp)

        self.wpValue = QSpinBox()
        self.wpValue.setMinimum(10)
        self.wpValue.setMaximum(1000)
        self.wpValue.setSingleStep(10)
        self.wpValue.setValue(wordsPerPage)
        self.wpValue.setToolTip(wordsHelp)
        self.wpValue.valueChanged.connect(self._populateTree)

        self.poLabel = QLabel(self.tr("Count pages from"))
        self.poLabel.setToolTip(offsetHelp)

        self.poValue = QSpinBox()
        self.poValue.setMinimum(1)
        self.poValue.setMaximum(9999)
        self.poValue.setSingleStep(1)
        self.poValue.setValue(countFrom)
        self.poValue.setToolTip(offsetHelp)
        self.poValue.valueChanged.connect(self._populateTree)

        self.dblLabel = QLabel(self.tr("Clear double pages"))
        self.dblLabel.setToolTip(dblHelp)

        self.dblValue = QSwitch(self, 2 * iPx, iPx)
        self.dblValue.setChecked(clearDouble)
        self.dblValue.setToolTip(dblHelp)
        self.dblValue.clicked.connect(self._populateTree)

        self.optionsBox = QGridLayout()
        self.optionsBox.addWidget(self.wpLabel, 0, 0)
        self.optionsBox.addWidget(self.wpValue, 0, 1)
        self.optionsBox.addWidget(self.dblLabel, 0, 3)
        self.optionsBox.addWidget(self.dblValue, 0, 4)
        self.optionsBox.addWidget(self.poLabel, 1, 0)
        self.optionsBox.addWidget(self.poValue, 1, 1)
        self.optionsBox.setHorizontalSpacing(hPx)
        self.optionsBox.setVerticalSpacing(vPx)
        self.optionsBox.setColumnStretch(2, 1)

        # Assemble
        # ========

        self.outerBox = QVBoxLayout()
        self.outerBox.addWidget(
            QLabel("<b>%s</b>" % self.tr("Table of Contents")))
        self.outerBox.addWidget(self.tocTree)
        self.outerBox.addLayout(self.optionsBox)

        self.setLayout(self.outerBox)

        self._prepareData()
        self._populateTree()

        return

    def getColumnSizes(self):
        """Return the column widths for the tree columns.
        """
        retVals = [
            self.tocTree.columnWidth(0),
            self.tocTree.columnWidth(1),
            self.tocTree.columnWidth(2),
            self.tocTree.columnWidth(3),
            self.tocTree.columnWidth(4),
        ]
        return retVals

    ##
    #  Internal Functions
    ##

    def _prepareData(self):
        """Extract the data for the tree.
        """
        self._theToC = []
        self._theToC = self.theIndex.getTableOfContents(2)
        self._theToC.append(("", 0, self.tr("END"), 0))
        return

    ##
    #  Slots
    ##

    def _populateTree(self):
        """Set the content of the chapter/page tree.
        """
        dblPages = self.dblValue.isChecked()
        wpPage = self.wpValue.value()
        fstPage = self.poValue.value() - 1

        pTotal = 0
        tPages = 1

        theList = []
        for _, tLevel, tTitle, wCount in self._theToC:
            pCount = math.ceil(wCount / wpPage)
            if dblPages:
                pCount += pCount % 2

            pTotal += pCount
            theList.append((tLevel, tTitle, wCount, pCount))

        pMax = pTotal - fstPage

        self.tocTree.clear()
        for tLevel, tTitle, wCount, pCount in theList:
            newItem = QTreeWidgetItem()

            if tPages <= fstPage:
                progPage = numberToRoman(tPages, True)
                progText = ""
            else:
                cPage = tPages - fstPage
                pgProg = 100.0 * (cPage - 1) / pMax if pMax > 0 else 0.0
                progPage = f"{cPage:n}"
                progText = f"{pgProg:.1f}{nwUnicode.U_THSP}%"

            newItem.setIcon(self.C_TITLE,
                            self.theTheme.getIcon("doc_h%d" % tLevel))
            newItem.setText(self.C_TITLE, tTitle)
            newItem.setText(self.C_WORDS, f"{wCount:n}")
            newItem.setText(self.C_PAGES, f"{pCount:n}")
            newItem.setText(self.C_PAGE, progPage)
            newItem.setText(self.C_PROG, progText)

            newItem.setTextAlignment(self.C_WORDS, Qt.AlignRight)
            newItem.setTextAlignment(self.C_PAGES, Qt.AlignRight)
            newItem.setTextAlignment(self.C_PAGE, Qt.AlignRight)
            newItem.setTextAlignment(self.C_PROG, Qt.AlignRight)

            # Make pages and titles/partitions stand out
            if tLevel < 2:
                bFont = newItem.font(self.C_TITLE)
                if tLevel == 0:
                    bFont.setItalic(True)
                else:
                    bFont.setBold(True)
                    bFont.setUnderline(True)
                newItem.setFont(self.C_TITLE, bFont)

            tPages += pCount

            self.tocTree.addTopLevelItem(newItem)

        return