class ListModel(QAbstractTableModel): orderChanged = pyqtSignal() elementSelected = pyqtSignal(int) class ColumnID(object): """ Define how many column the model holds and their type """ ncols = 2 Name = 0 Delete = 1 def __init__(self, elements=None, parent=None): """ Common interface for the labelListModel, the boxListModel, and the cropListModel see concrete implementations for details :param elements: :param parent: """ QAbstractTableModel.__init__(self, parent) if elements is None: elements = [] self._elements = list(elements) self._selectionModel = QItemSelectionModel(self) def onSelectionChanged(selected, deselected): if selected: ind = selected[0].indexes() if len(ind) > 0: self.elementSelected.emit(ind[0].row()) self._selectionModel.selectionChanged.connect(onSelectionChanged) self._allowRemove = True self._toolTipSuffixes = {} self.unremovable_rows = [ ] # rows in this list cannot be removed from the gui, # to add to this list call self.makeRowPermanent(int) # to remove make the self.makeRowRemovable(int) def makeRowPermanent(self, rowIndex): """ The rowindex cannot be removed from gui to remove this index use self.makeRowRemovable """ self.unremovable_rows.append(rowIndex) def makeRowRemovable(self, rowIndex): """ :param rowIndex: is the index for the label of interest in the current gui setting """ self.unremovable_rows.remove(rowIndex) def __len__(self): return len(self._elements) def __getitem__(self, i): return self._elements[i] def selectedRow(self): selected = self._selectionModel.selectedRows() if len(selected) == 1: return selected[0].row() return -1 def selectedIndex(self): row = self.selectedRow() if row >= 0: return self.index(self.selectedRow()) else: return QModelIndex() def rowCount(self, parent=None): return len(self._elements) def columnCount(self, parent): return self.ColumnID.ncols def _getToolTipSuffix(self, row): """ Get the middle column tooltip suffix """ suffix = "; Click to select" if row in self._toolTipSuffixes: suffix = self._toolTipSuffixes[row] return suffix def _setToolTipSuffix(self, row, text): """ Set the middle column tooltip suffix """ self._toolTipSuffixes[row] = text index = self.createIndex(row, 1) self.dataChanged.emit(index, index) class EntryToolTipAdapter(object): """This class can be used to make each row look like a separate widget with its own tooltip. In this case, the "tooltip" is the suffix appended to the tooltip of the middle column. """ def __init__(self, table, row): self._row = row self._table = table def toolTip(self): return self._table._getToolTipSuffix(self._row) def setToolTip(self, text): self._table._setToolTipSuffix(self._row, text) def insertRow(self, position, object, parent=QModelIndex()): self.beginInsertRows(parent, position, position) object.changed.connect(self.modelReset) self._elements.insert(position, object) self.endInsertRows() return True def removeRow(self, position, parent=QModelIndex()): if position in self.unremovable_rows: return False self.beginRemoveRows(parent, position, position) value = self._elements[position] logger.debug("removing row: " + str(value)) self._elements.remove(value) self.endRemoveRows() return True def allowRemove(self, check): # Allow removing of rows. Needed to be able to disallow it # in interactive mode self._allowRemove = check self.dataChanged.emit( self.createIndex(0, self.ColumnID.Delete), self.createIndex(self.rowCount(), self.ColumnID.Delete)) def data(self, index, role): """ Reimplement, see labelListModel or boxListModel for concrete example :param index: :param role: """ if role == Qt.EditRole and index.column() == self.ColumnID.Name: name = self._elements[index.row()].name return name elif role == Qt.ToolTipRole and index.column() == self.ColumnID.Delete: s = "Delete {}".format(self._elements[index.row()].name) return s elif role == Qt.ToolTipRole and index.column() == self.ColumnID.Name: suffix = self._getToolTipSuffix(index.row()) s = "{}\nDouble click to rename {}".format( self._elements[index.row()].name, suffix) return s elif role == Qt.DisplayRole and index.column() == self.ColumnID.Name: name = self._elements[index.row()].name return name if role == Qt.DecorationRole and index.column( ) == self.ColumnID.Delete: if index.row() in self.unremovable_rows: return row = index.row() pixmap = QPixmap(_NPIXELS, _NPIXELS) pixmap.fill(Qt.transparent) painter = QPainter() painter.begin(pixmap) painter.setRenderHint(QPainter.Antialiasing) painter.setBrush(QColor("red")) painter.drawEllipse(1, 1, _NPIXELS - 2, _NPIXELS - 2) pen = QPen(QColor("black")) pen.setWidth(2) painter.setPen(pen) x = _XSTART y = _NPIXELS - x painter.drawLine(x, x, y, y) painter.drawLine(y, x, x, y) painter.end() icon = QIcon(pixmap) return icon def flags(self, index): """ Reimplement, see labelListModel or boxListModel for concrete example :param index: """ if index.column() == self.ColumnID.Delete: if self._allowRemove: return Qt.ItemIsEnabled | Qt.ItemIsSelectable else: return Qt.NoItemFlags elif index.column() == self.ColumnID.Name: return Qt.ItemIsEditable | Qt.ItemIsEnabled | Qt.ItemIsSelectable else: return Qt.NoItemFlags def setData(self, index, value, role=Qt.EditRole): """ Reimplement, see labelListModel or boxListModel for concrete example :param index: """ if role == Qt.EditRole and index.column() == self.ColumnID.Name: row = index.row() self._elements[row].name = value self.dataChanged.emit(index, index) return True return False def select(self, row): """ Reimplement, see labelListModel or boxListModel for concrete example :param row """ self._selectionModel.clear() self._selectionModel.select(self.index(row, self.ColumnID.Name), QItemSelectionModel.Select) def clearSelectionModel(self): self._selectionModel.clear()
class SMSTreeView(QTreeView): ''' 自定义树控件 ''' def __init__(self, parent=None): QTreeView.__init__(self, parent) #model self._model = CategoriesTreeModel(self) self.setModel(self._model) # selectionMode self._selection_model = QItemSelectionModel(self._model, self) self.setSelectionModel(self._selection_model) #delegate self._delegate = CategoriesTreeDelegate(parent) self.setItemDelegate(self._delegate) #阻止双击时出现编辑框 self.setEditTriggers(self.NoEditTriggers) #设置展开/收缩动画 self.setAnimated(True) #展开所有 self.expandAll() #开启右键自定义菜单 self.setContextMenuPolicy(Qt.CustomContextMenu) #右键菜单信号槽 self.customContextMenuRequested.connect(self._slot_custom_context_menu) #右键菜单项 self._context_menu_add_child = QAction('添加子分类') self._context_menu_add_child.triggered.connect( self._slot_context_menu_add_child) self._context_menu_rename = QAction('重命名') self._context_menu_rename.triggered.connect( self._slot_context_menu_rename) self._context_menu_delete = QAction('删除该分类') self._context_menu_delete.triggered.connect( self._slot_context_menu_delete) # 设置默认选择为 root self._selection_model.select(self.rootIndex(), QItemSelectionModel.SelectCurrent) def _slot_custom_context_menu(self, point): menu = QMenu() current_index = self.currentIndex() current_item = current_index.internalPointer() #添加子类 菜单项 menu.addAction(self._context_menu_add_child) #重命名 菜单项 和 删除 菜单项 if current_item.parent != 0: menu.addAction(self._context_menu_rename) menu.addAction(self._context_menu_delete) menu.exec(QCursor.pos()) def _slot_context_menu_rename(self, checked): self.edit(self.currentIndex()) def _slot_context_menu_add_child(self, checked): current_index = self.currentIndex() result = QInputDialog.getText(self, '添加新分类', '请输入分类名', text='新分类') if result[1]: if not result[0]: QMessageBox.critical(self, '添加失败', '分类名不能为空') else: self._model.add_item(result[0], current_index) # 2017.2.11 sqlalchemy 一执行删除就挂了,不晓得为啥子 def _slot_context_menu_delete(self, checked): QMessageBox.information(self, '功能暂时不可用', 'sqlalchemy 一执行删除就挂了,不晓得为啥子') return 0 current_index = self.currentIndex() if self._model.has_child(current_index): if QMessageBox.question(self, '确认', \ '该分类下还有子分类,删除该分类将连带删除子分类,确定删除?') \ != QMessageBox.Yes: return 0 self._model.remove_item(current_index) def set_select_changed_slot(self, slot): self._selection_model.selectionChanged.connect(slot)
def select(self, index, flags=QItemSelectionModel.ClearAndSelect): if isinstance(index, int): index = self.model().index(index) return QItemSelectionModel.select(self, index, flags)
class LayerStackModel(QAbstractListModel): canMoveSelectedUp = pyqtSignal("bool") canMoveSelectedDown = pyqtSignal("bool") canDeleteSelected = pyqtSignal("bool") orderChanged = pyqtSignal() layerAdded = pyqtSignal(Layer, int) # is now in row layerRemoved = pyqtSignal(Layer, int) # was in row stackCleared = pyqtSignal() def __init__(self, parent=None): QAbstractListModel.__init__(self, parent) self._layerStack = [] self.selectionModel = QItemSelectionModel(self) self.selectionModel.selectionChanged.connect(self.updateGUI) self.selectionModel.selectionChanged.connect(self._onSelectionChanged) self._movingRows = False QTimer.singleShot(0, self.updateGUI) def _handleRemovedLayer(layer): # Layerstacks *own* the layers they hold, and thus are # responsible for cleaning them up when they are removed: layer.clean_up() self.layerRemoved.connect(_handleRemovedLayer) #### ## High level API to manipulate the layerstack ### def __len__(self): return self.rowCount() def __repr__(self): return "<LayerStackModel: layerStack='%r'>" % (self._layerStack, ) def __getitem__(self, i): return self._layerStack[i] def __iter__(self): return self._layerStack.__iter__() def layerIndex(self, layer): #note that the 'index' function already has a different implementation #from Qt side return self._layerStack.index(layer) def findMatchingIndex(self, func): """Call the given function with each layer and return the index of the first layer for which f is True.""" for index, layer in enumerate(self._layerStack): if func(layer): return index raise ValueError("No matching layer in stack.") def append(self, data): self.insert(0, data) def clear(self): if len(self) > 0: self.removeRows(0, len(self)) self.stackCleared.emit() def insert(self, index, data): """ Insert a layer into this layer stack, which *takes ownership* of the layer. """ assert isinstance( data, Layer), "Only Layers can be added to a LayerStackModel" self.insertRow(index) self.setData(self.index(index), data) if self.selectedRow() >= 0: self.selectionModel.select(self.index(self.selectedRow()), QItemSelectionModel.Deselect) self.selectionModel.select(self.index(index), QItemSelectionModel.Select) data.changed.connect( functools.partial(self._onLayerChanged, self.index(index))) index = self._layerStack.index(data) self.layerAdded.emit(data, index) self.updateGUI() def selectRow(self, row): already_selected_rows = self.selectionModel.selectedRows() if len(already_selected_rows) == 1 and row == already_selected_rows[0]: # Nothing to do if this row is already selected return self.selectionModel.clear() self.selectionModel.setCurrentIndex(self.index(row), QItemSelectionModel.SelectCurrent) def deleteSelected(self): num_rows = len(self.selectionModel.selectedRows()) assert num_rows == 1, "Can't delete selected row: {} layers are currently selected.".format( num_rows) row = self.selectionModel.selectedRows()[0] layer = self._layerStack[row.row()] assert not layer._cleaned_up, "This layer ({}) has already been cleaned up. Shouldn't it already be removed from the layerstack?".format( layer.name) self.removeRow(row.row()) if self.rowCount() > 0: self.selectionModel.select(self.index(0), QItemSelectionModel.Select) self.layerRemoved.emit(layer, row.row()) self.updateGUI() def moveSelectedUp(self): assert len(self.selectionModel.selectedRows()) == 1 row = self.selectionModel.selectedRows()[0] if row.row() != 0: oldRow = row.row() newRow = oldRow - 1 self._moveToRow(oldRow, newRow) def moveSelectedDown(self): assert len(self.selectionModel.selectedRows()) == 1 row = self.selectionModel.selectedRows()[0] if row.row() != self.rowCount() - 1: oldRow = row.row() newRow = oldRow + 1 self._moveToRow(oldRow, newRow) def moveSelectedToTop(self): assert len(self.selectionModel.selectedRows()) == 1 row = self.selectionModel.selectedRows()[0] if row.row() != 0: oldRow = row.row() newRow = 0 self._moveToRow(oldRow, newRow) def moveSelectedToBottom(self): assert len(self.selectionModel.selectedRows()) == 1 row = self.selectionModel.selectedRows()[0] if row.row() != self.rowCount() - 1: oldRow = row.row() newRow = self.rowCount() - 1 self._moveToRow(oldRow, newRow) def moveSelectedToRow(self, newRow): assert len(self.selectionModel.selectedRows()) == 1 row = self.selectionModel.selectedRows()[0] if row.row() != newRow: oldRow = row.row() self._moveToRow(oldRow, newRow) def _moveToRow(self, oldRow, newRow): d = self._layerStack[oldRow] self.removeRow(oldRow) self.insertRow(newRow) self.setData(self.index(newRow), d) self.selectionModel.select(self.index(newRow), QItemSelectionModel.Select) self.orderChanged.emit() self.updateGUI() #### ## Low level API. To add, remove etc. layers use the high level API from above. #### def updateGUI(self): self.canMoveSelectedUp.emit(self.selectedRow() > 0) self.canMoveSelectedDown.emit(self.selectedRow() < self.rowCount() - 1) self.canDeleteSelected.emit(self.rowCount() > 0) self.wantsUpdate() def selectedRow(self): selected = self.selectionModel.selectedRows() if len(selected) == 1: return selected[0].row() return -1 def selectedIndex(self): row = self.selectedRow() if row >= 0: return self.index(self.selectedRow()) else: return QModelIndex() def rowCount(self, parent=QModelIndex()): if not parent.isValid(): return len(self._layerStack) return 0 def insertRows(self, row, count, parent=QModelIndex()): '''Insert empty rows in the stack. DO NOT USE THIS METHOD TO INSERT NEW LAYERS! Always use the insert() or append() method. ''' if parent.isValid(): return False oldRowCount = self.rowCount() #for some reason, row can be negative! beginRow = max(0, row) endRow = min(beginRow + count - 1, len(self._layerStack)) self.beginInsertRows(parent, beginRow, endRow) while (beginRow <= endRow): self._layerStack.insert(row, Layer(datasources=[])) beginRow += 1 self.endInsertRows() assert self.rowCount( ) == oldRowCount + 1, "oldRowCount = %d, self.rowCount() = %d" % ( oldRowCount, self.rowCount()) return True def removeRows(self, row, count, parent=QModelIndex()): '''Remove rows from the stack. DO NOT USE THIS METHOD TO REMOVE LAYERS! Use the deleteSelected() method instead. ''' if parent.isValid(): return False if row + count <= 0 or row >= len(self._layerStack): return False oldRowCount = self.rowCount() beginRow = max(0, row) endRow = min(row + count - 1, len(self._layerStack) - 1) self.beginRemoveRows(parent, beginRow, endRow) while (beginRow <= endRow): del self._layerStack[row] beginRow += 1 self.endRemoveRows() return True def flags(self, index): defaultFlags = Qt.ItemIsSelectable | Qt.ItemIsEditable | Qt.ItemIsEnabled if index.isValid(): return Qt.ItemIsDragEnabled | defaultFlags else: return Qt.ItemIsDropEnabled | defaultFlags def supportedDropActions(self): return Qt.MoveAction def data(self, index, role=Qt.DisplayRole): if not index.isValid(): return None if index.row() >= len(self._layerStack): return None if role == Qt.DisplayRole or role == Qt.EditRole: return self._layerStack[index.row()] elif role == Qt.ToolTipRole: return self._layerStack[index.row()].toolTip() else: return None def setData(self, index, value, role=Qt.EditRole): '''Replace one layer with another. DO NOT USE THIS METHOD TO INSERT NEW LAYERS! Use deleteSelected() followed by insert() or append(). ''' if role == Qt.EditRole: layer = value if not isinstance(value, Layer): layer = value self._layerStack[index.row()] = layer self.dataChanged.emit(index, index) return True elif role == Qt.ToolTipRole: self._layerStack[index.row()].setToolTip() return True return False def headerData(self, section, orientation, role=Qt.DisplayRole): if role != Qt.DisplayRole: return None if orientation == Qt.Horizontal: return "Column %r" % section else: return "Row %r" % section def wantsUpdate(self): self.layoutChanged.emit() def _onLayerChanged(self, idx): self.dataChanged.emit(idx, idx) self.updateGUI() def _onSelectionChanged(self, selected, deselected): for idx in deselected.indexes(): self[idx.row()].setActive(False) for idx in selected.indexes(): self[idx.row()].setActive(True)
class MainWindow(QMainWindow, Ui_MainWindow): def __init__(self, ): super(MainWindow, self).__init__() self.setupUi(self) self.fig_dict = {} self.tabWidget.setCurrentIndex(0) self.plotAreaVerticalLayout = QtWidgets.QVBoxLayout() self.plotsFrame.setLayout(self.plotAreaVerticalLayout) #initialize Table self.headerV = self.tableWidget.verticalHeader() self.headerV.show() # add a widget for previewing plots, they can then be added to the actual plot self.plotPreview = PlotWidget() self.plotAreaVerticalLayout.addWidget(self.plotPreview) # Create tree model to store sim data items and connect it to views self.simDataItemTreeModel = SimDataItemTreeModel() self.bdTableModel = BdTableModel() self.bdUserGeneratedTableModel = BdUserGeneratedCurvesTableModel() self.simDataItemTreeView.setModel(self.simDataItemTreeModel) self.plotPreview.tableView.setModel(self.bdTableModel) # connect a double clicked section of the bd table to a change of the anchor self.plotPreview.tableView.horizontalHeader( ).sectionDoubleClicked.connect(self.update_bd_table) self.plotPreview.tableView.verticalHeader( ).sectionDoubleClicked.connect( self.update_bd_user_generated_curves_table) # Set custom selection model, so that sub items are automatically # selected if parent is selected self._selection_model = QRecursiveSelectionModel( self.simDataItemTreeView.model()) self.simDataItemTreeView.setSelectionModel(self._selection_model) # Connect list view with model for the selected values of tree view self.selectedSimulationDataItemListModel = OrderedDictModel() self.simDataItemListView.setModel( self.selectedSimulationDataItemListModel) self._selection_model.selectionChanged.connect(self.change_list) # set up signals and slots self.selectedSimulationDataItemListModel.items_changed.connect( self.update_variable_tree) # Connect signals of menus self.actionOpen_File.triggered.connect( self.simDataItemTreeView.add_file) self.actionOpen_Directory.triggered.connect( self.simDataItemTreeView.add_folder) self.actionOpen_Directory_List.triggered.connect( self.simDataItemTreeView.add_folder_list) self.actionHide_PlotSettings.triggered.connect( self.set_plot_settings_visibility) self.actionHide_Sequence.triggered.connect( self.set_sequence_widget_visibility) self.actionHide_Status.triggered.connect( self.set_status_widget_visibility) self.actionSave_Table.triggered.connect(self.save_bd_table) self.actionExport_Figure_as_Tikzpicture.triggered.connect( self.plotPreview.export_plot_tikz) self.actionExport_TableWidget.triggered.connect( self.export_table_to_csv) self.actionSave_Data.triggered.connect(self.save_current_selection) self.action_About.triggered.connect(self.open_about_page) self.variableTreeModel = VariableTreeModel() self.variableTreeView.setModel(self.variableTreeModel) self.plotsettings.visibilityChanged.connect( self.plot_settings_visibility_changed) self.sequenceWidget.visibilityChanged.connect( self.sequence_widget_visibility_changed) self.statusWidget.visibilityChanged.connect( self.status_widget_visibility_changed) # Set recursive selection model for variable view self._variable_tree_selection_model = QRecursiveSelectionModel( self.variableTreeView.model()) self.variableTreeView.setSelectionModel( self._variable_tree_selection_model) # set up combo boxes for rate/psnr and interpolation options self.combo_interp.addItems(["pchip", "pol"]) self.combo_rate_psnr.addItems(["drate", "dsnr"]) self.combo_interp.currentIndexChanged.connect(self.on_combo_box) self.combo_rate_psnr.currentIndexChanged.connect(self.on_combo_box) # set up bd plot checkbox self.checkBox_bdplot.stateChanged.connect(self.update_bd_plot) self.curveWidget.hide() self.curveListModel = OrderedDictModel() self.curveListView.setModel(self.curveListModel) self.curveListSelectionModel = QItemSelectionModel(self.curveListModel) self.curveListView.setSelectionModel(self.curveListSelectionModel) self.curveListView.setSelectionMode( QtWidgets.QAbstractItemView.ExtendedSelection) self.curveWidget.visibilityChanged.connect( self.curve_widget_visibility_changed) self.actionGenerate_curve.triggered.connect(self.generate_new_curve) self.actionRemove_items.triggered.connect(self.remove) self.actionReload_files.triggered.connect(self.reload_files) self.settings = QSettings() self.get_recent_files() self.simDataItemTreeView.itemsOpened.connect(self.add_recent_files) self.watcher = QFileSystemWatcher(self) self.watcher.fileChanged.connect(self.warning_file_change) self.watcher.directoryChanged.connect(self.warning_file_change) self.simDataItemTreeView.parserThread.newParsedData.connect( self.add_files_to_watcher) self.show_file_changed_message = True self.reset_timer = QTimer(self) self.reset_timer.setSingleShot(True) self.reset_timer.setInterval(15000) self.reset_timer.timeout.connect(self._reset_file_changed_message) self.simDataItemTreeView.customContextMenuRequested.connect( self.show_sequences_context_menu) # self.curveListView.actionCalculateBD.triggered.connect(self.bd_user_generated_curves) # sets Visibility for the Plotsettings Widget def set_plot_settings_visibility(self): self.plotsettings.visibilityChanged.disconnect( self.plot_settings_visibility_changed) if self.plotsettings.isHidden(): self.plotsettings.setVisible(True) else: self.plotsettings.setHidden(True) self.plotsettings.visibilityChanged.connect( self.plot_settings_visibility_changed) # updates the QAction if Visibility is changed def plot_settings_visibility_changed(self): if self.plotsettings.isHidden(): self.actionHide_PlotSettings.setChecked(True) else: self.actionHide_PlotSettings.setChecked(False) self._variable_tree_selection_model.selectionChanged.connect( self.update_plot) self.curveListSelectionModel.selectionChanged.connect(self.update_plot) self.simDataItemTreeView.deleteKey.connect(self.remove) # sets Visibility for the Sequence Widget def set_sequence_widget_visibility(self): self.sequenceWidget.visibilityChanged.disconnect( self.sequence_widget_visibility_changed) if self.sequenceWidget.isHidden(): self.sequenceWidget.setVisible(True) else: self.sequenceWidget.setHidden(True) self.sequenceWidget.visibilityChanged.connect( self.sequence_widget_visibility_changed) def sequence_widget_visibility_changed(self): if self.sequenceWidget.isHidden(): self.actionHide_Sequence.setChecked(True) else: self.actionHide_Sequence.setChecked(False) # Sets Visibility for the Status Widget def set_status_widget_visibility(self): self.statusWidget.visibilityChanged.disconnect( self.status_widget_visibility_changed) if self.statusWidget.isHidden(): self.statusWidget.setVisible(True) else: self.statusWidget.setHidden(True) self.statusWidget.visibilityChanged.connect( self.status_widget_visibility_changed) def status_widget_visibility_changed(self): if self.statusWidget.isHidden(): self.actionHide_Status.setChecked(True) else: self.actionHide_Status.setChecked(False) def curve_widget_visibility_changed(self): if self.curveWidget.isHidden(): self.curveListView.delete_key.disconnect() else: self.curveListView.delete_key.connect(self.remove_curves) def remove(self): values = self.selectedSimulationDataItemListModel.values() for value in values: self.watcher.removePath(value.path) # List call necessary to avoid runtime error because of elements changing # during iteration self._variable_tree_selection_model.selectionChanged.disconnect() # disconnect slot to avoid multiple function triggers by selectionChanged signal # not disconnecting slows program down significantly self._selection_model.selectionChanged.disconnect(self.change_list) self.simDataItemTreeModel.remove(list(values)) self.change_list(QItemSelection(), QItemSelection()) self._selection_model.selectionChanged.connect(self.change_list) self._variable_tree_selection_model.selectionChanged.connect( self.update_plot) if len(self.selectedSimulationDataItemListModel.values()) == 0: self.update_plot() def change_list(self, q_selected, q_deselected): """Extend superclass behavior by automatically adding the values of all selected items in :param: `q_selected` to value list model. """ selected_q_indexes = q_deselected.indexes() q_reselect_indexes = [] for q_index in self.simDataItemTreeView.selectedIndexes(): if q_index not in selected_q_indexes: q_reselect_indexes.append(q_index) # Find all all values that are contained by selected tree items tuples = [] for q_index in q_selected.indexes() + q_reselect_indexes: # Add values, ie. sim data items stored at the item, to the list # model. sim_data_items = q_index.internalPointer().values tuples.extend((e.path, e) for e in sim_data_items) # Overwrite all elements in dictionary by selected values # Note, that overwriting only issues one `updated` signal, and thus, # only rerenders the plots one time. Therefore, simply overwriting # is much more efficient, despite it would seem, that selectively # overwriting keys is. self.selectedSimulationDataItemListModel.clear_and_update_from_tuples( tuples) def get_selected_simulation_data_items(self): return [ self.selectedSimulationDataItemListModel[key] for key in self.selectedSimulationDataItemListModel ] def get_plot_data_collection_from_selected_variables(self): """Get a :class: `dict` with y-variable as key and values as items from the current selection of the variable tree. :rtype: :class: `dict` of :class: `string` and :class: `list` """ plot_data_collection = [] for q_index in self.variableTreeView.selectedIndexes(): item = q_index.internalPointer() if len(item.values) > 0: plot_data_collection.extend(item.values) return plot_data_collection def update_variable_tree(self): """Collect all SimDataItems currently selected, and create variable tree and corresponding data from it. Additionaly reselect all previously selected variables. """ # Create a list of all currently selected paths # Note, that *q_index* can not be used directly, because it might # change if the variable tree is recreated selected_path_collection = [] for q_index in self.variableTreeView.selectedIndexes(): selected_path_collection.append( # Create list of identifiers from path # Note, that the root node is excluded from the path [ item.identifier for item in q_index.internalPointer().path[1:] ]) # Join the data of all currently selected items to a dictionary # tree sim_data_items = self.get_selected_simulation_data_items() dict_tree = dict_tree_from_sim_data_items(sim_data_items) # check if qp values are the same # self.check_qp(sim_data_items) # Reset variable tree and update it with *dict_tree* self.variableTreeModel.clear_and_update_from_dict_tree(dict_tree) # Auto expand variable tree self.variableTreeView.expandToDepth(1) # Reselect all variables, which also exist on the new tree for path in selected_path_collection: # Try to reselect, and do nothing, if path does not exist anymore try: # Reselect new item corresponding to the previously selected # path item = self.variableTreeModel.get_item_from_path(*path) self.variableTreeView.selectionModel().select( self.variableTreeModel._get_index_from_item(item), QItemSelectionModel.Select, ) except KeyError: pass # TODO: it might be that some log files do not have a QP value, therefore the check_qp method must be # implemented in a way that these files are not affected # def check_qp(self, sim_data_items): # check if qp values are the same for each sequence # if len(sim_data_items) < 2: return # sim_data_items.sort(key=lambda item: (item.sequence)) # # qp_list,list = [],[] # seq = sim_data_items[0].sequence # config = sim_data_items[0].config # # for item in sim_data_items: # if ((seq == item.sequence) & (config == item.config)): # list.append(item.qp) # elif (seq == item.sequence): #same sequence different config # config = item.config # qp_list.append(list) # list = [] # list.append(item.qp) # else: # different sequence # seq = item.sequence # config = item.config # qp_list.append(list) # if not(all(list == qp_list[0] for list in qp_list)): # QtWidgets.QMessageBox.warning(self, "Warning", # "Be careful! You chose a sequence with different QP.") # return # list, qp_list = [], [] # list.append(item.qp) # qp_list.append(list) # # if not(all(list == qp_list[0] for list in qp_list )): # QtWidgets.QMessageBox.warning(self, "Warning", # "Be careful! You chose a sequence with different QP.") def check_labels(self): selectionmodel = self.variableTreeView.selectionModel() selected = self.variableTreeView.selectedIndexes() # return if no comparison needed if len(selected) < 2: return labelx = [] labely = [] for index in selected: x = index.internalPointer() if len(x.values) > 0: labelx.append(x.values[0].label[0]) labely.append(x.values[0].label[1]) if all(x == labelx[0] for x in labelx) and all(x == labely[0] for x in labely): return else: QtWidgets.QMessageBox.information( self, "Error!", "You should not choose curves with different units.") selectionmodel.clearSelection() # updates the plot if the plot variable is changed def update_plot(self): # user-generated curves and curves loaded from files are not supposed to be mixed user_generated_curves = False if self.sender() == self._variable_tree_selection_model or self.sender( ) == self.curveListSelectionModel: self.check_labels() data_collection = self.get_plot_data_collection_from_selected_variables( ) data_collection_user_generated = [] for index in self.curveListView.selectedIndexes(): data_collection_user_generated.append( self.curveListModel[index.data()]) else: return plot_data_collection = data_collection + data_collection_user_generated if len(data_collection_user_generated): self.plotPreview.tableView.setModel(self.bdUserGeneratedTableModel) self.plotPreview.change_plot(plot_data_collection, True) else: self.plotPreview.tableView.setModel(self.bdTableModel) self.update_table(data_collection) self.plotPreview.change_plot(plot_data_collection, False) if len(data_collection) and len(data_collection_user_generated): # don't mix user-generated and normal curves self.plotPreview.tableView.hide() self.plotPreview.label_warning.show() return self.plotPreview.tableView.show() self.plotPreview.label_warning.hide() self.plotPreview.tableView.model().update( plot_data_collection, self.combo_rate_psnr.currentText(), self.combo_interp.currentText(), not (self.checkBox_bdplot.isChecked())) def get_table_header(self, plot_data_collection): tmp_legend = [] tmp_config = [] # make legend for plot_data in plot_data_collection: tmp = [] for identifiers in plot_data.identifiers[1:]: tmp += identifiers.split(sep) tmp2 = tmp + plot_data.path tmp_legend.append(tmp2) tmp_config.append(tmp) legend = [] config = [] for c in tmp_legend: result = list( filter(lambda x: all(x in l for l in tmp_legend) == False, c)) if result == []: result = [plot_data.path[-1]] legend.append(" ".join(result)) if len(tmp_legend) == 1: legend = [plot_data.path[-1]] #make config for c in tmp_config: result = list( filter(lambda x: all(x in l for l in tmp_config) == False, c)) if ((set([" ".join(result)]) - set(config) != set()) & (result != [])): config.append(" ".join(result)) result = (legend, config) return result #updates the table def update_table(self, plot_data_collection): self.tableWidget.clear() self.tableWidget.setColumnCount(0) self.tableWidget.setRowCount(0) if plot_data_collection != []: if 'Temporal' in plot_data_collection[0].path: self.change_table_temporal(plot_data_collection) else: self.change_table_summary(plot_data_collection) self.tableWidget.resizeColumnsToContents() def change_table_temporal(self, plot_data_collect): plot_data_collection = plot_data_collect self.tableWidget.setRowCount(len(plot_data_collection)) plot_count = data_count = 0 data_names = [] plot_data_collection.sort( key=lambda plot_data: (plot_data.identifiers)) legend = self.get_table_header(plot_data_collection) header = legend[0] for plot_data in plot_data_collection: values = ((float(x), float(y)) for (x, y) in plot_data.values) sorted_value_pairs = sorted(values, key=lambda pair: pair[0]) [xs, ys] = list(zip(*sorted_value_pairs)) # make header if plot_data.identifiers[0] not in data_names: self.tableWidget.insertRow(plot_count) v_item = QtWidgets.QTableWidgetItem( str(plot_data.identifiers[0])) font = self.tableWidget.font() v_item.setData( 6, QtGui.QFont(self.tableWidget.font().setBold(True))) v_item.setData(6, QtGui.QFont("Ubuntu", 11, QtGui.QFont.Bold)) self.tableWidget.setVerticalHeaderItem(plot_count, v_item) header_count = plot_count data_names.append(plot_data.identifiers[0]) plot_count += 1 # round data if plot_data.label[1] == 'dB': ys = tuple(map(lambda i: round(i, 1), ys)) self.tableWidget.horizontalHeader().setVisible(False) # fill up column per column for column_count in range(0, len(xs)): self.tableWidget.setCurrentCell(plot_count, column_count) if column_count > self.tableWidget.currentColumn(): self.tableWidget.insertColumn(column_count) self.tableWidget.setItem( plot_count, column_count, QtWidgets.QTableWidgetItem(str(ys[column_count]))) self.tableWidget.setVerticalHeaderItem( plot_count, QtWidgets.QTableWidgetItem( str(header[data_count]) + " [" + str(plot_data.label[1]) + "] ")) new_item = QtWidgets.QTableWidgetItem(str(xs[column_count])) new_item.setData(6, QtGui.QFont("Ubuntu", 11, QtGui.QFont.Bold)) self.tableWidget.setItem(header_count, column_count, new_item) column_count += 1 plot_count += 1 data_count += 1 def change_table_summary(self, plot_data_collect): plot_data_collection = plot_data_collect header_count = plot_count = data_count = config_count = column_saver = 0 data_names = [] plot_data_collection.sort(key=lambda plot_data: plot_data.path[-1]) plot_data_collection.sort( key=lambda plot_data: plot_data.identifiers[0]) legend = self.get_table_header(plot_data_collection) header = legend[0] config = legend[1] if ((config == []) | (len(config) == 1)): self.change_table_temporal(plot_data_collection) return self.tableWidget.setRowCount(len(plot_data_collection) / len(config)) for plot_data in plot_data_collection: values = ((float(x), float(y)) for (x, y) in plot_data.values) sorted_value_pairs = sorted(values, key=lambda pair: pair[0]) [xs, ys] = list(zip(*sorted_value_pairs)) # make header, important if more than one plot if plot_data.identifiers[0] not in data_names: self.tableWidget.insertRow(plot_count) v_item = QtWidgets.QTableWidgetItem( str(plot_data.identifiers[0])) v_item.setData(6, QtGui.QFont("Ubuntu", 11, QtGui.QFont.Bold)) self.tableWidget.setVerticalHeaderItem(plot_count, v_item) header_count = plot_count data_names.append(plot_data.identifiers[0]) plot_count += 1 # round data if plot_data.label[1] == 'dB': ys = tuple(map(lambda i: round(i, 1), ys)) #horizontal header if more than one config if len(config) > 1: self.tableWidget.horizontalHeader().setVisible(True) else: self.tableWidget.horizontalHeader().setVisible(False) for column_count in range(0, len(xs)): columns = column_saver + column_count if (((column_saver + column_count) >= self.tableWidget.columnCount()) | (self.tableWidget.columnCount() == 0)): self.tableWidget.insertColumn(column_saver + column_count) if plot_count >= self.tableWidget.rowCount(): self.tableWidget.insertRow(plot_count) # units in first row of table new_item = QtWidgets.QTableWidgetItem(plot_data.label[0] + ' | ' + plot_data.label[1]) new_item.setData(6, QtGui.QFont("Ubuntu", 11, QtGui.QFont.Bold)) self.tableWidget.setItem(header_count, column_saver + column_count, new_item) #self.tableWidget.setItem(header_count, column_saver + column_count, QtWidgets.QTableWidgetItem(plot_data.label[0] + ' | ' + plot_data.label[1])) # x and y-value in one cell self.tableWidget.setItem( plot_count, columns, QtWidgets.QTableWidgetItem( str(xs[column_count]) + ' | ' + str(ys[column_count]))) # header self.tableWidget.setHorizontalHeaderItem( column_saver + column_count, QtWidgets.QTableWidgetItem(str(config[config_count]))) column_count += 1 if config[config_count] == header[data_count]: header[data_count] = header[data_count].replace( config[config_count], plot_data.path[-1]) elif config[config_count] in header[data_count]: header[data_count] = header[data_count].replace( config[config_count], '') self.tableWidget.setVerticalHeaderItem( plot_count, QtWidgets.QTableWidgetItem(str(header[data_count]))) column_saver = column_saver + column_count config_count += 1 if config_count == len(config): plot_count += 1 column_saver = config_count = 0 data_count += 1 def update_bd_table(self, index): # update bd table, the index determines the anchor, # if it is non integer per default the first config is regarded as # anchor self.bdTableModel.update_table(self.combo_rate_psnr.currentText(), self.combo_interp.currentText(), index, not (self.checkBox_bdplot.isChecked())) def update_bd_user_generated_curves_table(self, index): clicked_text = self.bdUserGeneratedTableModel.headerData( index, Qt.Vertical, Qt.DisplayRole) self.bdUserGeneratedTableModel.update( None, self.combo_rate_psnr.currentText(), self.combo_interp.currentText(), not (self.checkBox_bdplot.isChecked()), clicked_text) def update_bd_plot(self): data_collection = self.get_plot_data_collection_from_selected_variables( ) data_collection_user_generated = [] for index in self.curveListSelectionModel.selectedIndexes(): data_collection_user_generated.append( self.curveListModel[index.data()]) if len(data_collection): self.bdTableModel.update(data_collection, self.combo_rate_psnr.currentText(), self.combo_interp.currentText(), not (self.checkBox_bdplot.isChecked())) elif len(data_collection_user_generated): self.bdTableModel.update(data_collection_user_generated, self.combo_rate_psnr.currentText(), self.combo_interp.currentText(), not (self.checkBox_bdplot.isChecked())) def export_table_to_csv(self): # remember that the decimal mark is '.' if self.tableWidget.rowCount() > 0: path, extension = QtWidgets.QFileDialog.getSaveFileName( self, 'Save Table View as', '.', 'CSV (*.csv)') if path != '': if '.csv' not in path: path += '.csv' with open(str(path), 'w', newline='') as stream: writer = csv.writer(stream) for row in range(self.tableWidget.rowCount()): rowdata = [] rowdata.append( str( self.tableWidget.verticalHeaderItem(row).data( 0))) #data(0) = data(Qt.displayRole) for column in range(self.tableWidget.columnCount()): item = self.tableWidget.item(row, column) if item is not None: rowdata.append(str(item.text())) else: rowdata.append('') writer.writerow(rowdata) def save_bd_table(self): if self.bdTableModel.rowCount(self) == 0: return filename, extension = QtWidgets.QFileDialog.getSaveFileName( self, 'Save Table as', '.', 'Latex (*.tex)') if filename != '': if '.tex' not in filename: filename += '.tex' self.bdTableModel.export_to_latex(filename) def on_combo_box(self): # just update the bd table but do not change the anchor self.update_bd_table(-1) def save_current_selection(self): """Saves the current selected sim data item collection""" if not self.get_selected_simulation_data_items(): msg = QtWidgets.QMessageBox(self) # use self as parent here msg.setIcon(QtWidgets.QMessageBox.Information) msg.setText( "You did not select any simulation data item to store\n" "Please make a selection and try again.") msg.setWindowTitle("Info") msg.show() return filename, extension = QtWidgets.QFileDialog.getSaveFileName( self, 'Save RD data as', '.', 'RDPlot (*.rd)') if filename != '': if '.rd' not in filename: filename += '.rd' f = open(filename, 'w') f.write( jsonpickle.encode(self.get_selected_simulation_data_items())) f.close() def process_cmd_line_args(self, args): """Processes cmd line arguments. Those are only pathes or files.""" for path in args[1:]: if not isdir(path) and not isfile(path): continue if path.endswith('.rd'): f = open(path, 'r') json_str = f.read() sim_data_items = jsonpickle.decode(json_str) self.simDataItemTreeModel.update(sim_data_items, False) f.close() continue self.simDataItemTreeView.msg.show() self.simDataItemTreeView.parserThread.add_path(path) self.simDataItemTreeView.parserThread.start() def open_about_page(self): """Opens and displays an Html About file""" try: html_path = path.abspath(here + '/docs/about.html') html_file = open(html_path, 'r', encoding='utf-8', errors='ignore') source_code = html_file.read() try: f = open(here + '/version.txt', 'r') app_version = f.readline() except: app_version = 'could not detect version' source_code = source_code.replace("##VERSION##", app_version) source_code = source_code.replace("##here##", here) about_dialog = QtWidgets.QDialog(self) about_dialog.setWindowTitle("About RDPlot") about_dialog.setMaximumSize(950, 800) about_text = QtWidgets.QTextBrowser(about_dialog) about_text.setMinimumWidth(950) about_text.setMinimumHeight(800) about_text.setHtml(source_code) about_text.setOpenExternalLinks(True) about_text.show() about_dialog.exec_() about_dialog.close() about_text.close() except IOError: html_error = QtWidgets.QMessageBox() html_error.setIcon(QtWidgets.QMessageBox.Critical) html_error.setText("Error opening about or help") html_error.setInformativeText( "The html file from the resource could not be loaded.") html_error.exec_() def generate_new_curve(self): plot_data_collection = self.get_plot_data_collection_from_selected_variables( ) if plot_data_collection: new_plot_values = [] for _plot_data in plot_data_collection: new_plot_values.extend(_plot_data.values) if len(new_plot_values) < 4: QtWidgets.QMessageBox.warning( self, "Warning!", "You didn't select at least 4 points.") else: curve_name, ok = QtWidgets.QInputDialog.getText( self, "New curve", "Please enter a name for the new curve.\n" "If you enter an already existing name,\nits data will be overwritten." ) curve_name = curve_name.strip() if curve_name is not '': new_plot_data = PlotData([curve_name], new_plot_values, [], plot_data_collection[0].label) self.add_curve(curve_name, new_plot_data) else: QtWidgets.QMessageBox.warning( self, "Warning!", "Please enter a valid name.") else: QtWidgets.QMessageBox.warning( self, "Warning!", "You didn't select at least 4 points.") def add_curve(self, name, data): if self.curveWidget.isHidden(): self.curveWidget.show() data_tuple = (name, data) self.curveListModel.update_from_tuples((data_tuple, )) # all_indexes = QItemSelection(self.curveListModel.index(0), # self.curveListModel.index(self.curveListModel.rowCount(QModelIndex()))) # self.curveListSelectionModel.select(all_indexes, QItemSelectionModel.Clear) # self.curveListSelectionModel.select(self.curveListModel.index(self.curveListModel.rowCount(QModelIndex())-1), # QItemSelectionModel.Select) # self.curveListView.setFocus() def remove_curves(self): # todo integrate bjontegaard for generated curves(should be fully functional already) curves_to_remove = [] for index in self.curveListSelectionModel.selectedIndexes(): curves_to_remove.append(index.data()) self.curveListModel.remove_keys(curves_to_remove) if len(self.curveListModel) > 0: self.curveListSelectionModel.select(self.curveListModel.index(0), QItemSelectionModel.Select) else: self.curveWidget.hide() self.update_plot() def get_recent_files(self): recent_files = self.settings.value('recentFiles') if recent_files is not None: for recent_file in recent_files: if path.exists(recent_file): action = self.menuRecent_files.addAction(recent_file) action.triggered.connect(self.open_recent_file) def open_recent_file(self): path_recent = self.sender().text() if path.isdir(path_recent): self.simDataItemTreeView.add_folder(path_recent) else: self.simDataItemTreeView.add_file(path_recent) def add_recent_files(self, files, reload): # files doesn't necessarily have to just be a list of files # it can also be a directory if not reload: recent_files = self.settings.value('recentFiles') if recent_files is None: recent_files = [] for file in files: if file in recent_files: # put our file on top of the list recent_files.remove(file) recent_files.insert(0, file) while len(recent_files) > 5: del recent_files[-1] self.settings.setValue('recentFiles', recent_files) self.menuRecent_files.clear() for recent_file in recent_files: if path.exists(recent_file): action = self.menuRecent_files.addAction(recent_file) action.triggered.connect(self.open_recent_file) def add_files_to_watcher(self, items): for item in items: if isfile(item.path): self.watcher.addPath(item.path) def warning_file_change(self, path_item): # inform user about the fact that one of the loaded files has been changed since the application has started # timer is used to avoid spamming the user when multiple files are deleted in a row # retrieve affected notes and parent nodes # change their style in the tree view to indicate which files are affected if self.show_file_changed_message: self.show_file_changed_message = False self.reset_timer.start() QtWidgets.QMessageBox.warning( self, 'File change', 'One or more of your loaded files have been changed.\n' 'You can choose to reload them.\n' 'Hint: Changed files are greyed out in the sequences widget.') else: self.reset_timer.stop() self.reset_timer.start() affected_notes = [] for leaf in self.simDataItemTreeModel.root.leafs: for value in leaf.values: if value.path == path_item: affected_notes.append(leaf) for node in affected_notes: node_index = self.simDataItemTreeModel._get_index_from_item(node) node.setProperty('needs_reload', 'True') parent = self.simDataItemTreeModel.parent(node_index) level = 0 while parent.isValid() and level < 2: #MAX_LEVEL parent.internalPointer().setProperty('needs_reload', 'True') parent = self.simDataItemTreeModel.parent(parent) level += 1 def _reset_file_changed_message(self): self.show_file_changed_message = True def reload_files(self): # remove all selected files first # reload available files # could possibly limit this to only files that we know have been changed def check_children(parent): if len(parent.children) > 0: for child in parent.children: if not check_children(child): return False parent.setProperty('needs_reload', 'False') return True else: if parent.property('needs_reload') == 'True': return False return True values = self.selectedSimulationDataItemListModel.values() if len(values) == 0: for index in self.simDataItemTreeModel.root.leafs: for sim_data_item in index.values: values.append(sim_data_item) items_to_be_reloaded = [] for value in values: if path.exists(value.path): # reload file items_to_be_reloaded.append(value) self._variable_tree_selection_model.selectionChanged.disconnect() self._selection_model.selectionChanged.disconnect(self.change_list) self.simDataItemTreeModel.remove(values) self.simDataItemTreeView.msg.show() for item in items_to_be_reloaded: self.simDataItemTreeView.add_file(item.path, reload=True) self.change_list(QItemSelection(), QItemSelection()) self._selection_model.selectionChanged.connect(self.change_list) self._variable_tree_selection_model.selectionChanged.connect( self.update_plot) for node in self.simDataItemTreeModel.root.children: # remove grey font color if all changed files have been reloaded # have to check every single item because possible deletion of older nodes makes things very difficult check_children(node) def show_sequences_context_menu(self, position): self.menuEdit.exec(self.simDataItemTreeView.mapToGlobal(position))
class SettingsDialog(QDialog): """In order to use a class as an option add it to self.widgets""" def __init__(self, controls, parent=None, status=None): QDialog.__init__(self, parent) self.setWindowTitle("puddletag settings") winsettings('settingswin', self) built_in = [ (translate("Settings", 'General'), GeneralSettings(controls), controls), (translate("Settings", 'Confirmations'), confirmations.Settings(), None), (translate("Settings", 'Mappings'), TagMappings(), None), (translate("Settings", 'Playlist'), Playlist(), None), (translate("Settings", 'Colours'), ColorEdit(), status['table']), (translate("Settings", 'Genres'), genres.Genres(status=status), None), (translate("Settings", 'Tags'), Tags(status=status), status['table']), (translate("Settings", 'Plugins'), PluginConfig(), None), (translate("Settings", 'Shortcuts'), ActionEditorDialog(status['actions']), None), ] d = dict(enumerate(built_in)) i = len(d) for control in controls: if hasattr(control, SETTINGSWIN): c = getattr(control, SETTINGSWIN)(status=status) d[i] = [c.title, c, control] i += 1 for c in config_widgets: d[i] = [c.title, c(status=status), None] i += 1 self._widgets = d self.listbox = SettingsList() self.model = ListModel(d) self.listbox.setModel(self.model) self.stack = QStackedWidget() self.stack.setFrameStyle(QFrame.StyledPanel) self.grid = QGridLayout() self.grid.addWidget(self.listbox) self.grid.addWidget(self.stack, 0, 1) self.grid.setColumnStretch(1, 2) self.setLayout(self.grid) self.listbox.selectionChangedSignal.connect(self.showOption) selection = QItemSelection() self.selectionModel = QItemSelectionModel(self.model) index = self.model.index(0, 0) selection.select(index, index) self.listbox.setSelectionModel(self.selectionModel) self.selectionModel.select(selection, QItemSelectionModel.Select) self.okbuttons = OKCancel() self.okbuttons.okButton.setDefault(True) self.grid.addLayout(self.okbuttons, 1, 0, 1, 2) self.okbuttons.ok.connect(self.saveSettings) self.accepted.connect(self.saveSettings) self.okbuttons.cancel.connect(self.close) def showOption(self, option): widget = self._widgets[option][1] stack = self.stack if stack.indexOf(widget) == -1: stack.addWidget(widget) stack.setCurrentWidget(widget) if self.width() < self.sizeHint().width(): self.setMinimumWidth(self.sizeHint().width()) def saveSettings(self): for z in self._widgets.values(): try: z[1].applySettings(z[2]) except SettingsError as e: QMessageBox.warning(self, 'puddletag', translate('Settings', 'An error occurred while saving the settings of <b>%1</b>: %2').arg(z[0]).arg(str(e))) return self.close()
class GraphDigitGraphicsView(QGraphicsView): sigMouseMovePoint = pyqtSignal(QPoint, QPointF) sigModified = pyqtSignal(bool) sigNewCurveAdded = pyqtSignal() # 自定义信号sigMouseMovePoint,当鼠标移动时,在mouseMoveEvent事件中,将当前的鼠标位置发送出去 # QPoint--传递的是view坐标 def __init__(self, parent=None): super(GraphDigitGraphicsView, self).__init__(parent) # scene rect = QRectF(0, 0, 300, 400) self.scene = QGraphicsScene(rect) # 创建场景 参数:场景区域 self.setScene(self.scene) # 给视图窗口设置场景 # image self.graphicsPixmapItem = QGraphicsPixmapItem() # chart image self.scene.addItem(self.graphicsPixmapItem) # setAntialias self.setRenderHint(QPainter.Antialiasing) # image object stored in project data # item objects storage self.axesxObjs = {} self.axesyObjs = {} self.gridObjs = [] self.curveObjs = {} self.pointObjs = {} # axis coord stored in project data # grid setting stored in project data # grid position self.gridxpos = [] self.gridypos = [] # axes curve and point datamodel self.axesxModel = QStandardItemModel() self.axesyModel = QStandardItemModel() self.axesxSelectModel = QItemSelectionModel(self.axesxModel) self.axesySelectModel = QItemSelectionModel(self.axesyModel) self.curveModel = QStandardItemModel() self.pointsModel = QStandardItemModel() self.curveModel.setHorizontalHeaderLabels( ["current", "name", "visible"]) self.pointsModel.setHorizontalHeaderLabels(["order", "x", "y"]) self.axesxModel.setHorizontalHeaderLabels(["position", "x"]) self.axesyModel.setHorizontalHeaderLabels(["position", "y"]) ### self.xNo = 0 self.yNo = 0 ### self.mode = OpMode.default # data manage self.proj = Digi() # init self.currentCurve = None self.pointsModel.itemChanged.connect(self.changePointOrder) self.curveModel.itemChanged.connect(self.changeCurveVisible) self.scene.selectionChanged.connect(self.slotSceneSeletChanged) # state self._lastCurve = None def load(self, proj): # image # only for load self.scaleGraphImage(proj.imgScale) # axes xadded = [] yadded = [] for xpos, xcoord in proj.data["axesxObjs"].items(): if xpos in xadded: continue else: xadded.append(xpos) item = QGraphicsAxesItem( 0, self.scene.sceneRect().y(), 0, self.scene.sceneRect().y() + self.scene.sceneRect().height()) item.setFlags(QGraphicsItem.ItemIsSelectable | QGraphicsItem.ItemIsFocusable | QGraphicsItem.ItemIsMovable) item.setPos(xpos, 0) item.axis = "x" item.setPen(QPen(Qt.red, 1, Qt.DashLine)) self.scene.addItem(item) self.axesxObjs[item] = xcoord labelitem = QStandardItem("x:{}".format(xpos)) labelitem.setData(item) self.axesxModel.appendRow([labelitem, QStandardItem(str(xcoord))]) self.xNo += 1 self.axesxModel.setVerticalHeaderItem( self.axesxModel.rowCount() - 1, QStandardItem("x{}".format(self.xNo))) for ypos, ycoord in proj.data["axesyObjs"].items(): if ypos in yadded: continue else: yadded.append(ypos) item = QGraphicsAxesItem( self.scene.sceneRect().x(), 0, self.scene.sceneRect().x() + self.scene.sceneRect().width(), 0) item.setFlags(QGraphicsItem.ItemIsSelectable | QGraphicsItem.ItemIsFocusable | QGraphicsItem.ItemIsMovable) item.setPos(0, ypos) item.axis = "y" item.setPen(QPen(Qt.red, 1, Qt.DashLine)) self.scene.addItem(item) self.axesyObjs[item] = ycoord labelitem = QStandardItem("y:{}".format(ypos)) labelitem.setData(item) self.axesyModel.appendRow([labelitem, QStandardItem(str(ycoord))]) # curve for curve in proj.data["curves"]: self.pointObjs[curve] = [] self.addCurve(curve) clr = RandItem.nextColor() for x, y in proj.data["curves"][curve]: ptitem = QGraphicsPointItem() ptitem.pointColor = clr ptitem.linewidth = 1 ptitem.setPos(x, y) ptitem.parentCurve = curve ptitem.setFlags(QGraphicsItem.ItemIsSelectable | QGraphicsItem.ItemIsFocusable | QGraphicsItem.ItemIsMovable) i = pointInsertPosition(ptitem, self.pointObjs[self.currentCurve]) self.pointObjs[self.currentCurve].insert(i, ptitem) self.scene.addItem(ptitem) self.updateCurve(curve) # grid self.calGridCoord() self.updateGrid() self.sigModified.emit(True) self.mode = OpMode.select def resetview(self): self.setGraphImage(None) self.scaleGraphImage(1) for obj in self.axesxObjs: self.scene.removeItem(obj) self.axesxObjs = {} for obj in self.axesyObjs: self.scene.removeItem(obj) self.axesyObjs = {} for obj in self.gridObjs: self.scene.removeItem(obj) self.gridObjs = [] for curve in self.curveObjs: for obj in self.curveObjs[curve]: self.scene.removeItem(obj) self.curveObjs = {} for curve in self.pointObjs: for obj in self.pointObjs[curve]: self.scene.removeItem(obj) self.pointObjs = {} self.axesxModel.clear() self.axesyModel.clear() self.curveModel.clear() self.pointsModel.clear() self.axesxSelectModel.clear() self.axesySelectModel.clear() self.curveModel.setHorizontalHeaderLabels( ["current", "name", "visible"]) self.pointsModel.setHorizontalHeaderLabels(["order", "x", "y"]) self.axesxModel.setHorizontalHeaderLabels(["position", "x"]) self.axesyModel.setHorizontalHeaderLabels(["position", "y"]) self.xNo = 0 self.yNo = 0 ### self.mode = OpMode.default # data manage self.proj.resetData(True) # init self.currentCurve = None self._lastCurve = None def dump(self): proj = self.proj proj.data["axesxObjs"] = {} # [x1,x2,x3] proj.data["axesyObjs"] = {} # [y1,y2,y3] proj.data["curves"] = {} # {'default':(x1,y1),(x2,y2)} # axes for item, xcoord in self.axesxObjs.items(): proj.data["axesxObjs"][item.pos().x()] = xcoord for item, ycoord in self.axesyObjs.items(): proj.data["axesyObjs"][item.pos().y()] = ycoord # curve for curve in self.pointObjs: proj.data["curves"][curve] = [] for item in self.pointObjs[curve]: proj.data["curves"][curve].append((item.x(), item.y())) def mouseMoveEvent(self, evt): pt = evt.pos() # 获取鼠标坐标--view坐标 self.sigMouseMovePoint.emit(pt, self.mapToScene(pt)) # 发送鼠标位置 QGraphicsView.mouseMoveEvent(self, evt) item = self.scene.focusItem() if not item: items = self.scene.selectedItems() if len(items) != 1: return else: item = items[0] if item: if isinstance(item, QGraphicsPointItem) and item.parentCurve: # self.changeCurrentCurve(item.parentCurve) self.updateCurve(self.currentCurve, Qt.red) self.updateCurvePoints(self.currentCurve) self.sigModified.emit(True) elif isinstance(item, QGraphicsAxesItem): if item.axis == "x": if self.mode != OpMode.axesx: self.scene.clearSelection() return self.axesxSelectModel.clearSelection() self.axesySelectModel.clearSelection() for i in range(self.axesxModel.rowCount()): if self.axesxModel.item(i, 0).data() is item: parent = QModelIndex() topleftindex = self.axesxModel.index( i, 0, parent) # self.axesxModel.item(i,0).index() rightbottomindex = self.axesxModel.index( i, 1, parent) self.axesxSelectModel.select( QItemSelection(topleftindex, rightbottomindex), QItemSelectionModel.Select) self.axesxModel.item(i, 0).setText("x:{}".format( evt.pos().x())) break item.setLine( 0, self.scene.sceneRect().y(), 0, self.scene.sceneRect().y() + self.scene.sceneRect().height()) item.setPos(self.mapToScene(evt.pos()).x(), 0) self.sigModified.emit(True) self.calGridCoord() self.updateGrid() elif item.axis == "y": if self.mode != OpMode.axesy: self.scene.clearSelection() return self.axesxSelectModel.clearSelection() self.axesySelectModel.clearSelection() for i in range(self.axesyModel.rowCount()): if self.axesyModel.item(i, 0).data() is item: topleftindex = self.axesyModel.index( i, 0) # self.axesxModel.item(i,0).index() rightbottomindex = self.axesyModel.index(i, 1) self.axesySelectModel.select( QItemSelection(topleftindex, rightbottomindex), QItemSelectionModel.Select) self.axesyModel.item(i, 0).setText("y:{}".format( evt.pos().y())) break item.setLine( self.scene.sceneRect().x(), 0, self.scene.sceneRect().x() + self.scene.sceneRect().width(), 0) item.setPos(0, self.mapToScene(evt.pos()).y()) self.sigModified.emit(True) self.calGridCoord() self.updateGrid() # self.updateCurve(self.currentCurve) # self.repaint() # self.setDragMode(QGraphicsView.NoDrag) #(RubberBandDrag) #ScrollHandDrag) #NoDrag) def mousePressEvent(self, event): super().mousePressEvent(event) self.__pressPt = event.pos() ptscene = self.mapToScene(event.pos()) if self.mode is OpMode.axesx: for axisitem in self.axesxObjs: if abs(ptscene.x() - axisitem.pos().x()) < 5: self.scene.clearSelection() axisitem.setSelected(True) return elif self.mode is OpMode.axesy: for axisitem in self.axesyObjs: if abs(ptscene.y() - axisitem.pos().y()) < 5: self.scene.clearSelection() axisitem.setSelected(True) return def keyPressEvent(self, event: QKeyEvent) -> None: # super().keyPressEvent(event) item = self.scene.focusItem() if not item: items = self.scene.selectedItems() if len(items) != 1: return else: item = items[0] if item: if isinstance(item, QGraphicsPointItem) and item.parentCurve: if event.key() == Qt.Key_Up: item.setPos(item.pos().x(), item.pos().y() - 1) elif event.key() == Qt.Key_Down: item.setPos(item.pos().x(), item.pos().y() + 1) elif event.key() == Qt.Key_Left: item.setPos(item.pos().x() - 1, item.pos().y()) elif event.key() == Qt.Key_Right: item.setPos(item.pos().x() + 1, item.pos().y()) else: return self.updateCurve(self.currentCurve, Qt.red) self.updateCurvePoints(self.currentCurve) self.sigModified.emit(True) elif isinstance(item, QGraphicsAxesItem): if item.axis == "x": if event.key() == Qt.Key_Left: item.moveBy(-1, 0) elif event.key() == Qt.Key_Right: item.moveBy(1, 0) else: return self.sigModified.emit(True) self.calGridCoord() self.updateGrid() self.axesxSelectModel.clearSelection() self.axesySelectModel.clearSelection() for i in range(self.axesxModel.rowCount()): if self.axesxModel.item(i, 0).data() is item: parent = QModelIndex() topleftindex = self.axesxModel.index( i, 0, parent) # self.axesxModel.item(i,0).index() rightbottomindex = self.axesxModel.index( i, 1, parent) self.axesxSelectModel.select( QItemSelection(topleftindex, rightbottomindex), QItemSelectionModel.Select) self.axesxModel.item(i, 0).setText("x:{}".format( item.pos().x())) break elif item.axis == "y": if event.key() == Qt.Key_Up: item.setPos(0, item.pos().y() - 1) elif event.key() == Qt.Key_Down: item.setPos(0, item.pos().y() + 1) else: return self.sigModified.emit(True) self.calGridCoord() self.updateGrid() self.axesxSelectModel.clearSelection() self.axesySelectModel.clearSelection() for i in range(self.axesyModel.rowCount()): if self.axesyModel.item(i, 0).data() is item: topleftindex = self.axesyModel.index( i, 0) # self.axesxModel.item(i,0).index() rightbottomindex = self.axesyModel.index(i, 1) self.axesySelectModel.select( QItemSelection(topleftindex, rightbottomindex), QItemSelectionModel.Select) self.axesyModel.item(i, 0).setText("y:{}".format( item.pos().y())) break def mouseReleaseEvent(self, event): super().mouseReleaseEvent(event) ptscene = self.mapToScene(event.pos()) # item = self.scene.itemAt(ptscene, self.transform()) clicked = True if event.pos() == self.__pressPt else False if self.mode is OpMode.select: pass # super().mousePressEvent(event) elif self.mode is OpMode.axesx and clicked: self.axesxSelectModel.clear() self.axesySelectModel.clear() for axisitem in self.axesxObjs: if abs(ptscene.x() - axisitem.pos().x()) < 5: self.scene.clearSelection() axisitem.setSelected(True) return item = QGraphicsAxesItem( 0, self.scene.sceneRect().y(), 0, self.scene.sceneRect().y() + self.scene.sceneRect().height()) item.setPos(ptscene.x(), 0) item.axis = "x" item.setPen(QPen(Qt.red, 1, Qt.DashLine)) self.scene.addItem(item) item.setFlags(QGraphicsItem.ItemIsSelectable | QGraphicsItem.ItemIsFocusable | QGraphicsItem.ItemIsMovable) xs = list(self.axesxObjs.values()) if not self.axesxObjs: nextx = 0 elif len(self.axesxObjs) == 1: nextx = xs[0] + 0.1 else: nextx = 2 * xs[-1] - xs[-2] x, okPressed = QInputDialog.getDouble( self, self.tr("set x coordiniate"), self.tr("set the x coord for axis"), nextx, decimals=3) if okPressed and x not in self.axesxObjs.values(): self.axesxObjs[item] = x labelItem = QStandardItem("x:{}".format(item.pos().x())) labelItem.setData(item) self.axesxModel.appendRow([labelItem, QStandardItem(str(x))]) self.calGridCoord("x") self.updateGrid() # item.setSelected(True) for i in range(self.axesxModel.rowCount()): if self.axesxModel.item(i, 0).data() is item: topleftindex = self.axesxModel.index( i, 0) # self.axesxModel.item(i,0).index() rightbottomindex = self.axesxModel.index(i, 1) self.axesxSelectModel.select( QItemSelection(topleftindex, rightbottomindex), QItemSelectionModel.Select) break self.sigModified.emit(True) self.scene.clearSelection() else: self.scene.removeItem(item) elif self.mode is OpMode.axesy and clicked: self.axesxSelectModel.clear() self.axesySelectModel.clear() for axisitem in self.axesyObjs: if abs(ptscene.y() - axisitem.pos().y()) < 5: self.scene.clearSelection() axisitem.setSelected(True) return item = QGraphicsAxesItem( self.scene.sceneRect().x(), 0, self.scene.sceneRect().x() + self.scene.sceneRect().width(), 0) item.setPos(0, ptscene.y()) item.axis = "y" item.setPen(QPen(Qt.red, 1, Qt.DashLine)) self.scene.addItem(item) item.setFlags(QGraphicsItem.ItemIsSelectable | QGraphicsItem.ItemIsFocusable | QGraphicsItem.ItemIsMovable) ys = list(self.axesyObjs.values()) if not self.axesyObjs: nexty = 0 elif len(self.axesyObjs) == 1: nexty = ys[0] + 0.1 else: nexty = 2 * ys[-1] - ys[-2] y, okPressed = QInputDialog.getDouble( self, self.tr("set y coordiniate"), self.tr("set the y coord for axis"), nexty, decimals=3) if okPressed and y not in self.axesyObjs.values(): self.axesyObjs[item] = y labelItem = QStandardItem("y:{}".format(item.pos().y())) labelItem.setData(item) self.axesyModel.appendRow([labelItem, QStandardItem(str(y))]) self.calGridCoord("y") self.updateGrid() # item.setSelected(True) for i in range(self.axesyModel.rowCount()): if self.axesyModel.item(i, 0).data() is item: topleftindex = self.axesyModel.index( i, 0) # self.axesxModel.item(i,0).index() rightbottomindex = self.axesyModel.index(i, 1) self.axesySelectModel.select( QItemSelection(topleftindex, rightbottomindex), QItemSelectionModel.Select) break self.sigModified.emit(True) else: self.scene.removeItem(item) elif self.mode is OpMode.curve and clicked: self.sigMouseMovePoint.emit(event.pos(), ptscene) if len(self.curveObjs) == 0: self.addCurve('curve1') if self.currentCurve not in self.pointObjs: self.pointObjs[self.currentCurve] = [] ptitem = QGraphicsPointItem() ptitem.pointColor = Qt.blue ptitem.linewidth = 1 ptitem.setPos(ptscene) ptitem.parentCurve = self.currentCurve ptitem.setFlags(QGraphicsItem.ItemIsSelectable | QGraphicsItem.ItemIsFocusable | QGraphicsItem.ItemIsMovable) self.scene.addItem(ptitem) i = pointInsertPosition(ptitem, self.pointObjs[self.currentCurve]) self.pointObjs[self.currentCurve].insert(i, ptitem) self.updateCurve(self.currentCurve, Qt.red) self.sigModified.emit(True) ptitem.setSelected(True) # item1=QGraphicsRectItem(rect) #创建矩形---以场景为坐标 # item1.setFlags(QGraphicsItem.ItemIsSelectable|QGraphicsItem.ItemIsFocusable|QGraphicsItem.ItemIsMovable) #给图元设置标志 # QGraphicsItem.ItemIsSelectable---可选择 # QGraphicsItem.ItemIsFocusable---可设置焦点 # QGraphicsItem.ItemIsMovable---可移动 # QGraphicsItem.ItemIsPanel--- # self.scene.addItem(item1) #给场景添加图元 def slotSceneSeletChanged(self): items = self.scene.selectedItems() if len(items) != 1: # ony allow select one item self.scene.clearSelection() return # item = items[0] item = self.scene.focusItem() if not item: items = self.scene.selectedItems() if len(items) != 1: return else: item = items[0] if item: if isinstance(item, QGraphicsPointItem) and item.parentCurve: self.changeCurrentCurve(item.parentCurve) def deleteItem(self, item): """delete point on curve or axis object""" curvechange = None if isinstance(item, QGraphicsPointItem): for curvename, pointitems in self.pointObjs.items(): for ptitem in pointitems: if ptitem is item: curvechange = curvename pointitems.remove(ptitem) self.scene.removeItem(item) self.sigModified.emit(True) break if curvechange: self.updateCurve(curvechange) if isinstance(item, QGraphicsAxesItem): if item.axis == "x": for line in self.axesxObjs: if line is item: for i in range(self.axesxModel.rowCount()): if self.axesxModel.item(i, 0).data( ) is item: # float(self.axesxModel.item(i, 0).text().strip("x:")) == line.pos().x(): self.axesxModel.removeRow(i) break self.axesxObjs.pop(line) self.scene.removeItem(line) self.sigModified.emit(True) break elif item.axis == "y": for line in self.axesyObjs: if line is item: for i in range(self.axesyModel.rowCount()): if float( self.axesyModel.item(i, 0).text().strip( "y:")) == line.pos().y(): self.axesyModel.removeRow(i) break self.axesyObjs.pop(line) self.scene.removeItem(line) self.sigModified.emit(True) break self.calGridCoord() self.updateGrid() def deleteSelectedItem(self): pointitems = self.scene.selectedItems() if len(pointitems) == 1: self.deleteItem(pointitems[0]) def setGraphImage(self, imgfile): if not isinstance(imgfile, str): img = QPixmap(300, 400) img.fill(QColor("#EEE")) self.graphicsPixmapItem.setPixmap(img) self.scene.setSceneRect(0, 0, 300, 400) elif os.path.exists(imgfile): img = QPixmap(imgfile) self.graphicsPixmapItem.setPixmap(img) self.scene.setSceneRect(0, 0, img.width(), img.height()) self.scene.clearSelection() # 【清除选择】 self.sigModified.emit(True) return True else: return False def scaleGraphImage(self, scale=1): if scale and scale > 0: self.proj.imgScale = scale self.graphicsPixmapItem.setScale(scale) if self.graphicsPixmapItem.pixmap().width( ) > 0 and self.graphicsPixmapItem.pixmap().height() > 0: self.scene.setSceneRect( 0, 0, self.graphicsPixmapItem.pixmap().width() * scale, self.graphicsPixmapItem.pixmap().height() * scale) self.scene.clearSelection() # 【清除选择】 self.sigModified.emit(True) def addCurve(self, name=None): if not name: name = nextName(self.currentCurve) while (name in self.curveObjs): name = nextName(name) self.curveObjs[name] = [] self.pointObjs[name] = [] item1 = IconItem() item2 = QStandardItem(name) item3 = QStandardItem() item1.setEditable(False) item3.setCheckable(True) item3.setAutoTristate(False) item3.setEditable(False) item3.setCheckState(Qt.Checked) item1.setTextAlignment(Qt.AlignCenter) item2.setTextAlignment(Qt.AlignCenter) item3.setTextAlignment(Qt.AlignCenter) self.curveModel.appendRow([item1, item2, item3]) self.changeCurrentCurve(name) self.sigModified.emit(True) self.sigNewCurveAdded.emit() def renameCurve(self, newname=None, name=None): if name not in self.curveObjs: name = self.currentCurve if not newname: newname, okPressed = QInputDialog.getText( self, self.tr("change curve name"), self.tr("Curve to be renamed:{}".format(name)), QLineEdit.Normal, name) if okPressed and newname != '': if newname != name: self.curveObjs[newname] = self.curveObjs.pop(name) self.pointObjs[newname] = self.pointObjs.pop(name) for i in range(self.curveModel.rowCount()): item = self.curveModel.item(i, 1) if item.text() == name: item.setText(newname) self.sigModified.emit(True) self.changeCurrentCurve(newname) def delCurve(self, name=None): if name is None: name = self.currentCurve if name not in self.curveObjs: return try: self.curveObjs.pop(name) self.pointObjs.pop(name) for i in range(self.curveModel.rowCount()): item = self.curveModel.item(i, 1) if item.text() == name: self.curveModel.removeRows(i, 1) self.sigModified.emit(True) if len(self.curveObjs) > 0: self.changeCurrentCurve(list(self.curveObjs.keys())[0]) else: self.currentCurve = None except: pass def showCurve(self, curvename, visible=True): for pts in self.pointObjs[curvename]: pts.setVisible(visible) for line in self.curveObjs[curvename]: line.setVisible(visible) def updateCurve(self, name, color=Qt.black): # if name in self.curveObjs: # curveitem = self.curveObjs[name] # else: # curveitem = QGraphicsPathItem() # self.scene.addItem(curveitem) # # path=curveitem.path() # path = QPainterPath() # # pointItems = self.curvePointObjs[name] # if len(pointItems) > 0: # path.moveTo(pointItems[0].pos()) # for pointitem in pointItems[1:]: # path.lineTo(pointitem.pos()) # curveitem.setPath(path) # curveitem.update(curveitem.boundingRect()) # curveitem.prepareGeometryChange() # self.scene.update() # self.viewport().repaint() # self.viewport().update() if not isinstance(name, str): return if name not in self.pointObjs: return lastitems = [] if name in self.curveObjs: lastitems = self.curveObjs[name] if not isinstance(lastitems, list): lastitems = [] if name in self.pointObjs: pointItems = self.pointObjs[name] else: pointItems = [] points = [] for ptitem in pointItems: points.append(ptitem.pos()) self.curveObjs[name] = [] if len(points) > 1: for i in range(1, len(points)): l = QGraphicsLineItem(points[i - 1].x(), points[i - 1].y(), points[i].x(), points[i].y()) l.setPen(color) l.setZValue(10) # l.setFlag(QGraphicsItem.ItemIsSelectable) self.curveObjs[name].append(l) self.scene.addItem(l) for line in lastitems: self.scene.removeItem(line) self.updateCurvePoints(name) def showAxes(self, visible=True): if visible: for line in self.axesxObjs: line.setVisible(True) for line in self.axesyObjs: line.setVisible(True) else: for line in self.axesxObjs: line.setVisible(False) for line in self.axesyObjs: line.setVisible(False) def showGrid(self, visible=True): if visible: for line in self.gridObjs: line.setVisible(True) else: for line in self.gridObjs: line.setVisible(False) def calGridCoord(self, mode="all"): """calc the coord and pixel position of gridx list and gridy list""" if mode in ("x", "all") and len(self.axesxObjs) >= 2: axesxcoord = list(self.axesxObjs.values()) xmin = min(axesxcoord) if self.proj.gridx[0] is None else min( self.proj.gridx[0], min(axesxcoord)) xmax = max(axesxcoord) if self.proj.gridx[1] is None else max( self.proj.gridx[1], max(axesxcoord)) if self.proj.gridx[2] is None: if len(self.axesxObjs) == 2: xstep = (xmax - xmin) / 5 else: axesStep = round(abs(axesxcoord[1] - axesxcoord[0]), 5) for i in range(2, len(axesxcoord)): st = round(abs(axesxcoord[i] - axesxcoord[i - 1]), 5) if axesStep > st: axesStep = st xstep = axesStep else: xstep = self.proj.gridx[2] n = int(round((xmax - xmin) / xstep, 0)) + 1 gridxcoord = list(np.linspace(xmin, xmax, n)) else: gridxcoord = [] if mode in ("y", "all") and len(self.axesyObjs) >= 2: axesycoord = list(self.axesyObjs.values()) ymin = min(axesycoord) if self.proj.gridy[0] is None else min( self.proj.gridy[0], min(axesycoord)) ymax = max(axesycoord) if self.proj.gridy[1] is None else max( self.proj.gridy[1], max(axesycoord)) if self.proj.gridy[2] is None: if len(self.axesyObjs) == 2: ystep = (ymax - ymin) / 5 else: axesStep = round(abs(axesycoord[1] - axesycoord[0]), 5) for i in range(2, len(axesycoord)): st = round(axesycoord[i] - axesycoord[i - 1], 5) if axesStep > st: axesStep = st ystep = axesStep else: ystep = self.proj.gridy[2] n = int(round((ymax - ymin) / ystep, 0)) + 1 gridycoord = list(np.linspace(ymin, ymax, n)) # gridycoord = list(np.arange(ymin, ymax, ystep)) + [ymax] else: gridycoord = [] xpos, ypos = self.coordToPoint(gridxcoord, gridycoord) if mode in ["x", "all"]: self.gridxpos = xpos if mode in ["y", "all"]: self.gridypos = ypos return def updateGrid(self): for line in self.gridObjs: self.scene.removeItem(line) if self.gridxpos and self.gridypos: clr = QColor(self.proj.gridColor) clr.setAlphaF(self.proj.gridOpacity) for x in self.gridxpos: line = QGraphicsLineItem(x, self.gridypos[0], x, self.gridypos[-1]) line.setZValue(5) line.setPen( QPen(clr, self.proj.gridLineWidth, self.proj.gridLineType)) self.gridObjs.append(line) self.scene.addItem(line) for y in self.gridypos: line = QGraphicsLineItem(self.gridxpos[0], y, self.gridxpos[-1], y) line.setZValue(5) line.setPen( QPen(clr, self.proj.gridLineWidth, self.proj.gridLineType)) self.gridObjs.append(line) self.scene.addItem(line) def updateCurvePoints(self, name): extra = len(self.pointObjs[name]) - self.pointsModel.rowCount() if extra > 0: for i in range(extra): item1 = QStandardItem() item2 = QStandardItem() item3 = QStandardItem() item2.setEditable(False) item3.setEditable(False) self.pointsModel.appendRow([item1, item2, item3]) elif extra < 0: j = self.pointsModel.rowCount() i = j + extra self.pointsModel.removeRows(i, -extra) for i in range(self.pointsModel.rowCount()): pt = self.pointObjs[name][i] self.pointsModel.item(i, 0).setText(str(i + 1)) xlist, ylist = self.pointToCoord([pt.x()], [pt.y()]) self.pointsModel.item(i, 1).setText(str(round(xlist[0], 6))) self.pointsModel.item(i, 2).setText(str(round(ylist[0], 6))) def calcCurveCoords(self): """calculate datas for export""" data = {} for curve in self.pointObjs: data[curve] = ([], []) for item in self.pointObjs[curve]: data[curve][0].append(item.x()) data[curve][1].append(item.y()) data[curve] = self.pointToCoord(data[curve][0], data[curve][1]) return data def axisvalid(self): """if there are axes with same coord value, or if there are axes at the save position, return False""" a = len(self.axesxObjs) b = len(set(self.axesxObjs.values())) if a != b: return False a = len(self.axesyObjs) b = len(set(self.axesyObjs.values())) if a != b: return False xs = [] for item in self.axesxObjs: xs.append(item.pos().x()) ys = [] for item in self.axesyObjs: ys.append(item.pos().y()) a = len(xs) b = len(set(xs)) if a != b: return False a = len(ys) b = len(set(ys)) if a != b: return False return True def exportToCSVtext(self): """return text in csv format, like following: curve1 x,1,2,3 y,2,3,4 """ text = "" data = self.calcCurveCoords() for curve in data: text += curve text += "\nx," for x in data[curve][0]: text += str(x) + ',' text += "\ny," for y in data[curve][1]: text += str(y) + ',' text += "\n" return text def changeCurrentCurve(self, name=None): self._lastCurve = self.currentCurve if name is None: name = self.currentCurve if name is None: return for i in range(self.curveModel.rowCount()): if self.curveModel.item(i, 1).text() == name: self.curveModel.item(i, 0).switch(True) else: self.curveModel.item(i, 0).switch(False) self.currentCurve = name self.updateCurve(self._lastCurve) self.updateCurve(self.currentCurve, Qt.red) self.updateCurvePoints(name) def changeCurveVisible(self, item): if item.index().column() == 2: visible = item.checkState() curvename = self.curveModel.item(item.index().row(), 1).text() self.showCurve(curvename, visible) def changePointOrder(self, item): row = item.row() if item.column() != 0: return newindex = item.text() if not newindex.isdigit(): return newindex = int(newindex) if newindex == row + 1: return if newindex > self.pointsModel.rowCount(): newindex = self.pointsModel.rowCount() newindex -= 1 self.pointObjs[self.currentCurve].insert( newindex, self.pointObjs[self.currentCurve].pop(row)) self.updateCurve(self.currentCurve) self.updateCurvePoints(self.currentCurve) # def curvetabChanged(self, item): # i = item.row() # j = item.column() # if j == 0: # if item.checkState() is Qt.Checked: # self.curveModel.item(i,0).setCheckState(Qt.Checked) # return # else: # for k in range(self.curveModel.rowCount()): # if k == i: # #self.curveModel.item(k,0).setCheckState(Qt.Checked) # newcurrent = self.curveModel.item(k,1).text() # print(self.curveModel.item(k,0).checkState()) # else: # self.curveModel.item(k,0).setCheckState(Qt.Unchecked) # if newcurrent and newcurrent != self.currentCurve: # self.changeCurrentCurve(newcurrent) def pointToCoord(self, xlist, ylist): if len(self.axesxObjs) >= 2: gridxs = [] for item in self.axesxObjs: gridxs.append(item.pos().x()) coordx = list(self.axesxObjs.values()) xCoords = interp(gridxs, coordx, xlist) else: xCoords = xlist if len(self.axesyObjs) >= 2: gridys = [] for item in self.axesyObjs: gridys.append(item.pos().y()) coordy = list(self.axesyObjs.values()) yCoords = interp(gridys, coordy, ylist) else: yCoords = ylist return (xCoords, yCoords) def coordToPoint(self, xlist, ylist): if len(self.axesxObjs) >= 2: gridxpos = [] for item in self.axesxObjs: gridxpos.append(item.pos().x()) coordx = list(self.axesxObjs.values()) xposs = interp(coordx, gridxpos, xlist) else: xposs = xlist if len(self.axesyObjs) >= 2: gridypos = [] for item in self.axesyObjs: gridypos.append(item.pos().y()) coordy = list(self.axesyObjs.values()) yposs = interp(coordy, gridypos, ylist) else: yposs = ylist return (xposs, yposs)
class ListModel(QAbstractTableModel): orderChanged = pyqtSignal() elementSelected = pyqtSignal(int) class ColumnID(object): ''' Define how many column the model holds and their type ''' ncols=2 Name=0 Delete=1 def __init__(self, elements=None, parent=None): ''' Common interface for the labelListModel, the boxListModel, and the cropListModel see concrete implementations for details :param elements: :param parent: ''' QAbstractTableModel.__init__(self, parent) if elements is None: elements = [] self._elements = list(elements) self._selectionModel = QItemSelectionModel(self) def onSelectionChanged(selected, deselected): if selected: ind = selected[0].indexes() if len(ind)>0: self.elementSelected.emit(ind[0].row()) self._selectionModel.selectionChanged.connect(onSelectionChanged) self._allowRemove = True self._toolTipSuffixes = {} self.unremovable_rows=[] #rows in this list cannot be removed from the gui, # to add to this list call self.makeRowPermanent(int) # to remove make the self.makeRowRemovable(int) def makeRowPermanent(self, rowIndex): """ The rowindex cannot be removed from gui to remove this index use self.makeRowRemovable """ self.unremovable_rows.append(rowIndex) def makeRowRemovable(self, rowIndex): """ :param rowIndex: is the index for the label of interest in the current gui setting """ self.unremovable_rows.remove(rowIndex) def __len__(self): return len(self._elements) def __getitem__(self, i): return self._elements[i] def selectedRow(self): selected = self._selectionModel.selectedRows() if len(selected) == 1: return selected[0].row() return -1 def selectedIndex(self): row = self.selectedRow() if row >= 0: return self.index(self.selectedRow()) else: return QModelIndex() def rowCount(self, parent=None): return len(self._elements) def columnCount(self, parent): return self.ColumnID.ncols def _getToolTipSuffix(self, row): """ Get the middle column tooltip suffix """ suffix = "; Click to select" if row in self._toolTipSuffixes: suffix = self._toolTipSuffixes[row] return suffix def _setToolTipSuffix(self, row, text): """ Set the middle column tooltip suffix """ self._toolTipSuffixes[row] = text index = self.createIndex(row, 1) self.dataChanged.emit(index, index) class EntryToolTipAdapter(object): """This class can be used to make each row look like a separate widget with its own tooltip. In this case, the "tooltip" is the suffix appended to the tooltip of the middle column. """ def __init__(self, table, row): self._row = row self._table = table def toolTip(self): return self._table._getToolTipSuffix(self._row) def setToolTip(self, text): self._table._setToolTipSuffix(self._row, text) def insertRow(self, position, object, parent=QModelIndex()): self.beginInsertRows(parent, position, position) object.changed.connect(self.modelReset) self._elements.insert(position, object) self.endInsertRows() return True def removeRow(self, position, parent=QModelIndex()): if position in self.unremovable_rows: return False self.beginRemoveRows(parent, position, position) value = self._elements[position] logger.debug("removing row: " + str(value)) self._elements.remove(value) self.endRemoveRows() return True def allowRemove(self, check): #Allow removing of rows. Needed to be able to disallow it #in interactive mode self._allowRemove = check self.dataChanged.emit(self.createIndex(0, self.ColumnID.Delete), self.createIndex(self.rowCount(), self.ColumnID.Delete)) def data(self, index, role): ''' Reimplement, see labelListModel or boxListModel for concrete example :param index: :param role: ''' if role == Qt.EditRole and index.column() == self.ColumnID.Name: name = self._elements[index.row()].name return name elif role == Qt.ToolTipRole and index.column() == self.ColumnID.Delete: s = "Delete {}".format(self._elements[index.row()].name) return s elif role == Qt.ToolTipRole and index.column() == self.ColumnID.Name: suffix = self._getToolTipSuffix(index.row()) s = "{}\nDouble click to rename {}".format( self._elements[index.row()].name, suffix) return s elif role == Qt.DisplayRole and index.column() == self.ColumnID.Name: name = self._elements[index.row()].name return name if role == Qt.DecorationRole and index.column() == self.ColumnID.Delete: if index.row() in self.unremovable_rows: return row = index.row() pixmap = QPixmap(_NPIXELS, _NPIXELS) pixmap.fill(Qt.transparent) painter = QPainter() painter.begin(pixmap) painter.setRenderHint(QPainter.Antialiasing) painter.setBrush(QColor("red")) painter.drawEllipse(1, 1, _NPIXELS - 2, _NPIXELS - 2) pen = QPen(QColor("black")) pen.setWidth(2) painter.setPen(pen) x = _XSTART y = _NPIXELS - x painter.drawLine(x, x, y, y) painter.drawLine(y, x, x, y) painter.end() icon = QIcon(pixmap) return icon def flags(self, index): ''' Reimplement, see labelListModel or boxListModel for concrete example :param index: ''' if index.column() == self.ColumnID.Delete: if self._allowRemove: return Qt.ItemIsEnabled | Qt.ItemIsSelectable else: return Qt.NoItemFlags elif index.column() == self.ColumnID.Name: return Qt.ItemIsEditable | Qt.ItemIsEnabled | Qt.ItemIsSelectable else: return Qt.NoItemFlags def setData(self, index, value, role=Qt.EditRole): ''' Reimplement, see labelListModel or boxListModel for concrete example :param index: ''' if role == Qt.EditRole and index.column() == self.ColumnID.Name: row = index.row() self._elements[row].name = value self.dataChanged.emit(index, index) return True return False def select(self, row): ''' Reimplement, see labelListModel or boxListModel for concrete example :param row ''' self._selectionModel.clear() self._selectionModel.select(self.index(row, self.ColumnID.Name), QItemSelectionModel.Select) def clearSelectionModel(self): self._selectionModel.clear()
class LayerStackModel(QAbstractListModel): canMoveSelectedUp = pyqtSignal("bool") canMoveSelectedDown = pyqtSignal("bool") canDeleteSelected = pyqtSignal("bool") orderChanged = pyqtSignal() layerAdded = pyqtSignal(Layer, int) # is now in row layerRemoved = pyqtSignal(Layer, int) # was in row stackCleared = pyqtSignal() def __init__(self, parent=None): QAbstractListModel.__init__(self, parent) self._layerStack = [] self.selectionModel = QItemSelectionModel(self) self.selectionModel.selectionChanged.connect(self.updateGUI) self.selectionModel.selectionChanged.connect(self._onSelectionChanged) self._movingRows = False QTimer.singleShot(0, self.updateGUI) def _handleRemovedLayer(layer): # Layerstacks *own* the layers they hold, and thus are # responsible for cleaning them up when they are removed: layer.clean_up() self.layerRemoved.connect(_handleRemovedLayer) #### ## High level API to manipulate the layerstack ### def __len__(self): return self.rowCount() def __repr__(self): return "<LayerStackModel: layerStack='%r'>" % (self._layerStack,) def __getitem__(self, i): return self._layerStack[i] def __iter__(self): return self._layerStack.__iter__() def layerIndex(self, layer): # note that the 'index' function already has a different implementation # from Qt side return self._layerStack.index(layer) def findMatchingIndex(self, func): """Call the given function with each layer and return the index of the first layer for which f is True.""" for index, layer in enumerate(self._layerStack): if func(layer): return index raise ValueError("No matching layer in stack.") def append(self, data): self.insert(0, data) def clear(self): if len(self) > 0: self.removeRows(0, len(self)) self.stackCleared.emit() def insert(self, index, data): """ Insert a layer into this layer stack, which *takes ownership* of the layer. """ assert isinstance(data, Layer), "Only Layers can be added to a LayerStackModel" self.insertRow(index) self.setData(self.index(index), data) if self.selectedRow() >= 0: self.selectionModel.select(self.index(self.selectedRow()), QItemSelectionModel.Deselect) self.selectionModel.select(self.index(index), QItemSelectionModel.Select) data.changed.connect(functools.partial(self._onLayerChanged, self.index(index))) index = self._layerStack.index(data) self.layerAdded.emit(data, index) self.updateGUI() def selectRow(self, row): already_selected_rows = self.selectionModel.selectedRows() if len(already_selected_rows) == 1 and row == already_selected_rows[0]: # Nothing to do if this row is already selected return self.selectionModel.clear() self.selectionModel.setCurrentIndex(self.index(row), QItemSelectionModel.SelectCurrent) def deleteSelected(self): num_rows = len(self.selectionModel.selectedRows()) assert num_rows == 1, "Can't delete selected row: {} layers are currently selected.".format(num_rows) row = self.selectionModel.selectedRows()[0] layer = self._layerStack[row.row()] assert ( not layer._cleaned_up ), "This layer ({}) has already been cleaned up. Shouldn't it already be removed from the layerstack?".format( layer.name ) self.removeRow(row.row()) if self.rowCount() > 0: self.selectionModel.select(self.index(0), QItemSelectionModel.Select) self.layerRemoved.emit(layer, row.row()) self.updateGUI() def moveSelectedUp(self): assert len(self.selectionModel.selectedRows()) == 1 row = self.selectionModel.selectedRows()[0] if row.row() != 0: oldRow = row.row() newRow = oldRow - 1 self._moveToRow(oldRow, newRow) def moveSelectedDown(self): assert len(self.selectionModel.selectedRows()) == 1 row = self.selectionModel.selectedRows()[0] if row.row() != self.rowCount() - 1: oldRow = row.row() newRow = oldRow + 1 self._moveToRow(oldRow, newRow) def moveSelectedToTop(self): assert len(self.selectionModel.selectedRows()) == 1 row = self.selectionModel.selectedRows()[0] if row.row() != 0: oldRow = row.row() newRow = 0 self._moveToRow(oldRow, newRow) def moveSelectedToBottom(self): assert len(self.selectionModel.selectedRows()) == 1 row = self.selectionModel.selectedRows()[0] if row.row() != self.rowCount() - 1: oldRow = row.row() newRow = self.rowCount() - 1 self._moveToRow(oldRow, newRow) def moveSelectedToRow(self, newRow): assert len(self.selectionModel.selectedRows()) == 1 row = self.selectionModel.selectedRows()[0] if row.row() != newRow: oldRow = row.row() self._moveToRow(oldRow, newRow) def _moveToRow(self, oldRow, newRow): d = self._layerStack[oldRow] self.removeRow(oldRow) self.insertRow(newRow) self.setData(self.index(newRow), d) self.selectionModel.select(self.index(newRow), QItemSelectionModel.Select) self.orderChanged.emit() self.updateGUI() #### ## Low level API. To add, remove etc. layers use the high level API from above. #### def updateGUI(self): self.canMoveSelectedUp.emit(self.selectedRow() > 0) self.canMoveSelectedDown.emit(self.selectedRow() < self.rowCount() - 1) self.canDeleteSelected.emit(self.rowCount() > 0) self.wantsUpdate() def selectedRow(self): selected = self.selectionModel.selectedRows() if len(selected) == 1: return selected[0].row() return -1 def selectedIndex(self): row = self.selectedRow() if row >= 0: return self.index(self.selectedRow()) else: return QModelIndex() def rowCount(self, parent=QModelIndex()): if not parent.isValid(): return len(self._layerStack) return 0 def insertRows(self, row, count, parent=QModelIndex()): """Insert empty rows in the stack. DO NOT USE THIS METHOD TO INSERT NEW LAYERS! Always use the insert() or append() method. """ if parent.isValid(): return False oldRowCount = self.rowCount() # for some reason, row can be negative! beginRow = max(0, row) endRow = min(beginRow + count - 1, len(self._layerStack)) self.beginInsertRows(parent, beginRow, endRow) while beginRow <= endRow: self._layerStack.insert(row, Layer(datasources=[])) beginRow += 1 self.endInsertRows() assert self.rowCount() == oldRowCount + 1, "oldRowCount = %d, self.rowCount() = %d" % ( oldRowCount, self.rowCount(), ) return True def removeRows(self, row, count, parent=QModelIndex()): """Remove rows from the stack. DO NOT USE THIS METHOD TO REMOVE LAYERS! Use the deleteSelected() method instead. """ if parent.isValid(): return False if row + count <= 0 or row >= len(self._layerStack): return False oldRowCount = self.rowCount() beginRow = max(0, row) endRow = min(row + count - 1, len(self._layerStack) - 1) self.beginRemoveRows(parent, beginRow, endRow) while beginRow <= endRow: del self._layerStack[row] beginRow += 1 self.endRemoveRows() return True def flags(self, index): defaultFlags = Qt.ItemIsSelectable | Qt.ItemIsEditable | Qt.ItemIsEnabled if index.isValid(): return Qt.ItemIsDragEnabled | defaultFlags else: return Qt.ItemIsDropEnabled | defaultFlags def supportedDropActions(self): return Qt.MoveAction def data(self, index, role=Qt.DisplayRole): if not index.isValid(): return None if index.row() >= len(self._layerStack): return None if role == Qt.DisplayRole or role == Qt.EditRole: return self._layerStack[index.row()] elif role == Qt.ToolTipRole: return self._layerStack[index.row()].toolTip() else: return None def setData(self, index, value, role=Qt.EditRole): """Replace one layer with another. DO NOT USE THIS METHOD TO INSERT NEW LAYERS! Use deleteSelected() followed by insert() or append(). """ if role == Qt.EditRole: layer = value if not isinstance(value, Layer): layer = value self._layerStack[index.row()] = layer self.dataChanged.emit(index, index) return True elif role == Qt.ToolTipRole: self._layerStack[index.row()].setToolTip() return True return False def headerData(self, section, orientation, role=Qt.DisplayRole): if role != Qt.DisplayRole: return None if orientation == Qt.Horizontal: return "Column %r" % section else: return "Row %r" % section def wantsUpdate(self): self.layoutChanged.emit() def _onLayerChanged(self, idx): self.dataChanged.emit(idx, idx) self.updateGUI() def _onSelectionChanged(self, selected, deselected): for idx in deselected.indexes(): self[idx.row()].setActive(False) for idx in selected.indexes(): self[idx.row()].setActive(True)