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)
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)
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 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()
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()
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)