Esempio n. 1
0
class MemUsageDialog(QDialog):
    def __init__(self, parent=None, update=True):
        QDialog.__init__(self, parent=parent)
        layout = QVBoxLayout()
        self.tree = QTreeWidget()
        layout.addWidget(self.tree)
        self.setLayout(layout)

        self._mgr = CacheMemoryManager()

        self._tracked_caches = {}

        # tree setup code
        self.tree.setHeaderLabels(
            ["cache", "memory", "roi", "dtype", "type", "info", "id"])
        self._idIndex = self.tree.columnCount() - 1
        self.tree.setColumnHidden(self._idIndex, True)
        self.tree.setSortingEnabled(True)
        self.tree.clear()

        self._root = TreeNode()

        # refresh every x seconds (see showEvent())
        self.timer = QTimer(self)
        if update:
            self.timer.timeout.connect(self._updateReport)

    def _updateReport(self):
        # we keep track of dirty reports so we just have to update the tree
        # instead of reconstructing it
        reports = []
        for c in self._mgr.getFirstClassCaches():
            r = MemInfoNode()
            c.generateReport(r)
            reports.append(r)
        self._root.handleChildrenReports(reports,
                                         root=self.tree.invisibleRootItem())

    def hideEvent(self, event):
        self.timer.stop()

    def showEvent(self, show):
        # update once so we don't have to wait for initial report
        self._updateReport()
        # update every 5 sec.
        self.timer.start(5 * 1000)
Esempio n. 2
0
class MemUsageDialog(QDialog):
    def __init__(self, parent=None, update=True):
        QDialog.__init__(self, parent=parent)
        layout = QVBoxLayout()
        self.tree = QTreeWidget()
        layout.addWidget(self.tree)
        self.setLayout(layout)

        self._mgr = CacheMemoryManager()

        self._tracked_caches = {}

        # tree setup code
        self.tree.setHeaderLabels(
            ["cache", "memory", "roi", "dtype", "type", "info", "id"])
        self._idIndex = self.tree.columnCount() - 1
        self.tree.setColumnHidden(self._idIndex, True)
        self.tree.setSortingEnabled(True)
        self.tree.clear()

        self._root = TreeNode()

        # refresh every x seconds (see showEvent())
        self.timer = QTimer(self)
        if update:
            self.timer.timeout.connect(self._updateReport)

    def _updateReport(self):
        # we keep track of dirty reports so we just have to update the tree
        # instead of reconstructing it
        reports = []
        for c in self._mgr.getFirstClassCaches():
            r = MemInfoNode()
            c.generateReport(r)
            reports.append(r)
        self._root.handleChildrenReports(
            reports, root=self.tree.invisibleRootItem())

    def hideEvent(self, event):
        self.timer.stop()

    def showEvent(self, show):
        # update once so we don't have to wait for initial report
        self._updateReport()
        # update every 5 sec.
        self.timer.start(5*1000)
Esempio n. 3
0
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)
Esempio n. 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()
Esempio n. 5
0
class Player(QWidget):
 
    fullScreenChanged = pyqtSignal(bool)
 
    def __init__(self, playlist, parent=None):
        super(Player, self).__init__(parent)
 
        self.colorDialog = None
        self.trackInfo = ""
        self.statusInfo = ""
        self.duration = 0
 
        self.player = QMediaPlayer()
        self.playlist = QMediaPlaylist()
        self.player.setPlaylist(self.playlist)
 
        self.player.durationChanged.connect(self.durationChanged)
        self.player.positionChanged.connect(self.positionChanged)
        self.player.metaDataChanged.connect(self.metaDataChanged)
        self.playlist.currentIndexChanged.connect(self.playlistPositionChanged)
        self.player.mediaStatusChanged.connect(self.statusChanged)
        self.player.bufferStatusChanged.connect(self.bufferingProgress)
        self.player.videoAvailableChanged.connect(self.videoAvailableChanged)
        self.player.error.connect(self.displayErrorMessage)
 
        self.videoWidget = VideoWidget()
        self.player.setVideoOutput(self.videoWidget)
 
        self.playlistModel = PlaylistModel()
        self.playlistModel.setPlaylist(self.playlist)
 
        self.playlistView = QListView()
        self.playlistView.setModel(self.playlistModel)
        self.playlistView.setCurrentIndex(
                self.playlistModel.index(self.playlist.currentIndex(), 0))
 
        self.playlistView.activated.connect(self.jump)

        self.script_box = QPlainTextEdit()
        self.segmentList = QTreeWidget()
        self.segmentList.setSortingEnabled(True)
        #self.segmentList.setColumnCount(5)
        self.segmentList.setColumnCount(4)
        #self.segmentList.setHeaderLabels(['Product','Start','Label','Tool','Behavior'])
        self.segmentList.setHeaderLabels(['Start segment', 'End segment', 'Label', 'Event'])

        '''
        self.productTextInput = QLineEdit()
        self.startTextInput = QLineEdit()
        self.labelTextInput = QLineEdit()
        self.toolTextInput = QLineEdit()
        self.behaviorTextInput = QLineEdit()
        '''
        self.startTextInput = QLineEdit()
        self.endTextInput = QLineEdit()
        self.labelTextInput = QLineEdit()
        self.contentTextInput = QLineEdit()

        self.addBtn = QPushButton("Add")
        self.addBtn.clicked.connect(self.addSegment)

        self.saveBtn = QPushButton("Save")
        self.saveBtn.clicked.connect(self.saveSegments)

        self.slider = QSlider(Qt.Horizontal)
        self.slider.setRange(0, self.player.duration() / 1000)
 
        self.labelDuration = QLabel()
        self.slider.sliderMoved.connect(self.seek)
 
        self.labelHistogram = QLabel()
        self.labelHistogram.setText("Histogram:")
        self.histogram = HistogramWidget()
        histogramLayout = QHBoxLayout()
        histogramLayout.addWidget(self.labelHistogram)
        histogramLayout.addWidget(self.histogram, 1)
 
        self.probe = QVideoProbe()
        self.probe.videoFrameProbed.connect(self.histogram.processFrame)
        self.probe.setSource(self.player)
 
        openButton = QPushButton("Open", clicked=self.open)
        if os.path.isdir(VIDEO_DIR):
            self.open_folder(VIDEO_DIR)

        controls = PlayerControls()
        controls.setState(self.player.state())
        controls.setVolume(self.player.volume())
        controls.setMuted(controls.isMuted())
 
        controls.play.connect(self.player.play)
        controls.pause.connect(self.player.pause)
        controls.stop.connect(self.player.stop)
        controls.next.connect(self.playlist.next)
        controls.previous.connect(self.previousClicked)
        controls.changeVolume.connect(self.player.setVolume)
        controls.changeMuting.connect(self.player.setMuted)
        controls.changeRate.connect(self.player.setPlaybackRate)
        controls.stop.connect(self.videoWidget.update)
 
        self.player.stateChanged.connect(controls.setState)
        self.player.volumeChanged.connect(controls.setVolume)
        self.player.mutedChanged.connect(controls.setMuted)

        #self.segmentButton = QPushButton("Segment")
        #self.segmentButton.clicked.connect(self.createNewSegment)
        self.startSegmentButton = QPushButton("Start Segment")
        self.startSegmentButton.clicked.connect(self.createNewStartSegment)
        # self.segmentButton.setCheckable(True)

        self.endSegmentButton = QPushButton("End Segment")
        self.endSegmentButton.clicked.connect(self.createNewEndSegment)

        #self.fullScreenButton = QPushButton("FullScreen")
        #self.fullScreenButton.setCheckable(True)
 
        self.colorButton = QPushButton("Color Options...")
        self.colorButton.setEnabled(False)
        self.colorButton.clicked.connect(self.showColorDialog)
 
        displayLayout = QHBoxLayout()
        # videoLayout = QVBoxLayout()
        # videoLayout.addWidget(self.videoWidget)
        # videoLayout.addWidget(self.script_box)
        
        displayLayout.addWidget(self.videoWidget, 3)

        editLayout = QVBoxLayout()
        editLayout.addWidget(self.playlistView, 2)
        #editLayout.addWidget(self.script_box, 4)
        editLayout.addWidget(self.segmentList, 3)
        segmentInputLayout = QHBoxLayout()
        '''
        segmentInputLayout.addWidget(self.productTextInput)
        segmentInputLayout.addWidget(self.startTextInput)
        segmentInputLayout.addWidget(self.labelTextInput)
        segmentInputLayout.addWidget(self.toolTextInput)
        segmentInputLayout.addWidget(self.behaviorTextInput)
        '''
        segmentInputLayout.addWidget(self.startTextInput)
        segmentInputLayout.addWidget(self.endTextInput)
        segmentInputLayout.addWidget(self.labelTextInput)
        segmentInputLayout.addWidget(self.contentTextInput)

        editLayout.addLayout(segmentInputLayout,1)

        displayLayout.addLayout(editLayout, 2)
 
        controlLayout = QHBoxLayout()
        controlLayout.setContentsMargins(0, 0, 0, 0)
        controlLayout.addWidget(openButton)
        controlLayout.addStretch(1)
        controlLayout.addWidget(controls)
        controlLayout.addStretch(1)
        #controlLayout.addWidget(self.segmentButton)
        controlLayout.addWidget(self.startSegmentButton)
        controlLayout.addWidget(self.endSegmentButton)
        controlLayout.addWidget(self.addBtn)
        controlLayout.addWidget(self.saveBtn)
        #controlLayout.addWidget(self.fullScreenButton)
        # controlLayout.addWidget(self.colorButton)
 
        layout = QVBoxLayout()
        layout.addLayout(displayLayout, 2)
        hLayout = QHBoxLayout()
        hLayout.addWidget(self.slider)
        hLayout.addWidget(self.labelDuration)
        layout.addLayout(hLayout)
        layout.addLayout(controlLayout)
        # layout.addLayout(histogramLayout)
 
        self.setLayout(layout)
 
        if not self.player.isAvailable():
            QMessageBox.warning(self, "Service not available",
                    "The QMediaPlayer object does not have a valid service.\n"
                    "Please check the media service plugins are installed.")
 
            controls.setEnabled(False)
            self.playlistView.setEnabled(False)
            openButton.setEnabled(False)
            self.colorButton.setEnabled(False)
            #self.fullScreenButton.setEnabled(False)
 
        self.metaDataChanged()
 
        self.addToPlaylist(playlist)
    
    def open(self):
        fileNames, _ = QFileDialog.getOpenFileNames(self, "Open Files")
        self.addToPlaylist(fileNames)

    def open_folder(self, folder_path):
        fileNames = [folder_path+x for x in os.listdir(folder_path) if x.endswith('.mp4')]
        self.addToPlaylist(fileNames)
 
    def addToPlaylist(self, fileNames):
        for name in fileNames:
            fileInfo = QFileInfo(name)
            if fileInfo.exists():
                url = QUrl.fromLocalFile(fileInfo.absoluteFilePath())
                if fileInfo.suffix().lower() == 'm3u':
                    self.playlist.load(url)
                else:
                    self.playlist.addMedia(QMediaContent(url))
            else:
                url = QUrl(name)
                if url.isValid():
                    self.playlist.addMedia(QMediaContent(url))

    def addSegment(self):
        item = TreeWidgetItem(self.segmentList)
        '''
        item.setText(0, self.productTextInput.text())
        item.setText(1, self.startTextInput.text())
        item.setText(2, self.labelTextInput.text())
        item.setText(3, self.toolTextInput.text())
        item.setText(4, self.behaviorTextInput.text())
        '''

        item.setText(0, self.startTextInput.text())
        item.setText(1, self.endTextInput.text())
        item.setText(2, self.labelTextInput.text())
        item.setText(3, self.contentTextInput.text())

        item.setFlags(item.flags() | Qt.ItemIsEditable)
        self.segmentList.addTopLevelItem(item)
        self.segmentList.sortByColumn(0, Qt.AscendingOrder)

        self.clear_input_boxes()
        self.player.play()
    
    def saveSegments(self):
        itemCnt = self.segmentList.topLevelItemCount()
        colCnt = self.segmentList.columnCount()
        save_dict = {'segments':[]}

        for i in range(itemCnt):
            item = self.segmentList.topLevelItem(i)
            temp_data = []
            for j in range(colCnt):
                temp_data.append(item.text(j))
            #temp_dict = {'product': temp_data[0], 'start': temp_data[1], 'label': temp_data[2], 'tool': temp_data[3], 'behavior': temp_data[4]}

            if len(temp_data[0]) > 0 and len(temp_data[1]) > 0 and (':' in temp_data[0]) and (':' in temp_data[1]):

                start_interval_seconds = 0
                j = 0
                while j < len(temp_data[0].split(':')):
                    start_interval_seconds += (int(temp_data[0].split(':')[- 1 - j]) * (60 ** j))
                    j += 1

                end_interval_seconds = 0
                j = 0
                while j < len(temp_data[1].split(':')):
                    end_interval_seconds += (int(temp_data[1].split(':')[- 1 - j]) * (60 ** j))
                    j += 1

            else:
                start_interval_seconds = ''
                end_interval_seconds = ''

            temp_dict = {'start_segment': start_interval_seconds, 'end_segment': end_interval_seconds, 'label': temp_data[2], 'event': temp_data[3]}
            save_dict['segments'].append(temp_dict)
        
        import json
        file_name = self.playlist.currentMedia().canonicalUrl().fileName()
        with open(SEGMENT_DIR+file_name.replace('.mp4','.json'),'w') as file:
            json.dump(save_dict, file)

    def durationChanged(self, duration):
        duration /= 1000
 
        self.duration = duration
        self.slider.setMaximum(duration)
 
    def positionChanged(self, progress):
        progress /= 1000

        if not self.slider.isSliderDown():
            self.slider.setValue(progress)
 
        self.updateDurationInfo(progress)
 
    def metaDataChanged(self):
        if self.player.isMetaDataAvailable():
            self.setTrackInfo("%s - %s" % (
                    self.player.metaData(QMediaMetaData.AlbumArtist),
                    self.player.metaData(QMediaMetaData.Title)))
 
    def previousClicked(self):
        # Go to the previous track if we are within the first 5 seconds of
        # playback.  Otherwise, seek to the beginning.
        if self.player.position() <= 5000:
            self.playlist.previous()
        else:
            self.player.setPosition(0)
    
    def clear_input_boxes(self):
        '''
        self.productTextInput.clear()
        self.startTextInput.clear()
        self.labelTextInput.clear()
        self.toolTextInput.clear()
        self.behaviorTextInput.clear()
        '''
        self.startTextInput.clear()
        self.endTextInput.clear()
        self.labelTextInput.clear()
        self.contentTextInput.clear()


    def jump(self, index):
        if index.isValid():
            self.playlist.setCurrentIndex(index.row())
            self.player.play()
            file_name = self.playlist.currentMedia().canonicalUrl().fileName()
            '''
            script_file_name = file_name.replace('.mp4','.txt')
            if os.path.isfile(SCRIPT_DIR+script_file_name):
                text=open(SCRIPT_DIR+script_file_name).read()
                self.script_box.setPlainText(text)
            '''

            segment_file_path = SEGMENT_DIR + file_name.replace('.mp4','.json')
            json_dict = self.open_json(segment_file_path)
            self.clear_input_boxes()
            self.segmentList.clear()
            for segment in json_dict["segments"]:
                item = TreeWidgetItem(self.segmentList)
                '''
                item.setText(0, segment['product'])
                item.setText(1, str(segment['start']))
                item.setText(2, segment['label'])
                item.setText(3, segment['tool'])
                item.setText(4, segment['behavior'])
                '''
                item.setText(0, segment['start_segment'])
                item.setText(1, segment['end_segment'])
                item.setText(2, segment['label'])
                item.setText(3, segment['content'])

                item.setFlags(item.flags() | Qt.ItemIsEditable)
                self.segmentList.addTopLevelItem(item)
                
            
            
            # print([str(x.text()) for x in self.segmentList.currentItem()])

    def open_json(self, file_path):
        import json
        try:
            with open(file_path, 'r') as file:
                json_dict = json.loads(file.read())
        except:
            json_dict = {"segments":[]}
            # json_dict = {"segments":[{"product":"Sorry","start":"File not found.","label":"","tool":"","behavior":""}]}
        return json_dict
    
    def playlistPositionChanged(self, position):
        self.playlistView.setCurrentIndex(
                self.playlistModel.index(position, 0))
 
    def seek(self, seconds):
        self.player.setPosition(seconds * 1000)
 
    def statusChanged(self, status):
        self.handleCursor(status)
 
        if status == QMediaPlayer.LoadingMedia:
            self.setStatusInfo("Loading...")
        elif status == QMediaPlayer.StalledMedia:
            self.setStatusInfo("Media Stalled")
        elif status == QMediaPlayer.EndOfMedia:
            QApplication.alert(self)
        elif status == QMediaPlayer.InvalidMedia:
            self.displayErrorMessage()
        else:
            self.setStatusInfo("")
 
    def handleCursor(self, status):
        if status in (QMediaPlayer.LoadingMedia, QMediaPlayer.BufferingMedia, QMediaPlayer.StalledMedia):
            self.setCursor(Qt.BusyCursor)
        else:
            self.unsetCursor()
 
    def bufferingProgress(self, progress):
        self.setStatusInfo("Buffering %d%" % progress)
 
    def videoAvailableChanged(self, available):
        '''
        if available:
            self.fullScreenButton.clicked.connect(
                    self.videoWidget.setFullScreen)
            self.videoWidget.fullScreenChanged.connect(
                    self.fullScreenButton.setChecked)
 
            if self.fullScreenButton.isChecked():
                self.videoWidget.setFullScreen(True)
        else:
            self.fullScreenButton.clicked.disconnect(
                    self.videoWidget.setFullScreen)
            self.videoWidget.fullScreenChanged.disconnect(
                    self.fullScreenButton.setChecked)
 
            self.videoWidget.setFullScreen(False)

        '''
        self.colorButton.setEnabled(available)
 
    def setTrackInfo(self, info):
        self.trackInfo = info
 
        if self.statusInfo != "":
            self.setWindowTitle("%s | %s" % (self.trackInfo, self.statusInfo))
        else:
            self.setWindowTitle(self.trackInfo)
 
    def setStatusInfo(self, info):
        self.statusInfo = info
 
        if self.statusInfo != "":
            self.setWindowTitle("%s | %s" % (self.trackInfo, self.statusInfo))
        else:
            self.setWindowTitle(self.trackInfo)
 
    def displayErrorMessage(self):
        self.setStatusInfo(self.player.errorString())
 
    def updateDurationInfo(self, currentInfo):
        duration = self.duration
        if currentInfo or duration:
            currentTime = QTime((currentInfo/3600)%60, (currentInfo/60)%60,
                    currentInfo%60, (currentInfo*1000)%1000)
            totalTime = QTime((duration/3600)%60, (duration/60)%60,
                    duration%60, (duration*1000)%1000);
 
            format = 'hh:mm:ss' if duration > 3600 else 'mm:ss'
            tStr = currentTime.toString(format) + " / " + totalTime.toString(format)
        else:
            tStr = ""
 
        self.labelDuration.setText(tStr)
    '''
    def createNewSegment(self):
        self.startTextInput.setText(str(int(self.player.position()/1000)))
    '''

    def createNewStartSegment(self):
        seconds = int(self.player.position()/1000)
        self.startTextInput.setText("{:02d}".format(math.floor(seconds / 3600)) + ':' + "{:02d}".format(
            math.floor((seconds / 60)) - math.floor(seconds / 3600) * 60) + ':' + "{:02d}".format(seconds % 60))

    def createNewEndSegment(self):
        seconds = int(self.player.position() / 1000)
        self.endTextInput.setText("{:02d}".format(math.floor(seconds / 3600)) + ':' + "{:02d}".format(
            math.floor((seconds / 60)) - math.floor(seconds / 3600) * 60) + ':' + "{:02d}".format(seconds % 60))
        self.player.pause()

    def showColorDialog(self):
        if self.colorDialog is None:
            brightnessSlider = QSlider(Qt.Horizontal)
            brightnessSlider.setRange(-100, 100)
            brightnessSlider.setValue(self.videoWidget.brightness())
            brightnessSlider.sliderMoved.connect(
                    self.videoWidget.setBrightness)
            self.videoWidget.brightnessChanged.connect(
                    brightnessSlider.setValue)
 
            contrastSlider = QSlider(Qt.Horizontal)
            contrastSlider.setRange(-100, 100)
            contrastSlider.setValue(self.videoWidget.contrast())
            contrastSlider.sliderMoved.connect(self.videoWidget.setContrast)
            self.videoWidget.contrastChanged.connect(contrastSlider.setValue)
 
            hueSlider = QSlider(Qt.Horizontal)
            hueSlider.setRange(-100, 100)
            hueSlider.setValue(self.videoWidget.hue())
            hueSlider.sliderMoved.connect(self.videoWidget.setHue)
            self.videoWidget.hueChanged.connect(hueSlider.setValue)
 
            saturationSlider = QSlider(Qt.Horizontal)
            saturationSlider.setRange(-100, 100)
            saturationSlider.setValue(self.videoWidget.saturation())
            saturationSlider.sliderMoved.connect(
                    self.videoWidget.setSaturation)
            self.videoWidget.saturationChanged.connect(
                    saturationSlider.setValue)
 
            layout = QFormLayout()
            layout.addRow("Brightness", brightnessSlider)
            layout.addRow("Contrast", contrastSlider)
            layout.addRow("Hue", hueSlider)
            layout.addRow("Saturation", saturationSlider)
 
            button = QPushButton("Close")
            layout.addRow(button)
 
            self.colorDialog = QDialog(self)
            self.colorDialog.setWindowTitle("Color Options")
            self.colorDialog.setLayout(layout)
 
            button.clicked.connect(self.colorDialog.close)
 
        self.colorDialog.show()
Esempio n. 6
0
def autoresize_columns(tree_widget: QTreeWidget):
    """Resize all columns of a QTreeWidget to fit content."""
    tree_widget.expandAll()
    for i in range(0, tree_widget.columnCount() - 1):
        tree_widget.resizeColumnToContents(i)