class TrackingWindow(QMainWindow): """ Main window of the application. This class is responsible for the global data structures too. :IVariables: undo_stack : `QUndoStack` Undo stack. All actions that can be undone should be pushed on the stack. _project : `project.Project` Project object managing the loaded data _data : `tracking_data.TrackingData` Data object keeping track of points and cells toolGroup : `QActionGroup` Group of actions to be enabled only when actions can be taken on images previousSelAct : `QActionGroup` Actions enabled when points are selected in the previous pane currentSelAct : `QActionGroup` Actions enabled when points are selected in the current pane projectAct : `QActionGroup` Actions to enable once a project is loaded _previousScene : `tracking_scene.TrackingScene` Object managing the previous pane _currentScene : `tracking_scene.LinkedTrackingScene` Object managing the current pane """ def __init__(self, *args, **kwords): QMainWindow.__init__(self, *args) self.undo_stack = QUndoStack(self) self.ui = Ui_TrackingWindow() self.ui.setupUi(self) self._project = None self._data = None self.toolGroup = QActionGroup(self) self.toolGroup.addAction(self.ui.actionAdd_point) self.toolGroup.addAction(self.ui.action_Move_point) self.toolGroup.addAction(self.ui.actionAdd_cell) self.toolGroup.addAction(self.ui.actionRemove_cell) self.toolGroup.addAction(self.ui.action_Pan) self.toolGroup.addAction(self.ui.actionZoom_out) self.toolGroup.addAction(self.ui.actionZoom_in) self.previousSelAct = QActionGroup(self) self.previousSelAct.addAction(self.ui.actionCopy_selection_from_Previous) self.previousSelAct.addAction(self.ui.actionDelete_Previous) self.previousSelAct.setEnabled(False) self.currentSelAct = QActionGroup(self) self.currentSelAct.addAction(self.ui.actionCopy_selection_from_Current) self.currentSelAct.addAction(self.ui.actionDelete_Current) self.currentSelAct.setEnabled(False) self.projectAct = QActionGroup(self) self.projectAct.addAction(self.ui.action_Next_image) self.projectAct.addAction(self.ui.action_Previous_image) self.projectAct.addAction(self.ui.actionAdd_point) self.projectAct.addAction(self.ui.action_Move_point) self.projectAct.addAction(self.ui.action_Pan) self.projectAct.addAction(self.ui.actionAdd_cell) self.projectAct.addAction(self.ui.actionRemove_cell) self.projectAct.addAction(self.ui.action_Change_data_file) self.projectAct.addAction(self.ui.actionNew_data_file) self.projectAct.addAction(self.ui.actionZoom_out) self.projectAct.addAction(self.ui.actionZoom_in) self.projectAct.addAction(self.ui.actionSave_as) self.projectAct.addAction(self.ui.action_Fit) self.projectAct.addAction(self.ui.actionZoom_100) self.projectAct.addAction(self.ui.actionMerge_points) self.projectAct.addAction(self.ui.actionCopy_from_previous) self.projectAct.addAction(self.ui.actionCopy_from_current) self.projectAct.addAction(self.ui.actionReset_alignment) self.projectAct.addAction(self.ui.actionAlign_images) self.projectAct.addAction(self.ui.actionSelectPreviousAll) self.projectAct.addAction(self.ui.actionSelectPreviousNew) self.projectAct.addAction(self.ui.actionSelectPreviousNone) self.projectAct.addAction(self.ui.actionSelectPreviousNon_associated) self.projectAct.addAction(self.ui.actionSelectPreviousAssociated) self.projectAct.addAction(self.ui.actionSelectPreviousInvert) self.projectAct.addAction(self.ui.actionSelectCurrentAll) self.projectAct.addAction(self.ui.actionSelectCurrentNew) self.projectAct.addAction(self.ui.actionSelectCurrentNone) self.projectAct.addAction(self.ui.actionSelectCurrentNon_associated) self.projectAct.addAction(self.ui.actionSelectCurrentAssociated) self.projectAct.addAction(self.ui.actionSelectCurrentInvert) self.projectAct.addAction(self.ui.actionEdit_timing) self.projectAct.addAction(self.ui.actionEdit_scales) self.projectAct.addAction(self.ui.actionCompute_growth) self.projectAct.addAction(self.ui.actionClean_cells) self.projectAct.addAction(self.ui.actionGotoCell) self.projectAct.setEnabled(False) current_sel_actions = [self.ui.actionSelectCurrentAll, self.ui.actionSelectCurrentNew, self.ui.actionSelectCurrentNone, self.ui.actionSelectCurrentInvert, '-', self.ui.actionSelectCurrentNon_associated, self.ui.actionSelectCurrentAssociated, self.ui.actionCopy_selection_from_Previous ] previous_sel_actions = [self.ui.actionSelectPreviousAll, self.ui.actionSelectPreviousNew, self.ui.actionSelectPreviousNone, self.ui.actionSelectPreviousInvert, '-', self.ui.actionSelectPreviousNon_associated, self.ui.actionSelectPreviousAssociated, self.ui.actionCopy_selection_from_Current ] self._previousScene = TrackingScene(self.undo_stack, self.ui.actionDelete_Previous, previous_sel_actions, self) self._currentScene = LinkedTrackingScene(self._previousScene, self.undo_stack, self.ui.actionDelete_Current, current_sel_actions, self) self._previousScene.hasSelectionChanged.connect(self.previousSelAct.setEnabled) self._currentScene.hasSelectionChanged.connect(self.currentSelAct.setEnabled) self._previousScene.realSceneSizeChanged.connect(self.sceneSizeChanged) self._currentScene.realSceneSizeChanged.connect(self.sceneSizeChanged) self._previousScene.zoomIn[QPointF].connect(self.zoomIn) self._currentScene.zoomIn.connect(self.zoomIn) self._previousScene.zoomOut[QPointF].connect(self.zoomOut) self._currentScene.zoomOut.connect(self.zoomOut) self.ui.previousData.setScene(self._previousScene) self.ui.currentData.setScene(self._currentScene) self.ui.previousData.setDragMode(QGraphicsView.ScrollHandDrag) self.ui.currentData.setDragMode(QGraphicsView.ScrollHandDrag) #self.ui.previousData.setCacheMode(QGraphicsView.CacheBackground) #self.ui.currentData.setCacheMode(QGraphicsView.CacheBackground) # Redefine shortcuts to standard key sequences self.ui.action_Save.setShortcut(QKeySequence.Save) self.ui.actionSave_as.setShortcut(QKeySequence.SaveAs) self.ui.action_Open_project.setShortcut(QKeySequence.Open) self.ui.action_Undo.setShortcut(QKeySequence.Undo) self.ui.action_Redo.setShortcut(QKeySequence.Redo) self.ui.action_Next_image.setShortcut(QKeySequence.Forward) self.ui.action_Previous_image.setShortcut(QKeySequence.Back) # Connecting undo stack signals self.ui.action_Undo.triggered.connect(self.undo) self.ui.action_Redo.triggered.connect(self.redo) self.undo_stack.canRedoChanged[bool].connect(self.ui.action_Redo.setEnabled) self.undo_stack.canUndoChanged[bool].connect(self.ui.action_Undo.setEnabled) self.undo_stack.redoTextChanged["const QString&"].connect(self.changeRedoText) self.undo_stack.undoTextChanged["const QString&"].connect(self.changeUndoText) self.undo_stack.cleanChanged[bool].connect(self.ui.action_Save.setDisabled) # link_icon = QIcon() # pix = QPixmap(":/icons/link.png") # link_icon.addPixmap(pix, QIcon.Normal, QIcon.On) # pix = QPixmap(":/icons/link_broken.png") # link_icon.addPixmap(pix, QIcon.Normal, QIcon.Off) # self.link_icon = link_icon # #self.ui.linkViews.setIconSize(QSize(64,32)) # self.ui.linkViews.setIcon(link_icon) self._recent_projects_menu = QMenu(self) self.ui.actionRecent_projects.setMenu(self._recent_projects_menu) self._recent_projects_act = [] self._projects_mapper = QSignalMapper(self) self._projects_mapper.mapped[int].connect(self.loadRecentProject) self.param_dlg = None # Setting up the status bar bar = self.statusBar() # Adding current directory cur_dir = QLabel("") bar.addPermanentWidget(cur_dir) self._current_dir_label = cur_dir # Adding up zoom zoom = QLabel("") bar.addPermanentWidget(zoom) self._zoom_label = zoom self.changeZoom(1) self.loadConfig() parameters.instance.renderingChanged.connect(self.changeRendering) self.changeRendering() def changeRendering(self): if parameters.instance.use_OpenGL: self.ui.previousData.setViewport(QGLWidget(QGLFormat(QGL.SampleBuffers))) self.ui.currentData.setViewport(QGLWidget(QGLFormat(QGL.SampleBuffers))) else: self.ui.previousData.setViewport(QWidget()) self.ui.currentData.setViewport(QWidget()) def undo(self): self.undo_stack.undo() def redo(self): self.undo_stack.redo() def changeRedoText(self, text): self.ui.action_Redo.setText(text) self.ui.action_Redo.setToolTip(text) self.ui.action_Redo.setStatusTip(text) def changeUndoText(self, text): self.ui.action_Undo.setText(text) self.ui.action_Undo.setToolTip(text) self.ui.action_Undo.setStatusTip(text) def closeEvent(self, event): self.saveConfig() if not self.ensure_save_data("Exiting whith unsaved data", "The last modifications you made were not saved." " Are you sure you want to exit?"): event.ignore() return QMainWindow.closeEvent(self, event) #sys.exit(0) def loadConfig(self): params = parameters.instance self.ui.action_Show_vector.setChecked(params.show_vectors) self.ui.linkViews.setChecked(params.link_views) self.ui.action_Show_template.setChecked(parameters.instance.show_template) self.ui.actionShow_id.setChecked(parameters.instance.show_id) self.ui.action_Estimate_position.setChecked(parameters.instance.estimate) self.updateRecentFiles() params.recentProjectsChange.connect(self.updateRecentFiles) def updateRecentFiles(self): for a in self._recent_projects_act: self._projects_mapper.removeMappings(a) del self._recent_projects_act[:] menu = self._recent_projects_menu menu.clear() recent_projects = parameters.instance.recent_projects for i, p in enumerate(recent_projects): act = QAction(self) act.setText("&{0:d} {1}".format(i + 1, p)) self._recent_projects_act.append(act) act.triggered.connect(self._projects_mapper.map) self._projects_mapper.setMapping(act, i) menu.addAction(act) def saveConfig(self): parameters.instance.save() def check_for_data(self): if self._project is None: QMessageBox.critical(self, "No project loaded", "You have to load a project before performing this operation") return False return True def loadRecentProject(self, i): if self.ensure_save_data("Leaving unsaved data", "The last modifications you made were not saved." " Are you sure you want to change project?"): self.loadProject(parameters.instance.recent_projects[i]) @pyqtSignature("") def on_action_Open_project_triggered(self): if self.ensure_save_data("Leaving unsaved data", "The last modifications you made were not saved." " Are you sure you want to change project?"): dir_ = QFileDialog.getExistingDirectory(self, "Select a project directory", parameters.instance._last_dir) if dir_: self.loadProject(dir_) def loadProject(self, dir_): dir_ = path(dir_) project = Project(dir_) if project.valid: self._project = project else: create = QMessageBox.question(self, "Invalid project directory", "This directory does not contain a valid project. Turn into a directory?", QMessageBox.No, QMessageBox.Yes) if create == QMessageBox.No: return project.create() self._project = project self._project.use() parameters.instance.add_recent_project(dir_) parameters.instance._last_dir = dir_ if self._data is not None: _data = self._data _data.saved.disconnect(self.undo_stack.setClean) try: #self._project.load() self.load_data() _data = self._project.data _data.saved.connect(self.undo_stack.setClean) self._project.changedDataFile.connect(self.dataFileChanged) self._data = _data self._previousScene.changeDataManager(self._data) self._currentScene.changeDataManager(self._data) self.initFromData() self.projectAct.setEnabled(True) except TrackingDataException as ex: showException(self, "Error while loaded data", ex) def dataFileChanged(self, new_file): if new_file is None: self._current_dir_label.setText("") else: self._current_dir_label.setText(new_file) def initFromData(self): """ Initialize the interface using the current data """ self.ui.previousState.clear() self.ui.currentState.clear() for name in self._data.images_name: self.ui.previousState.addItem(name) self.ui.currentState.addItem(name) self.ui.previousState.setCurrentIndex(0) self.ui.currentState.setCurrentIndex(1) self._previousScene.changeImage(self._data.image_path(self._data.images_name[0])) self._currentScene.changeImage(self._data.image_path(self._data.images_name[1])) self.dataFileChanged(self._project.data_file) @pyqtSignature("int") def on_previousState_currentIndexChanged(self, index): #print "Previous image loaded: %s" % self._data.images[index] self.changeScene(self._previousScene, index) self._currentScene.changeImage(None) @pyqtSignature("int") def on_currentState_currentIndexChanged(self, index): #print "Current image loaded: %s" % self._data.images[index] self.changeScene(self._currentScene, index) def changeScene(self, scene, index): """ Set the scene to use the image number index. """ scene.changeImage(self._data.image_path(self._data.images_name[index])) @pyqtSignature("") def on_action_Save_triggered(self): self.save_data() @pyqtSignature("") def on_actionSave_as_triggered(self): fn = QFileDialog.getSaveFileName(self, "Select a data file to save in", self._project.data_dir, "CSV Files (*.csv);;All files (*.*)") if fn: self.save_data(path(fn)) def save_data(self, data_file=None): if self._data is None: raise TrackingDataException("Trying to save data when none have been loaded") try: self._project.save(data_file) return True except TrackingDataException as ex: showException(self, "Error while saving data", ex) return False def load_data(self, **opts): if self._project is None: raise TrackingDataException("Trying to load data when no project have been loaded") try: if self._project.load(**opts): log_debug("Data file was corrected. Need saving.") self.ui.action_Save.setEnabled(True) else: log_debug("Data file is clean.") self.ui.action_Save.setEnabled(False) return True except TrackingDataException as ex: showException(self, "Error while loading data", ex) return False except RetryTrackingDataException as ex: if retryException(self, "Problem while loading data", ex): new_opts = dict(opts) new_opts.update(ex.method_args) return self.load_data(**new_opts) return False def ensure_save_data(self, title, reason): if self._data is not None and not self.undo_stack.isClean(): button = QMessageBox.warning(self, title, reason, QMessageBox.Yes | QMessageBox.Save | QMessageBox.Cancel) if button == QMessageBox.Save: return self.save_data() elif button == QMessageBox.Cancel: return False self.undo_stack.clear() return True @pyqtSignature("") def on_action_Change_data_file_triggered(self): if self.ensure_save_data("Leaving unsaved data", "The last modifications you made were not saved." " Are you sure you want to change the current data file?"): fn = QFileDialog.getOpenFileName(self, "Select a data file to load", self._project.data_dir, "CSV Files (*.csv);;All files (*.*)") if fn: self._project.data_file = str(fn) if self.load_data(): self._previousScene.resetNewPoints() self._currentScene.resetNewPoints() @pyqtSignature("bool") def on_action_Show_vector_toggled(self, value): parameters.instance.show_vector = value self._currentScene.showVector(value) @pyqtSignature("bool") def on_action_Show_template_toggled(self, value): parameters.instance.show_template = value @pyqtSignature("bool") def on_actionShow_id_toggled(self, value): parameters.instance.show_id = value @pyqtSignature("") def on_action_Next_image_triggered(self): cur = self.ui.currentState.currentIndex() pre = self.ui.previousState.currentIndex() l = len(self._data.images_name) if cur < l-1 and pre < l-1: self.ui.previousState.setCurrentIndex(pre+1) self.ui.currentState.setCurrentIndex(cur+1) @pyqtSignature("") def on_action_Previous_image_triggered(self): cur = self.ui.currentState.currentIndex() pre = self.ui.previousState.currentIndex() if cur > 0 and pre > 0: self.ui.previousState.setCurrentIndex(pre-1) self.ui.currentState.setCurrentIndex(cur-1) @pyqtSignature("") def on_copyToPrevious_clicked(self): self._currentScene.copyFromLinked(self._previousScene) @pyqtSignature("") def on_copyToCurrent_clicked(self): self._previousScene.copyToLinked(self._currentScene) @pyqtSignature("bool") def on_action_Estimate_position_toggled(self, value): parameters.instance.estimate = value # @pyqtSignature("") # def on_action_Undo_triggered(self): # print "Undo" # @pyqtSignature("") # def on_action_Redo_triggered(self): # print "Redo" @pyqtSignature("bool") def on_action_Parameters_toggled(self, value): if value: from .parametersdlg import ParametersDlg self._previousScene.showTemplates() self._currentScene.showTemplates() #tracking_scene.saveParameters() parameters.instance.save() max_size = max(self._currentScene.width(), self._currentScene.height(), self._previousScene.width(), self._previousScene.height(), 400) self.param_dlg = ParametersDlg(max_size, self) self.param_dlg.setModal(False) self.ui.action_Pan.setChecked(True) self.ui.actionAdd_point.setEnabled(False) self.ui.action_Move_point.setEnabled(False) self.ui.actionAdd_cell.setEnabled(False) self.ui.actionRemove_cell.setEnabled(False) self.ui.action_Undo.setEnabled(False) self.ui.action_Redo.setEnabled(False) self.ui.action_Open_project.setEnabled(False) self.ui.actionRecent_projects.setEnabled(False) self.ui.action_Change_data_file.setEnabled(False) self.ui.copyToCurrent.setEnabled(False) self.ui.copyToPrevious.setEnabled(False) self.param_dlg.finished[int].connect(self.closeParam) self.param_dlg.show() elif self.param_dlg: self.param_dlg.accept() def closeParam(self, value): if value == QDialog.Rejected: parameters.instance.load() self.ui.actionAdd_point.setEnabled(True) self.ui.action_Move_point.setEnabled(True) self.ui.actionAdd_cell.setEnabled(True) self.ui.actionRemove_cell.setEnabled(True) self.ui.action_Undo.setEnabled(True) self.ui.action_Redo.setEnabled(True) self.ui.action_Open_project.setEnabled(True) self.ui.actionRecent_projects.setEnabled(True) self.ui.action_Change_data_file.setEnabled(True) self.ui.copyToCurrent.setEnabled(True) self.ui.copyToPrevious.setEnabled(True) self._previousScene.showTemplates(False) self._currentScene.showTemplates(False) self._previousScene.update() self._currentScene.update() self.param_dlg = None self.ui.action_Parameters.setChecked(False) @pyqtSignature("bool") def on_actionZoom_in_toggled(self, value): if value: self._previousScene.mode = TrackingScene.ZoomIn self._currentScene.mode = TrackingScene.ZoomIn @pyqtSignature("bool") def on_actionZoom_out_toggled(self, value): if value: self._previousScene.mode = TrackingScene.ZoomOut self._currentScene.mode = TrackingScene.ZoomOut #def resizeEvent(self, event): # self.ensureZoomFit() def ensureZoomFit(self): if self._data: prev_rect = self._previousScene.sceneRect() cur_rect = self._currentScene.sceneRect() prev_wnd = QRectF(self.ui.previousData.childrenRect()) cur_wnd = QRectF(self.ui.currentData.childrenRect()) prev_matrix = self.ui.previousData.matrix() cur_matrix = self.ui.currentData.matrix() prev_mapped_rect = prev_matrix.mapRect(prev_rect) cur_mapped_rect = cur_matrix.mapRect(cur_rect) if (prev_mapped_rect.width() < prev_wnd.width() or prev_mapped_rect.height() < prev_wnd.height() or cur_mapped_rect.width() < cur_wnd.width() or cur_mapped_rect.height() < cur_wnd.height()): self.on_action_Fit_triggered() @pyqtSignature("") def on_action_Fit_triggered(self): prev_rect = self._previousScene.sceneRect() cur_rect = self._currentScene.sceneRect() prev_wnd = self.ui.previousData.childrenRect() cur_wnd = self.ui.currentData.childrenRect() prev_sw = prev_wnd.width() / prev_rect.width() prev_sh = prev_wnd.height() / prev_rect.height() cur_sw = cur_wnd.width() / cur_rect.width() cur_sh = cur_wnd.height() / cur_rect.height() s = max(prev_sw, prev_sh, cur_sw, cur_sh) self.ui.previousData.resetMatrix() self.ui.previousData.scale(s, s) self.ui.currentData.resetMatrix() self.ui.currentData.scale(s, s) self.changeZoom(s) def zoomOut(self, point=None): self.ui.currentData.scale(0.5, 0.5) self.ui.previousData.scale(0.5, 0.5) self.changeZoom(self.ui.previousData.matrix().m11()) if point is not None: self.ui.previousData.centerOn(point) self.ui.currentData.centerOn(point) #self.ensureZoomFit() def zoomIn(self, point=None): self.ui.currentData.scale(2, 2) self.ui.previousData.scale(2, 2) self.changeZoom(self.ui.previousData.matrix().m11()) if point is not None: self.ui.previousData.centerOn(point) self.ui.currentData.centerOn(point) def changeZoom(self, zoom): self._zoom_label.setText("Zoom: %.5g%%" % (100*zoom)) @pyqtSignature("") def on_actionZoom_100_triggered(self): self.ui.previousData.resetMatrix() self.ui.currentData.resetMatrix() self.changeZoom(1) @pyqtSignature("bool") def on_actionAdd_point_toggled(self, value): if value: self._previousScene.mode = TrackingScene.Add self._currentScene.mode = TrackingScene.Add @pyqtSignature("bool") def on_actionAdd_cell_toggled(self, value): if value: self._previousScene.mode = TrackingScene.AddCell self._currentScene.mode = TrackingScene.AddCell @pyqtSignature("bool") def on_actionRemove_cell_toggled(self, value): if value: self._previousScene.mode = TrackingScene.RemoveCell self._currentScene.mode = TrackingScene.RemoveCell @pyqtSignature("bool") def on_action_Move_point_toggled(self, value): if value: self._previousScene.mode = TrackingScene.Move self._currentScene.mode = TrackingScene.Move @pyqtSignature("bool") def on_action_Pan_toggled(self, value): if value: self._previousScene.mode = TrackingScene.Pan self._currentScene.mode = TrackingScene.Pan @pyqtSignature("bool") def on_linkViews_toggled(self, value): parameters.instance.link_views = value phor = self.ui.previousData.horizontalScrollBar() pver = self.ui.previousData.verticalScrollBar() chor = self.ui.currentData.horizontalScrollBar() cver = self.ui.currentData.verticalScrollBar() if value: phor.valueChanged[int].connect(chor.setValue) pver.valueChanged[int].connect(cver.setValue) chor.valueChanged[int].connect(phor.setValue) cver.valueChanged[int].connect(pver.setValue) self._previousScene.templatePosChange.connect(self._currentScene.setTemplatePos) self._currentScene.templatePosChange.connect(self._previousScene.setTemplatePos) phor.setValue(chor.value()) pver.setValue(cver.value()) else: phor.valueChanged[int].disconnect(chor.setValue) pver.valueChanged[int].disconnect(cver.setValue) chor.valueChanged[int].disconnect(phor.setValue) cver.valueChanged[int].disconnect(pver.setValue) self._previousScene.templatePosChange.disconnect(self._currentScene.setTemplatePos) self._currentScene.templatePosChange.disconnect(self._previousScene.setTemplatePos) def copyFrom(self, start, items): if parameters.instance.estimate: dlg = createForm('copy_progress.ui', None) dlg.buttonBox.clicked["QAbstractButton*"].connect(self.cancelCopy) params = parameters.instance ts = params.template_size ss = params.search_size fs = params.filter_size self.copy_thread = algo.FindInAll(self._data, start, items, ts, ss, fs, self) dlg.imageProgress.setMaximum(self.copy_thread.num_images) self.copy_thread.start() self.copy_dlg = dlg dlg.exec_() else: algo.copyFromImage(self._data, start, items, self.undo_stack) def cancelCopy(self, *args): self.copy_thread.stop = True dlg = self.copy_dlg dlg.buttonBox.clicked['QAbstractButton*)'].disconnect(self.cancelCopy) self._previousScene.changeImage(None) self._currentScene.changeImage(None) def event(self, event): if isinstance(event, algo.NextImage): dlg = self.copy_dlg if dlg is not None: dlg.imageProgress.setValue(event.currentImage) dlg.pointProgress.setMaximum(event.nbPoints) dlg.pointProgress.setValue(0) return True elif isinstance(event, algo.NextPoint): dlg = self.copy_dlg if dlg is not None: dlg.pointProgress.setValue(event.currentPoint) return True elif isinstance(event, algo.FoundAll): dlg = self.copy_dlg if dlg is not None: self.cancelCopy() dlg.accept() return True elif isinstance(event, algo.Aborted): dlg = self.copy_dlg if dlg is not None: self.cancelCopy() dlg.accept() return True return QMainWindow.event(self, event) def itemsToCopy(self, scene): items = scene.getSelectedIds() if items: answer = QMessageBox.question(self, "Copy of points", "Some points were selected in the previous data window." " Do you want to copy only these point on the successive images?", QMessageBox.Yes, QMessageBox.No) if answer == QMessageBox.Yes: return items return scene.getAllIds() @pyqtSignature("") def on_actionCopy_from_previous_triggered(self): items = self.itemsToCopy(self._previousScene) if items: self.copyFrom(self.ui.previousState.currentIndex(), items) @pyqtSignature("") def on_actionCopy_from_current_triggered(self): items = self.itemsToCopy(self._currentScene) if items: self.copyFrom(self.ui.currentState.currentIndex(), items) @pyqtSignature("") def on_actionSelectPreviousAll_triggered(self): self._previousScene.selectAll() @pyqtSignature("") def on_actionSelectPreviousNew_triggered(self): self._previousScene.selectNew() @pyqtSignature("") def on_actionSelectPreviousNone_triggered(self): self._previousScene.selectNone() @pyqtSignature("") def on_actionSelectPreviousNon_associated_triggered(self): self._previousScene.selectNonAssociated() @pyqtSignature("") def on_actionSelectPreviousAssociated_triggered(self): self._previousScene.selectAssociated() @pyqtSignature("") def on_actionSelectPreviousInvert_triggered(self): self._previousScene.selectInvert() @pyqtSignature("") def on_actionSelectCurrentAll_triggered(self): self._currentScene.selectAll() @pyqtSignature("") def on_actionSelectCurrentNew_triggered(self): self._currentScene.selectNew() @pyqtSignature("") def on_actionSelectCurrentNone_triggered(self): self._currentScene.selectNone() @pyqtSignature("") def on_actionSelectCurrentNon_associated_triggered(self): self._currentScene.selectNonAssociated() @pyqtSignature("") def on_actionSelectCurrentAssociated_triggered(self): self._currentScene.selectAssociated() @pyqtSignature("") def on_actionSelectCurrentInvert_triggered(self): self._currentScene.selectInvert() def whichDelete(self): """ Returns a function deleting what the user wants """ dlg = createForm("deletedlg.ui", None) ret = dlg.exec_() if ret: if dlg.inAllImages.isChecked(): return TrackingScene.deleteInAllImages if dlg.toImage.isChecked(): return TrackingScene.deleteToImage if dlg.fromImage.isChecked(): return TrackingScene.deleteFromImage return lambda x: None @pyqtSignature("") def on_actionDelete_Previous_triggered(self): del_fct = self.whichDelete() del_fct(self._previousScene) @pyqtSignature("") def on_actionDelete_Current_triggered(self): del_fct = self.whichDelete() del_fct(self._currentScene) @pyqtSignature("") def on_actionMerge_points_triggered(self): if self._previousScene.mode == TrackingScene.AddCell: old_cell = self._previousScene.selected_cell new_cell = self._currentScene.selected_cell if old_cell is None or new_cell is None: QMessageBox.critical(self, "Cannot merge cells", "You have to select exactly one cell in the old state " "and one in the new state to merge them.") return try: if old_cell != new_cell: self.undo_stack.push(MergeCells(self._data, self._previousScene.image_name, old_cell, new_cell)) else: self.undo_stack.push(SplitCells(self._data, self._previousScene.image_name, old_cell, new_cell)) except AssertionError as error: QMessageBox.critical(self, "Cannot merge the cells", str(error)) else: old_pts = self._previousScene.getSelectedIds() new_pts = self._currentScene.getSelectedIds() if len(old_pts) != 1 or len(new_pts) != 1: QMessageBox.critical(self, "Cannot merge points", "You have to select exactly one point in the old state " "and one in the new state to link them.") return try: if old_pts != new_pts: self.undo_stack.push(ChangePointsId(self._data, self._previousScene.image_name, old_pts, new_pts)) else: log_debug("Splitting point of id %d" % old_pts[0]) self.undo_stack.push(SplitPointsId(self._data, self._previousScene.image_name, old_pts)) except AssertionError as error: QMessageBox.critical(self, "Cannot merge the points", str(error)) @pyqtSignature("") def on_actionCopy_selection_from_Current_triggered(self): cur_sel = self._currentScene.getSelectedIds() self._previousScene.setSelectedIds(cur_sel) @pyqtSignature("") def on_actionCopy_selection_from_Previous_triggered(self): cur_sel = self._previousScene.getSelectedIds() self._currentScene.setSelectedIds(cur_sel) @pyqtSignature("") def on_actionNew_data_file_triggered(self): if self.ensure_save_data("Leaving unsaved data", "The last modifications you made were not saved." " Are you sure you want to change the current data file?"): fn = QFileDialog.getSaveFileName(self, "Select a new data file to create", self._project.data_dir, "CSV Files (*.csv);;All files (*.*)") if fn: fn = path(fn) if fn.exists(): button = QMessageBox.question(self, "Erasing existing file", "Are you sure yo want to empty the file '%s' ?" % fn, QMessageBox.Yes, QMessageBox.No) if button == QMessageBox.No: return fn.remove() self._data.clear() self._previousScene.resetNewPoints() self._currentScene.resetNewPoints() self._project.data_file = fn self.initFromData() log_debug("Data file = %s" % (self._project.data_file,)) @pyqtSignature("") def on_actionAbout_triggered(self): dlg = QMessageBox(self) dlg.setWindowTitle("About Point Tracker") dlg.setIconPixmap(self.windowIcon().pixmap(64, 64)) #dlg.setTextFormat(Qt.RichText) dlg.setText("""Point Tracker Tool version %s rev %s Developper: Pierre Barbier de Reuille <*****@*****.**> Copyright 2008 """ % (__version__, __revision__)) img_read = ", ".join(str(s) for s in QImageReader.supportedImageFormats()) img_write = ", ".join(str(s) for s in QImageWriter.supportedImageFormats()) dlg.setDetailedText("""Supported image formats: - For reading: %s - For writing: %s """ % (img_read, img_write)) dlg.exec_() @pyqtSignature("") def on_actionAbout_Qt_triggered(self): QMessageBox.aboutQt(self, "About Qt") @pyqtSignature("") def on_actionReset_alignment_triggered(self): self.undo_stack.push(ResetAlignment(self._data)) @pyqtSignature("") def on_actionAlign_images_triggered(self): fn = QFileDialog.getOpenFileName(self, "Select a data file for alignment", self._project.data_dir, "CSV Files (*.csv);;All files (*.*)") if fn: d = self._data.copy() fn = path(fn) try: d.load(fn) except TrackingDataException as ex: showException(self, "Error while loading data file", ex) return if d._last_pt_id > 0: dlg = AlignmentDlg(d._last_pt_id+1, self) if dlg.exec_(): ref = dlg.ui.referencePoint.currentText() try: ref = int(ref) except ValueError: ref = str(ref) if dlg.ui.twoPointsRotation.isChecked(): r1 = int(dlg.ui.rotationPt1.currentText()) r2 = int(dlg.ui.rotationPt2.currentText()) rotation = ("TwoPoint", r1, r2) else: rotation = None else: return else: ref = 0 rotation = None try: shifts, angles = algo.alignImages(self._data, d, ref, rotation) self.undo_stack.push(AlignImages(self._data, shifts, angles)) except algo.AlgoException as ex: showException(self, "Error while aligning images", ex) def sceneSizeChanged(self): previous_rect = self._previousScene.real_scene_rect current_rect = self._currentScene.real_scene_rect rect = previous_rect | current_rect self._previousScene.setSceneRect(rect) self._currentScene.setSceneRect(rect) @pyqtSignature("") def on_actionEdit_timing_triggered(self): data = self._data dlg = TimeEditDlg(data.images_name, data.images_time, [data.image_path(n) for n in data.images_name], self) self.current_dlg = dlg if dlg.exec_() == QDialog.Accepted: self.undo_stack.push(ChangeTiming(data, [t for n, t in dlg.model])) del self.current_dlg @pyqtSignature("") def on_actionEdit_scales_triggered(self): data = self._data dlg = EditResDlg(data.images_name, data.images_scale, [data.image_path(n) for n in data.images_name], self) self.current_dlg = dlg if dlg.exec_() == QDialog.Accepted: self.undo_stack.push(ChangeScales(data, [sc for n, sc in dlg.model])) del self.current_dlg @pyqtSignature("") def on_actionCompute_growth_triggered(self): data = self._data dlg = GrowthComputationDlg(data, self) self.current_dlg = dlg dlg.exec_() del self.current_dlg @pyqtSignature("") def on_actionPlot_growth_triggered(self): data = self._data dlg = PlottingDlg(data, self) self.current_dlg = dlg dlg.exec_() del self.current_dlg @pyqtSignature("") def on_actionClean_cells_triggered(self): self.undo_stack.push(CleanCells(self._data)) @pyqtSignature("") def on_actionGotoCell_triggered(self): cells = [str(cid) for cid in self._data.cells] selected, ok = QInputDialog.getItem(self, "Goto cell", "Select the cell to go to", cells, 0) if ok: cid = int(selected) self.ui.actionAdd_cell.setChecked(True) data = self._data if cid not in data.cells: return ls = data.cells_lifespan[cid] prev_pos = self._previousScene.current_data._current_index cur_pos = self._currentScene.current_data._current_index full_poly = data.cells[cid] poly = [pid for pid in full_poly if pid in data[prev_pos]] #log_debug("Cell %d on time %d: %s" % (cid, prev_pos, poly)) if prev_pos < ls.start or prev_pos >= ls.end or not poly: for i in range(*ls.slice().indices(len(data))): poly = [pid for pid in full_poly if pid in data[i]] if poly: log_debug("Found cell %d on image %d with polygon %s" % (cid, i, poly)) new_prev_pos = i break else: log_debug("Cell %d found nowhere in range %s!!!" % (cid, ls.slice())) else: new_prev_pos = prev_pos new_cur_pos = min(max(cur_pos + new_prev_pos - prev_pos, 0), len(data)) self.ui.previousState.setCurrentIndex(new_prev_pos) self.ui.currentState.setCurrentIndex(new_cur_pos) self._previousScene.current_cell = cid self._currentScene.current_cell = cid prev_data = self._previousScene.current_data poly = data.cells[cid] prev_poly = QPolygonF([prev_data[ptid] for ptid in poly if ptid in prev_data]) prev_bbox = prev_poly.boundingRect() log_debug("Previous bounding box = %dx%d+%d+%d" % (prev_bbox.width(), prev_bbox.height(), prev_bbox.left(), prev_bbox.top())) self.ui.previousData.ensureVisible(prev_bbox) @pyqtSignature("") def on_actionGotoPoint_triggered(self): data = self._data points = [str(pid) for pid in data.cell_points] selected, ok = QInputDialog.getItem(self, "Goto point", "Select the point to go to", points, 0) if ok: pid = int(selected) self.ui.action_Move_point.setChecked(True) if pid not in data.cell_points: return prev_pos = self._previousScene.current_data._current_index cur_pos = self._currentScene.current_data._current_index prev_data = self._previousScene.current_data if not pid in prev_data: closest = -1 best_dist = len(data)+1 for img_data in data: if pid in img_data: dist = abs(img_data._current_index - prev_pos) if dist < best_dist: best_dist = dist closest = img_data._current_index new_prev_pos = closest else: new_prev_pos = prev_pos new_cur_pos = min(max(cur_pos + new_prev_pos - prev_pos, 0), len(data)) self.ui.previousState.setCurrentIndex(new_prev_pos) self.ui.currentState.setCurrentIndex(new_cur_pos) self._previousScene.setSelectedIds([pid]) self._currentScene.setSelectedIds([pid]) self.ui.previousData.centerOn(self._previousScene.current_data[pid])
class ToolGrid(QFrame): """ A widget containing a grid of actions/buttons. Actions can be added using standard :func:`QWidget.addAction(QAction)` and :func:`QWidget.insertAction(int, QAction)` methods. Parameters ---------- parent : :class:`QWidget` Parent widget. columns : int Number of columns in the grid layout. buttonSize : :class:`QSize`, optional Size of tool buttons in the grid. iconSize : :class:`QSize`, optional Size of icons in the buttons. toolButtonStyle : :class:`Qt.ToolButtonStyle` Tool button style. """ actionTriggered = Signal(QAction) actionHovered = Signal(QAction) def __init__(self, parent=None, columns=4, buttonSize=None, iconSize=None, toolButtonStyle=Qt.ToolButtonTextUnderIcon): QFrame.__init__(self, parent) if buttonSize is not None: buttonSize = QSize(buttonSize) if iconSize is not None: iconSize = QSize(iconSize) self.__columns = columns self.__buttonSize = buttonSize or QSize(50, 50) self.__iconSize = iconSize or QSize(26, 26) self.__toolButtonStyle = toolButtonStyle self.__gridSlots = [] self.__buttonListener = ToolButtonEventListener(self) self.__buttonListener.buttonRightClicked.connect( self.__onButtonRightClick) self.__buttonListener.buttonEnter.connect(self.__onButtonEnter) self.__mapper = QSignalMapper() self.__mapper.mapped[QObject].connect(self.__onClicked) self.__setupUi() def __setupUi(self): layout = QGridLayout() layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) layout.setSizeConstraint(QGridLayout.SetFixedSize) self.setLayout(layout) self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.MinimumExpanding) def setButtonSize(self, size): """ Set the button size. """ if self.__buttonSize != size: self.__buttonSize = size for slot in self.__gridSlots: slot.button.setFixedSize(size) def buttonSize(self): """ Return the button size. """ return QSize(self.__buttonSize) def setIconSize(self, size): """ Set the button icon size. """ if self.__iconSize != size: self.__iconSize = size for slot in self.__gridSlots: slot.button.setIconSize(size) def iconSize(self): """ Return the icon size """ return QSize(self.__iconSize) def setToolButtonStyle(self, style): """ Set the tool button style. """ if self.__toolButtonStyle != style: self.__toolButtonStyle = style for slot in self.__gridSlots: slot.button.setToolButtonStyle(style) def toolButtonStyle(self): """ Return the tool button style. """ return self.__toolButtonStyle def setColumnCount(self, columns): """ Set the number of button/action columns. """ if self.__columns != columns: self.__columns = columns self.__relayout() def columns(self): """ Return the number of columns in the grid. """ return self.__columns def clear(self): """ Clear all actions/buttons. """ for slot in reversed(list(self.__gridSlots)): self.removeAction(slot.action) self.__gridSlots = [] def insertAction(self, before, action): """ Insert a new action at the position currently occupied by `before` (can also be an index). Parameters ---------- before : :class:`QAction` or int Position where the `action` should be inserted. action : :class:`QAction` Action to insert """ if isinstance(before, int): actions = list(self.actions()) if len(actions) == 0 or before >= len(actions): # Insert as the first action or the last action. return self.addAction(action) before = actions[before] return QFrame.insertAction(self, before, action) def setActions(self, actions): """ Clear the grid and add `actions`. """ self.clear() for action in actions: self.addAction(action) def buttonForAction(self, action): """ Return the :class:`QToolButton` instance button for `action`. """ actions = [slot.action for slot in self.__gridSlots] index = actions.index(action) return self.__gridSlots[index].button def createButtonForAction(self, action): """ Create and return a :class:`QToolButton` for action. """ button = _ToolGridButton(self) button.setDefaultAction(action) if self.__buttonSize.isValid(): button.setFixedSize(self.__buttonSize) if self.__iconSize.isValid(): button.setIconSize(self.__iconSize) button.setToolButtonStyle(self.__toolButtonStyle) button.setProperty("tool-grid-button", QVariant(True)) return button def count(self): """ Return the number of buttons/actions in the grid. """ return len(self.__gridSlots) def actionEvent(self, event): QFrame.actionEvent(self, event) if event.type() == QEvent.ActionAdded: # Note: the action is already in the self.actions() list. actions = list(self.actions()) index = actions.index(event.action()) self.__insertActionButton(index, event.action()) elif event.type() == QEvent.ActionRemoved: self.__removeActionButton(event.action()) def __insertActionButton(self, index, action): """Create a button for the action and add it to the layout at index. """ self.__shiftGrid(index, 1) button = self.createButtonForAction(action) row = index / self.__columns column = index % self.__columns self.layout().addWidget(button, row, column, Qt.AlignLeft | Qt.AlignTop) self.__gridSlots.insert(index, _ToolGridSlot(button, action, row, column)) self.__mapper.setMapping(button, action) button.clicked.connect(self.__mapper.map) button.installEventFilter(self.__buttonListener) button.installEventFilter(self) def __removeActionButton(self, action): """Remove the button for the action from the layout and delete it. """ actions = [slot.action for slot in self.__gridSlots] index = actions.index(action) slot = self.__gridSlots.pop(index) slot.button.removeEventFilter(self.__buttonListener) slot.button.removeEventFilter(self) self.__mapper.removeMappings(slot.button) self.layout().removeWidget(slot.button) self.__shiftGrid(index + 1, -1) slot.button.deleteLater() def __shiftGrid(self, start, count=1): """Shift all buttons starting at index `start` by `count` cells. """ button_count = self.layout().count() direction = 1 if count >= 0 else -1 if direction == 1: start, end = button_count - 1, start - 1 else: start, end = start, button_count for index in range(start, end, -direction): item = self.layout().itemAtPosition(index / self.__columns, index % self.__columns) if item: button = item.widget() new_index = index + count self.layout().addWidget(button, new_index / self.__columns, new_index % self.__columns, Qt.AlignLeft | Qt.AlignTop) def __relayout(self): """Relayout the buttons. """ for i in reversed(range(self.layout().count())): self.layout().takeAt(i) self.__gridSlots = [ _ToolGridSlot(slot.button, slot.action, i / self.__columns, i % self.__columns) for i, slot in enumerate(self.__gridSlots) ] for slot in self.__gridSlots: self.layout().addWidget(slot.button, slot.row, slot.column, Qt.AlignLeft | Qt.AlignTop) def __indexOf(self, button): """Return the index of button widget. """ buttons = [slot.button for slot in self.__gridSlots] return buttons.index(button) def __onButtonRightClick(self, button): pass def __onButtonEnter(self, button): action = button.defaultAction() self.actionHovered.emit(action) def __onClicked(self, action): self.actionTriggered.emit(action) def paintEvent(self, event): return utils.StyledWidget_paintEvent(self, event) def eventFilter(self, obj, event): etype = event.type() if etype == QEvent.KeyPress and obj.hasFocus(): key = event.key() if key in [Qt.Key_Up, Qt.Key_Down, Qt.Key_Left, Qt.Key_Right]: if self.__focusMove(obj, key): event.accept() return True return QFrame.eventFilter(self, obj, event) def __focusMove(self, focus, key): assert (focus is self.focusWidget()) try: index = self.__indexOf(focus) except IndexError: return False if key == Qt.Key_Down: index += self.__columns elif key == Qt.Key_Up: index -= self.__columns elif key == Qt.Key_Left: index -= 1 elif key == Qt.Key_Right: index += 1 if index >= 0 and index < self.count(): button = self.__gridSlots[index].button button.setFocus(Qt.TabFocusReason) return True else: return False
class CanvasScene(QGraphicsScene): """ A Graphics Scene for displaying an :class:`~.scheme.Scheme` instance. """ #: Signal emitted when a :class:`NodeItem` has been added to the scene. node_item_added = Signal(object) #: Signal emitted when a :class:`NodeItem` has been removed from the #: scene. node_item_removed = Signal(object) #: Signal emitted when a new :class:`LinkItem` has been added to the #: scene. link_item_added = Signal(object) #: Signal emitted when a :class:`LinkItem` has been removed. link_item_removed = Signal(object) #: Signal emitted when a :class:`Annotation` item has been added. annotation_added = Signal(object) #: Signal emitted when a :class:`Annotation` item has been removed. annotation_removed = Signal(object) #: Signal emitted when the position of a :class:`NodeItem` has changed. node_item_position_changed = Signal(object, QPointF) #: Signal emitted when an :class:`NodeItem` has been double clicked. node_item_double_clicked = Signal(object) #: An node item has been activated (clicked) node_item_activated = Signal(object) #: An node item has been hovered node_item_hovered = Signal(object) #: Link item has been hovered link_item_hovered = Signal(object) def __init__(self, *args, **kwargs): QGraphicsScene.__init__(self, *args, **kwargs) self.scheme = None self.registry = None # All node items self.__node_items = [] # Mapping from SchemeNodes to canvas items self.__item_for_node = {} # All link items self.__link_items = [] # Mapping from SchemeLinks to canvas items. self.__item_for_link = {} # All annotation items self.__annotation_items = [] # Mapping from SchemeAnnotations to canvas items. self.__item_for_annotation = {} # Is the scene editable self.editable = True # Anchor Layout self.__anchor_layout = AnchorLayout() self.addItem(self.__anchor_layout) self.__channel_names_visible = True self.__node_animation_enabled = True self.user_interaction_handler = None self.activated_mapper = QSignalMapper(self) self.activated_mapper.mapped[QObject].connect( lambda node: self.node_item_activated.emit(node) ) self.hovered_mapper = QSignalMapper(self) self.hovered_mapper.mapped[QObject].connect( lambda node: self.node_item_hovered.emit(node) ) self.position_change_mapper = QSignalMapper(self) self.position_change_mapper.mapped[QObject].connect( self._on_position_change ) log.info("'%s' intitialized." % self) def clear_scene(self): """ Clear (reset) the scene. """ if self.scheme is not None: self.scheme.node_added.disconnect(self.add_node) self.scheme.node_removed.disconnect(self.remove_node) self.scheme.link_added.disconnect(self.add_link) self.scheme.link_removed.disconnect(self.remove_link) self.scheme.annotation_added.disconnect(self.add_annotation) self.scheme.annotation_removed.disconnect(self.remove_annotation) self.scheme.node_state_changed.disconnect( self.on_widget_state_change ) self.scheme.channel_state_changed.disconnect( self.on_link_state_change ) # Remove all items to make sure all signals from scheme items # to canvas items are disconnected. for annot in self.scheme.annotations: if annot in self.__item_for_annotation: self.remove_annotation(annot) for link in self.scheme.links: if link in self.__item_for_link: self.remove_link(link) for node in self.scheme.nodes: if node in self.__item_for_node: self.remove_node(node) self.scheme = None self.__node_items = [] self.__item_for_node = {} self.__link_items = [] self.__item_for_link = {} self.__annotation_items = [] self.__item_for_annotation = {} self.__anchor_layout.deleteLater() self.user_interaction_handler = None self.clear() log.info("'%s' cleared." % self) def set_scheme(self, scheme): """ Set the scheme to display. Populates the scene with nodes and links already in the scheme. Any further change to the scheme will be reflected in the scene. Parameters ---------- scheme : :class:`~.scheme.Scheme` """ if self.scheme is not None: # Clear the old scheme self.clear_scene() log.info("Setting scheme '%s' on '%s'" % (scheme, self)) self.scheme = scheme if self.scheme is not None: self.scheme.node_added.connect(self.add_node) self.scheme.node_removed.connect(self.remove_node) self.scheme.link_added.connect(self.add_link) self.scheme.link_removed.connect(self.remove_link) self.scheme.annotation_added.connect(self.add_annotation) self.scheme.annotation_removed.connect(self.remove_annotation) self.scheme.node_state_changed.connect( self.on_widget_state_change ) self.scheme.channel_state_changed.connect( self.on_link_state_change ) self.scheme.topology_changed.connect(self.on_scheme_change) for node in scheme.nodes: self.add_node(node) for link in scheme.links: self.add_link(link) for annot in scheme.annotations: self.add_annotation(annot) def set_registry(self, registry): """ Set the widget registry. """ # TODO: Remove/Deprecate. Is used only to get the category/background # color. That should be part of the SchemeNode/WidgetDescription. log.info("Setting registry '%s on '%s'." % (registry, self)) self.registry = registry def set_anchor_layout(self, layout): """ Set an :class:`~.layout.AnchorLayout` """ if self.__anchor_layout != layout: if self.__anchor_layout: self.__anchor_layout.deleteLater() self.__anchor_layout = None self.__anchor_layout = layout def anchor_layout(self): """ Return the anchor layout instance. """ return self.__anchor_layout def set_channel_names_visible(self, visible): """ Set the channel names visibility. """ self.__channel_names_visible = visible for link in self.__link_items: link.setChannelNamesVisible(visible) def channel_names_visible(self): """ Return the channel names visibility state. """ return self.__channel_names_visible def set_node_animation_enabled(self, enabled): """ Set node animation enabled state. """ if self.__node_animation_enabled != enabled: self.__node_animation_enabled = enabled for node in self.__node_items: node.setAnimationEnabled(enabled) def add_node_item(self, item): """ Add a :class:`.NodeItem` instance to the scene. """ if item in self.__node_items: raise ValueError("%r is already in the scene." % item) if item.pos().isNull(): if self.__node_items: pos = self.__node_items[-1].pos() + QPointF(150, 0) else: pos = QPointF(150, 150) item.setPos(pos) item.setFont(self.font()) # Set signal mappings self.activated_mapper.setMapping(item, item) item.activated.connect(self.activated_mapper.map) self.hovered_mapper.setMapping(item, item) item.hovered.connect(self.hovered_mapper.map) self.position_change_mapper.setMapping(item, item) item.positionChanged.connect(self.position_change_mapper.map) self.addItem(item) self.__node_items.append(item) self.node_item_added.emit(item) log.info("Added item '%s' to '%s'" % (item, self)) return item def add_node(self, node): """ Add and return a default constructed :class:`.NodeItem` for a :class:`SchemeNode` instance `node`. If the `node` is already in the scene do nothing and just return its item. """ if node in self.__item_for_node: # Already added return self.__item_for_node[node] item = self.new_node_item(node.description) if node.position: pos = QPointF(*node.position) item.setPos(pos) item.setTitle(node.title) item.setProcessingState(node.processing_state) item.setProgress(node.progress) for message in node.state_messages(): item.setStateMessage(message) item.setStatusMessage(node.status_message()) self.__item_for_node[node] = item node.position_changed.connect(self.__on_node_pos_changed) node.title_changed.connect(item.setTitle) node.progress_changed.connect(item.setProgress) node.processing_state_changed.connect(item.setProcessingState) node.state_message_changed.connect(item.setStateMessage) node.status_message_changed.connect(item.setStatusMessage) return self.add_node_item(item) def new_node_item(self, widget_desc, category_desc=None): """ Construct an new :class:`.NodeItem` from a `WidgetDescription`. Optionally also set `CategoryDescription`. """ item = items.NodeItem() item.setWidgetDescription(widget_desc) if category_desc is None and self.registry and widget_desc.category: category_desc = self.registry.category(widget_desc.category) if category_desc is None and self.registry is not None: try: category_desc = self.registry.category(widget_desc.category) except KeyError: pass if category_desc is not None: item.setWidgetCategory(category_desc) item.setAnimationEnabled(self.__node_animation_enabled) return item def remove_node_item(self, item): """ Remove `item` (:class:`.NodeItem`) from the scene. """ self.activated_mapper.removeMappings(item) self.hovered_mapper.removeMappings(item) self.position_change_mapper.removeMappings(item) item.hide() self.removeItem(item) self.__node_items.remove(item) self.node_item_removed.emit(item) log.info("Removed item '%s' from '%s'" % (item, self)) def remove_node(self, node): """ Remove the :class:`.NodeItem` instance that was previously constructed for a :class:`SchemeNode` `node` using the `add_node` method. """ item = self.__item_for_node.pop(node) node.position_changed.disconnect(self.__on_node_pos_changed) node.title_changed.disconnect(item.setTitle) node.progress_changed.disconnect(item.setProgress) node.processing_state_changed.disconnect(item.setProcessingState) node.state_message_changed.disconnect(item.setStateMessage) self.remove_node_item(item) def node_items(self): """ Return all :class:`.NodeItem` instances in the scene. """ return list(self.__node_items) def add_link_item(self, item): """ Add a link (:class:`.LinkItem`) to the scene. """ if item.scene() is not self: self.addItem(item) item.setFont(self.font()) self.__link_items.append(item) self.link_item_added.emit(item) log.info("Added link %r -> %r to '%s'" % \ (item.sourceItem.title(), item.sinkItem.title(), self)) self.__anchor_layout.invalidateLink(item) return item def add_link(self, scheme_link): """ Create and add a :class:`.LinkItem` instance for a :class:`SchemeLink` instance. If the link is already in the scene do nothing and just return its :class:`.LinkItem`. """ if scheme_link in self.__item_for_link: return self.__item_for_link[scheme_link] source = self.__item_for_node[scheme_link.source_node] sink = self.__item_for_node[scheme_link.sink_node] item = self.new_link_item(source, scheme_link.source_channel, sink, scheme_link.sink_channel) item.setEnabled(scheme_link.enabled) scheme_link.enabled_changed.connect(item.setEnabled) if scheme_link.is_dynamic(): item.setDynamic(True) item.setDynamicEnabled(scheme_link.dynamic_enabled) scheme_link.dynamic_enabled_changed.connect(item.setDynamicEnabled) item.setRuntimeState(scheme_link.runtime_state()) scheme_link.state_changed.connect(item.setRuntimeState) self.add_link_item(item) self.__item_for_link[scheme_link] = item return item def new_link_item(self, source_item, source_channel, sink_item, sink_channel): """ Construct and return a new :class:`.LinkItem` """ item = items.LinkItem() item.setSourceItem(source_item) item.setSinkItem(sink_item) def channel_name(channel): if isinstance(channel, str): return channel else: return channel.name source_name = channel_name(source_channel) sink_name = channel_name(sink_channel) fmt = "<b>{0}</b> \u2192 <b>{1}</b>" item.setToolTip( fmt.format(escape(source_name), escape(sink_name)) ) item.setSourceName(source_name) item.setSinkName(sink_name) item.setChannelNamesVisible(self.__channel_names_visible) return item def remove_link_item(self, item): """ Remove a link (:class:`.LinkItem`) from the scene. """ # Invalidate the anchor layout. self.__anchor_layout.invalidateAnchorItem( item.sourceItem.outputAnchorItem ) self.__anchor_layout.invalidateAnchorItem( item.sinkItem.inputAnchorItem ) self.__link_items.remove(item) # Remove the anchor points. item.removeLink() self.removeItem(item) self.link_item_removed.emit(item) log.info("Removed link '%s' from '%s'" % (item, self)) return item def remove_link(self, scheme_link): """ Remove a :class:`.LinkItem` instance that was previously constructed for a :class:`SchemeLink` instance `link` using the `add_link` method. """ item = self.__item_for_link.pop(scheme_link) scheme_link.enabled_changed.disconnect(item.setEnabled) if scheme_link.is_dynamic(): scheme_link.dynamic_enabled_changed.disconnect( item.setDynamicEnabled ) scheme_link.state_changed.disconnect(item.setRuntimeState) self.remove_link_item(item) def link_items(self): """ Return all :class:`.LinkItem`\s in the scene. """ return list(self.__link_items) def add_annotation_item(self, annotation): """ Add an :class:`.Annotation` item to the scene. """ self.__annotation_items.append(annotation) self.addItem(annotation) self.annotation_added.emit(annotation) return annotation def add_annotation(self, scheme_annot): """ Create a new item for :class:`SchemeAnnotation` and add it to the scene. If the `scheme_annot` is already in the scene do nothing and just return its item. """ if scheme_annot in self.__item_for_annotation: # Already added return self.__item_for_annotation[scheme_annot] if isinstance(scheme_annot, scheme.SchemeTextAnnotation): item = items.TextAnnotation() item.setPlainText(scheme_annot.text) x, y, w, h = scheme_annot.rect item.setPos(x, y) item.resize(w, h) item.setTextInteractionFlags(Qt.TextEditorInteraction) font = font_from_dict(scheme_annot.font, item.font()) item.setFont(font) scheme_annot.text_changed.connect(item.setPlainText) elif isinstance(scheme_annot, scheme.SchemeArrowAnnotation): item = items.ArrowAnnotation() start, end = scheme_annot.start_pos, scheme_annot.end_pos item.setLine(QLineF(QPointF(*start), QPointF(*end))) item.setColor(QColor(scheme_annot.color)) scheme_annot.geometry_changed.connect( self.__on_scheme_annot_geometry_change ) self.add_annotation_item(item) self.__item_for_annotation[scheme_annot] = item return item def remove_annotation_item(self, annotation): """ Remove an :class:`.Annotation` instance from the scene. """ self.__annotation_items.remove(annotation) self.removeItem(annotation) self.annotation_removed.emit(annotation) def remove_annotation(self, scheme_annotation): """ Remove an :class:`.Annotation` instance that was previously added using :func:`add_anotation`. """ item = self.__item_for_annotation.pop(scheme_annotation) scheme_annotation.geometry_changed.disconnect( self.__on_scheme_annot_geometry_change ) if isinstance(scheme_annotation, scheme.SchemeTextAnnotation): scheme_annotation.text_changed.disconnect( item.setPlainText ) self.remove_annotation_item(item) def annotation_items(self): """ Return all :class:`.Annotation` items in the scene. """ return self.__annotation_items def item_for_annotation(self, scheme_annotation): return self.__item_for_annotation[scheme_annotation] def annotation_for_item(self, item): rev = dict(reversed(item) \ for item in self.__item_for_annotation.items()) return rev[item] def commit_scheme_node(self, node): """ Commit the `node` into the scheme. """ if not self.editable: raise Exception("Scheme not editable.") if node not in self.__item_for_node: raise ValueError("No 'NodeItem' for node.") item = self.__item_for_node[node] try: self.scheme.add_node(node) except Exception: log.error("An error occurred while committing node '%s'", node, exc_info=True) # Cleanup (remove the node item) self.remove_node_item(item) raise log.info("Commited node '%s' from '%s' to '%s'" % \ (node, self, self.scheme)) def commit_scheme_link(self, link): """ Commit a scheme link. """ if not self.editable: raise Exception("Scheme not editable") if link not in self.__item_for_link: raise ValueError("No 'LinkItem' for link.") self.scheme.add_link(link) log.info("Commited link '%s' from '%s' to '%s'" % \ (link, self, self.scheme)) def node_for_item(self, item): """ Return the `SchemeNode` for the `item`. """ rev = dict([(v, k) for k, v in self.__item_for_node.items()]) return rev[item] def item_for_node(self, node): """ Return the :class:`NodeItem` instance for a :class:`SchemeNode`. """ return self.__item_for_node[node] def link_for_item(self, item): """ Return the `SchemeLink for `item` (:class:`LinkItem`). """ rev = dict([(v, k) for k, v in self.__item_for_link.items()]) return rev[item] def item_for_link(self, link): """ Return the :class:`LinkItem` for a :class:`SchemeLink` """ return self.__item_for_link[link] def selected_node_items(self): """ Return the selected :class:`NodeItem`'s. """ return [item for item in self.__node_items if item.isSelected()] def selected_annotation_items(self): """ Return the selected :class:`Annotation`'s """ return [item for item in self.__annotation_items if item.isSelected()] def node_links(self, node_item): """ Return all links from the `node_item` (:class:`NodeItem`). """ return self.node_output_links(node_item) + \ self.node_input_links(node_item) def node_output_links(self, node_item): """ Return a list of all output links from `node_item`. """ return [link for link in self.__link_items if link.sourceItem == node_item] def node_input_links(self, node_item): """ Return a list of all input links for `node_item`. """ return [link for link in self.__link_items if link.sinkItem == node_item] def neighbor_nodes(self, node_item): """ Return a list of `node_item`'s (class:`NodeItem`) neighbor nodes. """ neighbors = list(map(attrgetter("sourceItem"), self.node_input_links(node_item))) neighbors.extend(map(attrgetter("sinkItem"), self.node_output_links(node_item))) return neighbors def on_widget_state_change(self, widget, state): pass def on_link_state_change(self, link, state): pass def on_scheme_change(self, ): pass def _on_position_change(self, item): # Invalidate the anchor point layout and schedule a layout. self.__anchor_layout.invalidateNode(item) self.node_item_position_changed.emit(item, item.pos()) def __on_node_pos_changed(self, pos): node = self.sender() item = self.__item_for_node[node] item.setPos(*pos) def __on_scheme_annot_geometry_change(self): annot = self.sender() item = self.__item_for_annotation[annot] if isinstance(annot, scheme.SchemeTextAnnotation): item.setGeometry(QRectF(*annot.rect)) elif isinstance(annot, scheme.SchemeArrowAnnotation): p1 = item.mapFromScene(QPointF(*annot.start_pos)) p2 = item.mapFromScene(QPointF(*annot.end_pos)) item.setLine(QLineF(p1, p2)) else: pass def item_at(self, pos, type_or_tuple=None, buttons=0): """Return the item at `pos` that is an instance of the specified type (`type_or_tuple`). If `buttons` (`Qt.MouseButtons`) is given only return the item if it is the top level item that would accept any of the buttons (`QGraphicsItem.acceptedMouseButtons`). """ rect = QRectF(pos, QSizeF(1, 1)) items = self.items(rect) if buttons: items = itertools.dropwhile( lambda item: not item.acceptedMouseButtons() & buttons, items ) items = list(items)[:1] if type_or_tuple: items = [i for i in items if isinstance(i, type_or_tuple)] return items[0] if items else None if PYQT_VERSION < 0x40900: # For QGraphicsObject subclasses items, itemAt ... return a # QGraphicsItem wrapper instance and not the actual class instance. def itemAt(self, *args, **kwargs): item = QGraphicsScene.itemAt(self, *args, **kwargs) return toGraphicsObjectIfPossible(item) def items(self, *args, **kwargs): items = QGraphicsScene.items(self, *args, **kwargs) return list(map(toGraphicsObjectIfPossible, items)) def selectedItems(self, *args, **kwargs): return list(map(toGraphicsObjectIfPossible, QGraphicsScene.selectedItems(self, *args, **kwargs))) def collidingItems(self, *args, **kwargs): return list(map(toGraphicsObjectIfPossible, QGraphicsScene.collidingItems(self, *args, **kwargs))) def focusItem(self, *args, **kwargs): item = QGraphicsScene.focusItem(self, *args, **kwargs) return toGraphicsObjectIfPossible(item) def mouseGrabberItem(self, *args, **kwargs): item = QGraphicsScene.mouseGrabberItem(self, *args, **kwargs) return toGraphicsObjectIfPossible(item) def mousePressEvent(self, event): if self.user_interaction_handler and \ self.user_interaction_handler.mousePressEvent(event): return # Right (context) click on the node item. If the widget is not # in the current selection then select the widget (only the widget). # Else simply return and let customContextMenuReqested signal # handle it shape_item = self.item_at(event.scenePos(), items.NodeItem) if shape_item and event.button() == Qt.RightButton and \ shape_item.flags() & QGraphicsItem.ItemIsSelectable: if not shape_item.isSelected(): self.clearSelection() shape_item.setSelected(True) return QGraphicsScene.mousePressEvent(self, event) def mouseMoveEvent(self, event): if self.user_interaction_handler and \ self.user_interaction_handler.mouseMoveEvent(event): return return QGraphicsScene.mouseMoveEvent(self, event) def mouseReleaseEvent(self, event): if self.user_interaction_handler and \ self.user_interaction_handler.mouseReleaseEvent(event): return return QGraphicsScene.mouseReleaseEvent(self, event) def mouseDoubleClickEvent(self, event): if self.user_interaction_handler and \ self.user_interaction_handler.mouseDoubleClickEvent(event): return return QGraphicsScene.mouseDoubleClickEvent(self, event) def keyPressEvent(self, event): if self.user_interaction_handler and \ self.user_interaction_handler.keyPressEvent(event): return return QGraphicsScene.keyPressEvent(self, event) def keyReleaseEvent(self, event): if self.user_interaction_handler and \ self.user_interaction_handler.keyReleaseEvent(event): return return QGraphicsScene.keyReleaseEvent(self, event) def set_user_interaction_handler(self, handler): if self.user_interaction_handler and \ not self.user_interaction_handler.isFinished(): self.user_interaction_handler.cancel() log.info("Setting interaction '%s' to '%s'" % (handler, self)) self.user_interaction_handler = handler if handler: handler.start() def event(self, event): # TODO: change the base class of Node/LinkItem to QGraphicsWidget. # It already handles font changes. if event.type() == QEvent.FontChange: self.__update_font() return QGraphicsScene.event(self, event) def __update_font(self): font = self.font() for item in self.__node_items + self.__link_items: item.setFont(font) def __str__(self): return "%s(objectName=%r, ...)" % \ (type(self).__name__, str(self.objectName()))
class ToolGrid(QFrame): """ A widget containing a grid of actions/buttons. Actions can be added using standard :ref:`QWidget.addAction(QAction)` and :ref:`QWidget.insertAction(int, QAction)` methods. Parameters ---------- parent : :class:`QWidget` Parent widget. columns : int Number of columns in the grid layout. buttonSize : :class:`QSize`, optional Size of tool buttons in the grid. iconSize : :class:`QSize`, optional Size of icons in the buttons. toolButtonStyle : :class:`Qt.ToolButtonStyle` Tool button style. """ actionTriggered = Signal(QAction) actionHovered = Signal(QAction) def __init__(self, parent=None, columns=4, buttonSize=None, iconSize=None, toolButtonStyle=Qt.ToolButtonTextUnderIcon): QFrame.__init__(self, parent) if buttonSize is not None: buttonSize = QSize(buttonSize) if iconSize is not None: iconSize = QSize(iconSize) self.__columns = columns self.__buttonSize = buttonSize or QSize(50, 50) self.__iconSize = iconSize or QSize(26, 26) self.__toolButtonStyle = toolButtonStyle self.__gridSlots = [] self.__buttonListener = ToolButtonEventListener(self) self.__buttonListener.buttonRightClicked.connect( self.__onButtonRightClick) self.__buttonListener.buttonEnter.connect( self.__onButtonEnter) self.__mapper = QSignalMapper() self.__mapper.mapped[QObject].connect(self.__onClicked) self.__setupUi() def __setupUi(self): layout = QGridLayout() layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) layout.setSizeConstraint(QGridLayout.SetFixedSize) self.setLayout(layout) self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.MinimumExpanding) def setButtonSize(self, size): """ Set the button size. """ if self.__buttonSize != size: self.__buttonSize = size for slot in self.__gridSlots: slot.button.setFixedSize(size) def buttonSize(self): """ Return the button size. """ return QSize(self.__buttonSize) def setIconSize(self, size): """ Set the button icon size. """ if self.__iconSize != size: self.__iconSize = size for slot in self.__gridSlots: slot.button.setIconSize(size) def iconSize(self): """ Return the icon size """ return QSize(self.__iconSize) def setToolButtonStyle(self, style): """ Set the tool button style. """ if self.__toolButtonStyle != style: self.__toolButtonStyle = style for slot in self.__gridSlots: slot.button.setToolButtonStyle(style) def toolButtonStyle(self): """ Return the tool button style. """ return self.__toolButtonStyle def setColumnCount(self, columns): """ Set the number of button/action columns. """ if self.__columns != columns: self.__columns = columns self.__relayout() def columns(self): """ Return the number of columns in the grid. """ return self.__columns def clear(self): """ Clear all actions/buttons. """ for slot in reversed(list(self.__gridSlots)): self.removeAction(slot.action) self.__gridSlots = [] def insertAction(self, before, action): """ Insert a new action at the position currently occupied by `before` (can also be an index). Parameters ---------- before : :class:`QAction` or int Position where the `action` should be inserted. action : :class:`QAction` Action to insert """ if isinstance(before, int): actions = list(self.actions()) if len(actions) == 0 or before >= len(actions): # Insert as the first action or the last action. return self.addAction(action) before = actions[before] return QFrame.insertAction(self, before, action) def setActions(self, actions): """ Clear the grid and add `actions`. """ self.clear() for action in actions: self.addAction(action) def buttonForAction(self, action): """ Return the :class:`QToolButton` instance button for `action`. """ actions = [slot.action for slot in self.__gridSlots] index = actions.index(action) return self.__gridSlots[index].button def createButtonForAction(self, action): """ Create and return a :class:`QToolButton` for action. """ button = _ToolGridButton(self) button.setDefaultAction(action) if self.__buttonSize.isValid(): button.setFixedSize(self.__buttonSize) if self.__iconSize.isValid(): button.setIconSize(self.__iconSize) button.setToolButtonStyle(self.__toolButtonStyle) button.setProperty("tool-grid-button", qtcompat.qwrap(True)) return button def count(self): """ Return the number of buttons/actions in the grid. """ return len(self.__gridSlots) def actionEvent(self, event): QFrame.actionEvent(self, event) if event.type() == QEvent.ActionAdded: # Note: the action is already in the self.actions() list. actions = list(self.actions()) index = actions.index(event.action()) self.__insertActionButton(index, event.action()) elif event.type() == QEvent.ActionRemoved: self.__removeActionButton(event.action()) def __insertActionButton(self, index, action): """Create a button for the action and add it to the layout at index. """ self.__shiftGrid(index, 1) button = self.createButtonForAction(action) row = index / self.__columns column = index % self.__columns self.layout().addWidget( button, row, column, Qt.AlignLeft | Qt.AlignTop ) self.__gridSlots.insert( index, _ToolGridSlot(button, action, row, column) ) self.__mapper.setMapping(button, action) button.clicked.connect(self.__mapper.map) button.installEventFilter(self.__buttonListener) button.installEventFilter(self) def __removeActionButton(self, action): """Remove the button for the action from the layout and delete it. """ actions = [slot.action for slot in self.__gridSlots] index = actions.index(action) slot = self.__gridSlots.pop(index) slot.button.removeEventFilter(self.__buttonListener) slot.button.removeEventFilter(self) self.__mapper.removeMappings(slot.button) self.layout().removeWidget(slot.button) self.__shiftGrid(index + 1, -1) slot.button.deleteLater() def __shiftGrid(self, start, count=1): """Shift all buttons starting at index `start` by `count` cells. """ button_count = self.layout().count() direction = 1 if count >= 0 else -1 if direction == 1: start, end = button_count - 1, start - 1 else: start, end = start, button_count for index in range(start, end, -direction): item = self.layout().itemAtPosition(index / self.__columns, index % self.__columns) if item: button = item.widget() new_index = index + count self.layout().addWidget(button, new_index / self.__columns, new_index % self.__columns, Qt.AlignLeft | Qt.AlignTop) def __relayout(self): """Relayout the buttons. """ for i in reversed(range(self.layout().count())): self.layout().takeAt(i) self.__gridSlots = [_ToolGridSlot(slot.button, slot.action, i / self.__columns, i % self.__columns) for i, slot in enumerate(self.__gridSlots)] for slot in self.__gridSlots: self.layout().addWidget(slot.button, slot.row, slot.column, Qt.AlignLeft | Qt.AlignTop) def __indexOf(self, button): """Return the index of button widget. """ buttons = [slot.button for slot in self.__gridSlots] return buttons.index(button) def __onButtonRightClick(self, button): pass def __onButtonEnter(self, button): action = button.defaultAction() self.actionHovered.emit(action) def __onClicked(self, action): self.actionTriggered.emit(action) def paintEvent(self, event): return utils.StyledWidget_paintEvent(self, event) def eventFilter(self, obj, event): etype = event.type() if etype == QEvent.KeyPress and obj.hasFocus(): key = event.key() if key in [Qt.Key_Up, Qt.Key_Down, Qt.Key_Left, Qt.Key_Right]: if self.__focusMove(obj, key): event.accept() return True return QFrame.eventFilter(self, obj, event) def __focusMove(self, focus, key): assert(focus is self.focusWidget()) try: index = self.__indexOf(focus) except IndexError: return False if key == Qt.Key_Down: index += self.__columns elif key == Qt.Key_Up: index -= self.__columns elif key == Qt.Key_Left: index -= 1 elif key == Qt.Key_Right: index += 1 if index >= 0 and index < self.count(): button = self.__gridSlots[index].button button.setFocus(Qt.TabFocusReason) return True else: return False
class CanvasScene(QGraphicsScene): """ A Graphics Scene for displaying an :class:`~.scheme.Scheme` instance. """ #: Signal emitted when a :class:`NodeItem` has been added to the scene. node_item_added = Signal(object) #: Signal emitted when a :class:`NodeItem` has been removed from the #: scene. node_item_removed = Signal(object) #: Signal emitted when a new :class:`LinkItem` has been added to the #: scene. link_item_added = Signal(object) #: Signal emitted when a :class:`LinkItem` has been removed. link_item_removed = Signal(object) #: Signal emitted when a :class:`Annotation` item has been added. annotation_added = Signal(object) #: Signal emitted when a :class:`Annotation` item has been removed. annotation_removed = Signal(object) #: Signal emitted when the position of a :class:`NodeItem` has changed. node_item_position_changed = Signal(object, QPointF) #: Signal emitted when an :class:`NodeItem` has been double clicked. node_item_double_clicked = Signal(object) #: An node item has been activated (clicked) node_item_activated = Signal(object) #: An node item has been hovered node_item_hovered = Signal(object) #: Link item has been hovered link_item_hovered = Signal(object) def __init__(self, *args, **kwargs): QGraphicsScene.__init__(self, *args, **kwargs) self.scheme = None self.registry = None # All node items self.__node_items = [] # Mapping from SchemeNodes to canvas items self.__item_for_node = {} # All link items self.__link_items = [] # Mapping from SchemeLinks to canvas items. self.__item_for_link = {} # All annotation items self.__annotation_items = [] # Mapping from SchemeAnnotations to canvas items. self.__item_for_annotation = {} # Is the scene editable self.editable = True # Anchor Layout self.__anchor_layout = AnchorLayout() self.addItem(self.__anchor_layout) self.__channel_names_visible = True self.__node_animation_enabled = True self.user_interaction_handler = None self.activated_mapper = QSignalMapper(self) self.activated_mapper.mapped[QObject].connect( lambda node: self.node_item_activated.emit(node) ) self.hovered_mapper = QSignalMapper(self) self.hovered_mapper.mapped[QObject].connect( lambda node: self.node_item_hovered.emit(node) ) self.position_change_mapper = QSignalMapper(self) self.position_change_mapper.mapped[QObject].connect( self._on_position_change ) log.info("'%s' intitialized." % self) def clear_scene(self): """ Clear (reset) the scene. """ if self.scheme is not None: self.scheme.node_added.disconnect(self.add_node) self.scheme.node_removed.disconnect(self.remove_node) self.scheme.link_added.disconnect(self.add_link) self.scheme.link_removed.disconnect(self.remove_link) self.scheme.annotation_added.disconnect(self.add_annotation) self.scheme.annotation_removed.disconnect(self.remove_annotation) self.scheme.node_state_changed.disconnect( self.on_widget_state_change ) self.scheme.channel_state_changed.disconnect( self.on_link_state_change ) # Remove all items to make sure all signals from scheme items # to canvas items are disconnected. for annot in self.scheme.annotations: if annot in self.__item_for_annotation: self.remove_annotation(annot) for link in self.scheme.links: if link in self.__item_for_link: self.remove_link(link) for node in self.scheme.nodes: if node in self.__item_for_node: self.remove_node(node) self.scheme = None self.__node_items = [] self.__item_for_node = {} self.__link_items = [] self.__item_for_link = {} self.__annotation_items = [] self.__item_for_annotation = {} self.__anchor_layout.deleteLater() self.user_interaction_handler = None self.clear() log.info("'%s' cleared." % self) def set_scheme(self, scheme): """ Set the scheme to display. Populates the scene with nodes and links already in the scheme. Any further change to the scheme will be reflected in the scene. Parameters ---------- scheme : :class:`~.scheme.Scheme` """ if self.scheme is not None: # Clear the old scheme self.clear_scene() log.info("Setting scheme '%s' on '%s'" % (scheme, self)) self.scheme = scheme if self.scheme is not None: self.scheme.node_added.connect(self.add_node) self.scheme.node_removed.connect(self.remove_node) self.scheme.link_added.connect(self.add_link) self.scheme.link_removed.connect(self.remove_link) self.scheme.annotation_added.connect(self.add_annotation) self.scheme.annotation_removed.connect(self.remove_annotation) self.scheme.node_state_changed.connect( self.on_widget_state_change ) self.scheme.channel_state_changed.connect( self.on_link_state_change ) self.scheme.topology_changed.connect(self.on_scheme_change) for node in scheme.nodes: self.add_node(node) for link in scheme.links: self.add_link(link) for annot in scheme.annotations: self.add_annotation(annot) def set_registry(self, registry): """ Set the widget registry. """ # TODO: Remove/Deprecate. Is used only to get the category/background # color. That should be part of the SchemeNode/WidgetDescription. log.info("Setting registry '%s on '%s'." % (registry, self)) self.registry = registry def set_anchor_layout(self, layout): """ Set an :class:`~.layout.AnchorLayout` """ if self.__anchor_layout != layout: if self.__anchor_layout: self.__anchor_layout.deleteLater() self.__anchor_layout = None self.__anchor_layout = layout def anchor_layout(self): """ Return the anchor layout instance. """ return self.__anchor_layout def set_channel_names_visible(self, visible): """ Set the channel names visibility. """ self.__channel_names_visible = visible for link in self.__link_items: link.setChannelNamesVisible(visible) def channel_names_visible(self): """ Return the channel names visibility state. """ return self.__channel_names_visible def set_node_animation_enabled(self, enabled): """ Set node animation enabled state. """ if self.__node_animation_enabled != enabled: self.__node_animation_enabled = enabled for node in self.__node_items: node.setAnimationEnabled(enabled) def add_node_item(self, item): """ Add a :class:`.NodeItem` instance to the scene. """ if item in self.__node_items: raise ValueError("%r is already in the scene." % item) if item.pos().isNull(): if self.__node_items: pos = self.__node_items[-1].pos() + QPointF(150, 0) else: pos = QPointF(150, 150) item.setPos(pos) item.setFont(self.font()) # Set signal mappings self.activated_mapper.setMapping(item, item) item.activated.connect(self.activated_mapper.map) self.hovered_mapper.setMapping(item, item) item.hovered.connect(self.hovered_mapper.map) self.position_change_mapper.setMapping(item, item) item.positionChanged.connect(self.position_change_mapper.map) self.addItem(item) self.__node_items.append(item) self.node_item_added.emit(item) log.info("Added item '%s' to '%s'" % (item, self)) return item def add_node(self, node): """ Add and return a default constructed :class:`.NodeItem` for a :class:`SchemeNode` instance `node`. If the `node` is already in the scene do nothing and just return its item. """ if node in self.__item_for_node: # Already added return self.__item_for_node[node] item = self.new_node_item(node.description) if node.position: pos = QPointF(*node.position) item.setPos(pos) item.setTitle(node.title) item.setProcessingState(node.processing_state) item.setProgress(node.progress) for message in node.state_messages(): item.setStateMessage(message) item.setStatusMessage(node.status_message()) self.__item_for_node[node] = item node.position_changed.connect(self.__on_node_pos_changed) node.title_changed.connect(item.setTitle) node.progress_changed.connect(item.setProgress) node.processing_state_changed.connect(item.setProcessingState) node.state_message_changed.connect(item.setStateMessage) node.status_message_changed.connect(item.setStatusMessage) return self.add_node_item(item) def new_node_item(self, widget_desc, category_desc=None): """ Construct an new :class:`.NodeItem` from a `WidgetDescription`. Optionally also set `CategoryDescription`. """ item = items.NodeItem() item.setWidgetDescription(widget_desc) if category_desc is None and self.registry and widget_desc.category: category_desc = self.registry.category(widget_desc.category) if category_desc is None and self.registry is not None: try: category_desc = self.registry.category(widget_desc.category) except KeyError: pass if category_desc is not None: item.setWidgetCategory(category_desc) item.setAnimationEnabled(self.__node_animation_enabled) return item def remove_node_item(self, item): """ Remove `item` (:class:`.NodeItem`) from the scene. """ self.activated_mapper.removeMappings(item) self.hovered_mapper.removeMappings(item) self.position_change_mapper.removeMappings(item) item.hide() self.removeItem(item) self.__node_items.remove(item) self.node_item_removed.emit(item) log.info("Removed item '%s' from '%s'" % (item, self)) def remove_node(self, node): """ Remove the :class:`.NodeItem` instance that was previously constructed for a :class:`SchemeNode` `node` using the `add_node` method. """ item = self.__item_for_node.pop(node) node.position_changed.disconnect(self.__on_node_pos_changed) node.title_changed.disconnect(item.setTitle) node.progress_changed.disconnect(item.setProgress) node.processing_state_changed.disconnect(item.setProcessingState) node.state_message_changed.disconnect(item.setStateMessage) self.remove_node_item(item) def node_items(self): """ Return all :class:`.NodeItem` instances in the scene. """ return list(self.__node_items) def add_link_item(self, item): """ Add a link (:class:`.LinkItem`) to the scene. """ if item.scene() is not self: self.addItem(item) item.setFont(self.font()) self.__link_items.append(item) self.link_item_added.emit(item) log.info("Added link %r -> %r to '%s'" % \ (item.sourceItem.title(), item.sinkItem.title(), self)) self.__anchor_layout.invalidateLink(item) return item def add_link(self, scheme_link): """ Create and add a :class:`.LinkItem` instance for a :class:`SchemeLink` instance. If the link is already in the scene do nothing and just return its :class:`.LinkItem`. """ if scheme_link in self.__item_for_link: return self.__item_for_link[scheme_link] source = self.__item_for_node[scheme_link.source_node] sink = self.__item_for_node[scheme_link.sink_node] item = self.new_link_item(source, scheme_link.source_channel, sink, scheme_link.sink_channel) item.setEnabled(scheme_link.enabled) scheme_link.enabled_changed.connect(item.setEnabled) if scheme_link.is_dynamic(): item.setDynamic(True) item.setDynamicEnabled(scheme_link.dynamic_enabled) scheme_link.dynamic_enabled_changed.connect(item.setDynamicEnabled) self.add_link_item(item) self.__item_for_link[scheme_link] = item return item def new_link_item(self, source_item, source_channel, sink_item, sink_channel): """ Construct and return a new :class:`.LinkItem` """ item = items.LinkItem() item.setSourceItem(source_item) item.setSinkItem(sink_item) def channel_name(channel): if isinstance(channel, str): return channel else: return channel.name source_name = channel_name(source_channel) sink_name = channel_name(sink_channel) fmt = "<b>{0}</b> \u2192 <b>{1}</b>" item.setToolTip( fmt.format(escape(source_name), escape(sink_name)) ) item.setSourceName(source_name) item.setSinkName(sink_name) item.setChannelNamesVisible(self.__channel_names_visible) return item def remove_link_item(self, item): """ Remove a link (:class:`.LinkItem`) from the scene. """ # Invalidate the anchor layout. self.__anchor_layout.invalidateAnchorItem( item.sourceItem.outputAnchorItem ) self.__anchor_layout.invalidateAnchorItem( item.sinkItem.inputAnchorItem ) self.__link_items.remove(item) # Remove the anchor points. item.removeLink() self.removeItem(item) self.link_item_removed.emit(item) log.info("Removed link '%s' from '%s'" % (item, self)) return item def remove_link(self, scheme_link): """ Remove a :class:`.LinkItem` instance that was previously constructed for a :class:`SchemeLink` instance `link` using the `add_link` method. """ item = self.__item_for_link.pop(scheme_link) scheme_link.enabled_changed.disconnect(item.setEnabled) if scheme_link.is_dynamic(): scheme_link.dynamic_enabled_changed.disconnect( item.setDynamicEnabled ) self.remove_link_item(item) def link_items(self): """ Return all :class:`.LinkItem`\s in the scene. """ return list(self.__link_items) def add_annotation_item(self, annotation): """ Add an :class:`.Annotation` item to the scene. """ self.__annotation_items.append(annotation) self.addItem(annotation) self.annotation_added.emit(annotation) return annotation def add_annotation(self, scheme_annot): """ Create a new item for :class:`SchemeAnnotation` and add it to the scene. If the `scheme_annot` is already in the scene do nothing and just return its item. """ if scheme_annot in self.__item_for_annotation: # Already added return self.__item_for_annotation[scheme_annot] if isinstance(scheme_annot, scheme.SchemeTextAnnotation): item = items.TextAnnotation() item.setPlainText(scheme_annot.text) x, y, w, h = scheme_annot.rect item.setPos(x, y) item.resize(w, h) item.setTextInteractionFlags(Qt.TextEditorInteraction) font = font_from_dict(scheme_annot.font, item.font()) item.setFont(font) scheme_annot.text_changed.connect(item.setPlainText) elif isinstance(scheme_annot, scheme.SchemeArrowAnnotation): item = items.ArrowAnnotation() start, end = scheme_annot.start_pos, scheme_annot.end_pos item.setLine(QLineF(QPointF(*start), QPointF(*end))) item.setColor(QColor(scheme_annot.color)) scheme_annot.geometry_changed.connect( self.__on_scheme_annot_geometry_change ) self.add_annotation_item(item) self.__item_for_annotation[scheme_annot] = item return item def remove_annotation_item(self, annotation): """ Remove an :class:`.Annotation` instance from the scene. """ self.__annotation_items.remove(annotation) self.removeItem(annotation) self.annotation_removed.emit(annotation) def remove_annotation(self, scheme_annotation): """ Remove an :class:`.Annotation` instance that was previously added using :func:`add_anotation`. """ item = self.__item_for_annotation.pop(scheme_annotation) scheme_annotation.geometry_changed.disconnect( self.__on_scheme_annot_geometry_change ) if isinstance(scheme_annotation, scheme.SchemeTextAnnotation): scheme_annotation.text_changed.disconnect( item.setPlainText ) self.remove_annotation_item(item) def annotation_items(self): """ Return all :class:`.Annotation` items in the scene. """ return self.__annotation_items def item_for_annotation(self, scheme_annotation): return self.__item_for_annotation[scheme_annotation] def annotation_for_item(self, item): rev = dict(reversed(item) \ for item in self.__item_for_annotation.items()) return rev[item] def commit_scheme_node(self, node): """ Commit the `node` into the scheme. """ if not self.editable: raise Exception("Scheme not editable.") if node not in self.__item_for_node: raise ValueError("No 'NodeItem' for node.") item = self.__item_for_node[node] try: self.scheme.add_node(node) except Exception: log.error("An error occurred while committing node '%s'", node, exc_info=True) # Cleanup (remove the node item) self.remove_node_item(item) raise log.info("Commited node '%s' from '%s' to '%s'" % \ (node, self, self.scheme)) def commit_scheme_link(self, link): """ Commit a scheme link. """ if not self.editable: raise Exception("Scheme not editable") if link not in self.__item_for_link: raise ValueError("No 'LinkItem' for link.") self.scheme.add_link(link) log.info("Commited link '%s' from '%s' to '%s'" % \ (link, self, self.scheme)) def node_for_item(self, item): """ Return the `SchemeNode` for the `item`. """ rev = dict([(v, k) for k, v in self.__item_for_node.items()]) return rev[item] def item_for_node(self, node): """ Return the :class:`NodeItem` instance for a :class:`SchemeNode`. """ return self.__item_for_node[node] def link_for_item(self, item): """ Return the `SchemeLink for `item` (:class:`LinkItem`). """ rev = dict([(v, k) for k, v in self.__item_for_link.items()]) return rev[item] def item_for_link(self, link): """ Return the :class:`LinkItem` for a :class:`SchemeLink` """ return self.__item_for_link[link] def selected_node_items(self): """ Return the selected :class:`NodeItem`'s. """ return [item for item in self.__node_items if item.isSelected()] def selected_annotation_items(self): """ Return the selected :class:`Annotation`'s """ return [item for item in self.__annotation_items if item.isSelected()] def node_links(self, node_item): """ Return all links from the `node_item` (:class:`NodeItem`). """ return self.node_output_links(node_item) + \ self.node_input_links(node_item) def node_output_links(self, node_item): """ Return a list of all output links from `node_item`. """ return [link for link in self.__link_items if link.sourceItem == node_item] def node_input_links(self, node_item): """ Return a list of all input links for `node_item`. """ return [link for link in self.__link_items if link.sinkItem == node_item] def neighbor_nodes(self, node_item): """ Return a list of `node_item`'s (class:`NodeItem`) neighbor nodes. """ neighbors = list(map(attrgetter("sourceItem"), self.node_input_links(node_item))) neighbors.extend(map(attrgetter("sinkItem"), self.node_output_links(node_item))) return neighbors def on_widget_state_change(self, widget, state): pass def on_link_state_change(self, link, state): pass def on_scheme_change(self, ): pass def _on_position_change(self, item): # Invalidate the anchor point layout and schedule a layout. self.__anchor_layout.invalidateNode(item) self.node_item_position_changed.emit(item, item.pos()) def __on_node_pos_changed(self, pos): node = self.sender() item = self.__item_for_node[node] item.setPos(*pos) def __on_scheme_annot_geometry_change(self): annot = self.sender() item = self.__item_for_annotation[annot] if isinstance(annot, scheme.SchemeTextAnnotation): item.setGeometry(QRectF(*annot.rect)) elif isinstance(annot, scheme.SchemeArrowAnnotation): p1 = item.mapFromScene(QPointF(*annot.start_pos)) p2 = item.mapFromScene(QPointF(*annot.end_pos)) item.setLine(QLineF(p1, p2)) else: pass def item_at(self, pos, type_or_tuple=None, buttons=0): """Return the item at `pos` that is an instance of the specified type (`type_or_tuple`). If `buttons` (`Qt.MouseButtons`) is given only return the item if it is the top level item that would accept any of the buttons (`QGraphicsItem.acceptedMouseButtons`). """ rect = QRectF(pos, QSizeF(1, 1)) items = self.items(rect) if buttons: items = itertools.dropwhile( lambda item: not item.acceptedMouseButtons() & buttons, items ) items = list(items)[:1] if type_or_tuple: items = [i for i in items if isinstance(i, type_or_tuple)] return items[0] if items else None if PYQT_VERSION < 0x40900: # For QGraphicsObject subclasses items, itemAt ... return a # QGraphicsItem wrapper instance and not the actual class instance. def itemAt(self, *args, **kwargs): item = QGraphicsScene.itemAt(self, *args, **kwargs) return toGraphicsObjectIfPossible(item) def items(self, *args, **kwargs): items = QGraphicsScene.items(self, *args, **kwargs) return list(map(toGraphicsObjectIfPossible, items)) def selectedItems(self, *args, **kwargs): return list(map(toGraphicsObjectIfPossible, QGraphicsScene.selectedItems(self, *args, **kwargs))) def collidingItems(self, *args, **kwargs): return list(map(toGraphicsObjectIfPossible, QGraphicsScene.collidingItems(self, *args, **kwargs))) def focusItem(self, *args, **kwargs): item = QGraphicsScene.focusItem(self, *args, **kwargs) return toGraphicsObjectIfPossible(item) def mouseGrabberItem(self, *args, **kwargs): item = QGraphicsScene.mouseGrabberItem(self, *args, **kwargs) return toGraphicsObjectIfPossible(item) def mousePressEvent(self, event): if self.user_interaction_handler and \ self.user_interaction_handler.mousePressEvent(event): return # Right (context) click on the node item. If the widget is not # in the current selection then select the widget (only the widget). # Else simply return and let customContextMenuReqested signal # handle it shape_item = self.item_at(event.scenePos(), items.NodeItem) if shape_item and event.button() == Qt.RightButton and \ shape_item.flags() & QGraphicsItem.ItemIsSelectable: if not shape_item.isSelected(): self.clearSelection() shape_item.setSelected(True) return QGraphicsScene.mousePressEvent(self, event) def mouseMoveEvent(self, event): if self.user_interaction_handler and \ self.user_interaction_handler.mouseMoveEvent(event): return return QGraphicsScene.mouseMoveEvent(self, event) def mouseReleaseEvent(self, event): if self.user_interaction_handler and \ self.user_interaction_handler.mouseReleaseEvent(event): return return QGraphicsScene.mouseReleaseEvent(self, event) def mouseDoubleClickEvent(self, event): if self.user_interaction_handler and \ self.user_interaction_handler.mouseDoubleClickEvent(event): return return QGraphicsScene.mouseDoubleClickEvent(self, event) def keyPressEvent(self, event): if self.user_interaction_handler and \ self.user_interaction_handler.keyPressEvent(event): return return QGraphicsScene.keyPressEvent(self, event) def keyReleaseEvent(self, event): if self.user_interaction_handler and \ self.user_interaction_handler.keyReleaseEvent(event): return return QGraphicsScene.keyReleaseEvent(self, event) def set_user_interaction_handler(self, handler): if self.user_interaction_handler and \ not self.user_interaction_handler.isFinished(): self.user_interaction_handler.cancel() log.info("Setting interaction '%s' to '%s'" % (handler, self)) self.user_interaction_handler = handler if handler: handler.start() def event(self, event): # TODO: change the base class of Node/LinkItem to QGraphicsWidget. # It already handles font changes. if event.type() == QEvent.FontChange: self.__update_font() return QGraphicsScene.event(self, event) def __update_font(self): font = self.font() for item in self.__node_items + self.__link_items: item.setFont(font) def __str__(self): return "%s(objectName=%r, ...)" % \ (type(self).__name__, str(self.objectName()))
class TrackingWindow(QMainWindow): """ Main window of the application. This class is responsible for the global data structures too. :IVariables: undo_stack : `QUndoStack` Undo stack. All actions that can be undone should be pushed on the stack. _project : `project.Project` Project object managing the loaded data _data : `tracking_data.TrackingData` Data object keeping track of points and cells toolGroup : `QActionGroup` Group of actions to be enabled only when actions can be taken on images previousSelAct : `QActionGroup` Actions enabled when points are selected in the previous pane currentSelAct : `QActionGroup` Actions enabled when points are selected in the current pane projectAct : `QActionGroup` Actions to enable once a project is loaded _previousScene : `tracking_scene.TrackingScene` Object managing the previous pane _currentScene : `tracking_scene.LinkedTrackingScene` Object managing the current pane """ def __init__(self, *args, **kwords): QMainWindow.__init__(self, *args) self.undo_stack = QUndoStack(self) self.ui = Ui_TrackingWindow() self.ui.setupUi(self) self._project = None self._data = None self.toolGroup = QActionGroup(self) self.toolGroup.addAction(self.ui.actionAdd_point) self.toolGroup.addAction(self.ui.action_Move_point) self.toolGroup.addAction(self.ui.actionAdd_cell) self.toolGroup.addAction(self.ui.actionRemove_cell) self.toolGroup.addAction(self.ui.action_Pan) self.toolGroup.addAction(self.ui.actionZoom_out) self.toolGroup.addAction(self.ui.actionZoom_in) self.previousSelAct = QActionGroup(self) self.previousSelAct.addAction( self.ui.actionCopy_selection_from_Previous) self.previousSelAct.addAction(self.ui.actionDelete_Previous) self.previousSelAct.setEnabled(False) self.currentSelAct = QActionGroup(self) self.currentSelAct.addAction(self.ui.actionCopy_selection_from_Current) self.currentSelAct.addAction(self.ui.actionDelete_Current) self.currentSelAct.setEnabled(False) self.projectAct = QActionGroup(self) self.projectAct.addAction(self.ui.action_Next_image) self.projectAct.addAction(self.ui.action_Previous_image) self.projectAct.addAction(self.ui.actionAdd_point) self.projectAct.addAction(self.ui.action_Move_point) self.projectAct.addAction(self.ui.action_Pan) self.projectAct.addAction(self.ui.actionAdd_cell) self.projectAct.addAction(self.ui.actionRemove_cell) self.projectAct.addAction(self.ui.action_Change_data_file) self.projectAct.addAction(self.ui.actionNew_data_file) self.projectAct.addAction(self.ui.actionZoom_out) self.projectAct.addAction(self.ui.actionZoom_in) self.projectAct.addAction(self.ui.actionSave_as) self.projectAct.addAction(self.ui.action_Fit) self.projectAct.addAction(self.ui.actionZoom_100) self.projectAct.addAction(self.ui.actionMerge_points) self.projectAct.addAction(self.ui.actionCopy_from_previous) self.projectAct.addAction(self.ui.actionCopy_from_current) self.projectAct.addAction(self.ui.actionReset_alignment) self.projectAct.addAction(self.ui.actionAlign_images) self.projectAct.addAction(self.ui.actionSelectPreviousAll) self.projectAct.addAction(self.ui.actionSelectPreviousNew) self.projectAct.addAction(self.ui.actionSelectPreviousNone) self.projectAct.addAction(self.ui.actionSelectPreviousNon_associated) self.projectAct.addAction(self.ui.actionSelectPreviousAssociated) self.projectAct.addAction(self.ui.actionSelectPreviousInvert) self.projectAct.addAction(self.ui.actionSelectCurrentAll) self.projectAct.addAction(self.ui.actionSelectCurrentNew) self.projectAct.addAction(self.ui.actionSelectCurrentNone) self.projectAct.addAction(self.ui.actionSelectCurrentNon_associated) self.projectAct.addAction(self.ui.actionSelectCurrentAssociated) self.projectAct.addAction(self.ui.actionSelectCurrentInvert) self.projectAct.addAction(self.ui.actionEdit_timing) self.projectAct.addAction(self.ui.actionEdit_scales) self.projectAct.addAction(self.ui.actionCompute_growth) self.projectAct.addAction(self.ui.actionClean_cells) self.projectAct.addAction(self.ui.actionGotoCell) self.projectAct.setEnabled(False) current_sel_actions = [ self.ui.actionSelectCurrentAll, self.ui.actionSelectCurrentNew, self.ui.actionSelectCurrentNone, self.ui.actionSelectCurrentInvert, '-', self.ui.actionSelectCurrentNon_associated, self.ui.actionSelectCurrentAssociated, self.ui.actionCopy_selection_from_Previous ] previous_sel_actions = [ self.ui.actionSelectPreviousAll, self.ui.actionSelectPreviousNew, self.ui.actionSelectPreviousNone, self.ui.actionSelectPreviousInvert, '-', self.ui.actionSelectPreviousNon_associated, self.ui.actionSelectPreviousAssociated, self.ui.actionCopy_selection_from_Current ] self._previousScene = TrackingScene(self.undo_stack, self.ui.actionDelete_Previous, previous_sel_actions, self) self._currentScene = LinkedTrackingScene(self._previousScene, self.undo_stack, self.ui.actionDelete_Current, current_sel_actions, self) self._previousScene.hasSelectionChanged.connect( self.previousSelAct.setEnabled) self._currentScene.hasSelectionChanged.connect( self.currentSelAct.setEnabled) self._previousScene.realSceneSizeChanged.connect(self.sceneSizeChanged) self._currentScene.realSceneSizeChanged.connect(self.sceneSizeChanged) self._previousScene.zoomIn[QPointF].connect(self.zoomIn) self._currentScene.zoomIn.connect(self.zoomIn) self._previousScene.zoomOut[QPointF].connect(self.zoomOut) self._currentScene.zoomOut.connect(self.zoomOut) self.ui.previousData.setScene(self._previousScene) self.ui.currentData.setScene(self._currentScene) self.ui.previousData.setDragMode(QGraphicsView.ScrollHandDrag) self.ui.currentData.setDragMode(QGraphicsView.ScrollHandDrag) #self.ui.previousData.setCacheMode(QGraphicsView.CacheBackground) #self.ui.currentData.setCacheMode(QGraphicsView.CacheBackground) # Redefine shortcuts to standard key sequences self.ui.action_Save.setShortcut(QKeySequence.Save) self.ui.actionSave_as.setShortcut(QKeySequence.SaveAs) self.ui.action_Open_project.setShortcut(QKeySequence.Open) self.ui.action_Undo.setShortcut(QKeySequence.Undo) self.ui.action_Redo.setShortcut(QKeySequence.Redo) self.ui.action_Next_image.setShortcut(QKeySequence.Forward) self.ui.action_Previous_image.setShortcut(QKeySequence.Back) # Connecting undo stack signals self.ui.action_Undo.triggered.connect(self.undo) self.ui.action_Redo.triggered.connect(self.redo) self.undo_stack.canRedoChanged[bool].connect( self.ui.action_Redo.setEnabled) self.undo_stack.canUndoChanged[bool].connect( self.ui.action_Undo.setEnabled) self.undo_stack.redoTextChanged["const QString&"].connect( self.changeRedoText) self.undo_stack.undoTextChanged["const QString&"].connect( self.changeUndoText) self.undo_stack.cleanChanged[bool].connect( self.ui.action_Save.setDisabled) # link_icon = QIcon() # pix = QPixmap(":/icons/link.png") # link_icon.addPixmap(pix, QIcon.Normal, QIcon.On) # pix = QPixmap(":/icons/link_broken.png") # link_icon.addPixmap(pix, QIcon.Normal, QIcon.Off) # self.link_icon = link_icon # #self.ui.linkViews.setIconSize(QSize(64,32)) # self.ui.linkViews.setIcon(link_icon) self._recent_projects_menu = QMenu(self) self.ui.actionRecent_projects.setMenu(self._recent_projects_menu) self._recent_projects_act = [] self._projects_mapper = QSignalMapper(self) self._projects_mapper.mapped[int].connect(self.loadRecentProject) self.param_dlg = None # Setting up the status bar bar = self.statusBar() # Adding current directory cur_dir = QLabel("") bar.addPermanentWidget(cur_dir) self._current_dir_label = cur_dir # Adding up zoom zoom = QLabel("") bar.addPermanentWidget(zoom) self._zoom_label = zoom self.changeZoom(1) self.loadConfig() parameters.instance.renderingChanged.connect(self.changeRendering) self.changeRendering() def changeRendering(self): if parameters.instance.use_OpenGL: self.ui.previousData.setViewport( QGLWidget(QGLFormat(QGL.SampleBuffers))) self.ui.currentData.setViewport( QGLWidget(QGLFormat(QGL.SampleBuffers))) else: self.ui.previousData.setViewport(QWidget()) self.ui.currentData.setViewport(QWidget()) def undo(self): self.undo_stack.undo() def redo(self): self.undo_stack.redo() def changeRedoText(self, text): self.ui.action_Redo.setText(text) self.ui.action_Redo.setToolTip(text) self.ui.action_Redo.setStatusTip(text) def changeUndoText(self, text): self.ui.action_Undo.setText(text) self.ui.action_Undo.setToolTip(text) self.ui.action_Undo.setStatusTip(text) def closeEvent(self, event): self.saveConfig() if not self.ensure_save_data( "Exiting whith unsaved data", "The last modifications you made were not saved." " Are you sure you want to exit?"): event.ignore() return QMainWindow.closeEvent(self, event) #sys.exit(0) def loadConfig(self): params = parameters.instance self.ui.action_Show_vector.setChecked(params.show_vectors) self.ui.linkViews.setChecked(params.link_views) self.ui.action_Show_template.setChecked( parameters.instance.show_template) self.ui.actionShow_id.setChecked(parameters.instance.show_id) self.ui.action_Estimate_position.setChecked( parameters.instance.estimate) self.updateRecentFiles() params.recentProjectsChange.connect(self.updateRecentFiles) def updateRecentFiles(self): for a in self._recent_projects_act: self._projects_mapper.removeMappings(a) del self._recent_projects_act[:] menu = self._recent_projects_menu menu.clear() recent_projects = parameters.instance.recent_projects for i, p in enumerate(recent_projects): act = QAction(self) act.setText("&{0:d} {1}".format(i + 1, p)) self._recent_projects_act.append(act) act.triggered.connect(self._projects_mapper.map) self._projects_mapper.setMapping(act, i) menu.addAction(act) def saveConfig(self): parameters.instance.save() def check_for_data(self): if self._project is None: QMessageBox.critical( self, "No project loaded", "You have to load a project before performing this operation") return False return True def loadRecentProject(self, i): if self.ensure_save_data( "Leaving unsaved data", "The last modifications you made were not saved." " Are you sure you want to change project?"): self.loadProject(parameters.instance.recent_projects[i]) @pyqtSignature("") def on_action_Open_project_triggered(self): if self.ensure_save_data( "Leaving unsaved data", "The last modifications you made were not saved." " Are you sure you want to change project?"): dir_ = QFileDialog.getExistingDirectory( self, "Select a project directory", parameters.instance._last_dir) if dir_: self.loadProject(dir_) def loadProject(self, dir_): dir_ = path(dir_) project = Project(dir_) if project.valid: self._project = project else: create = QMessageBox.question( self, "Invalid project directory", "This directory does not contain a valid project. Turn into a directory?", QMessageBox.No, QMessageBox.Yes) if create == QMessageBox.No: return project.create() self._project = project self._project.use() parameters.instance.add_recent_project(dir_) parameters.instance._last_dir = dir_ if self._data is not None: _data = self._data _data.saved.disconnect(self.undo_stack.setClean) try: #self._project.load() self.load_data() _data = self._project.data _data.saved.connect(self.undo_stack.setClean) self._project.changedDataFile.connect(self.dataFileChanged) self._data = _data self._previousScene.changeDataManager(self._data) self._currentScene.changeDataManager(self._data) self.initFromData() self.projectAct.setEnabled(True) except TrackingDataException as ex: showException(self, "Error while loaded data", ex) def dataFileChanged(self, new_file): if new_file is None: self._current_dir_label.setText("") else: self._current_dir_label.setText(new_file) def initFromData(self): """ Initialize the interface using the current data """ self.ui.previousState.clear() self.ui.currentState.clear() for name in self._data.images_name: self.ui.previousState.addItem(name) self.ui.currentState.addItem(name) self.ui.previousState.setCurrentIndex(0) self.ui.currentState.setCurrentIndex(1) self._previousScene.changeImage( self._data.image_path(self._data.images_name[0])) self._currentScene.changeImage( self._data.image_path(self._data.images_name[1])) self.dataFileChanged(self._project.data_file) @pyqtSignature("int") def on_previousState_currentIndexChanged(self, index): #print "Previous image loaded: %s" % self._data.images[index] self.changeScene(self._previousScene, index) self._currentScene.changeImage(None) @pyqtSignature("int") def on_currentState_currentIndexChanged(self, index): #print "Current image loaded: %s" % self._data.images[index] self.changeScene(self._currentScene, index) def changeScene(self, scene, index): """ Set the scene to use the image number index. """ scene.changeImage(self._data.image_path(self._data.images_name[index])) @pyqtSignature("") def on_action_Save_triggered(self): self.save_data() @pyqtSignature("") def on_actionSave_as_triggered(self): fn = QFileDialog.getSaveFileName(self, "Select a data file to save in", self._project.data_dir, "CSV Files (*.csv);;All files (*.*)") if fn: self.save_data(path(fn)) def save_data(self, data_file=None): if self._data is None: raise TrackingDataException( "Trying to save data when none have been loaded") try: self._project.save(data_file) return True except TrackingDataException as ex: showException(self, "Error while saving data", ex) return False def load_data(self, **opts): if self._project is None: raise TrackingDataException( "Trying to load data when no project have been loaded") try: if self._project.load(**opts): log_debug("Data file was corrected. Need saving.") self.ui.action_Save.setEnabled(True) else: log_debug("Data file is clean.") self.ui.action_Save.setEnabled(False) return True except TrackingDataException as ex: showException(self, "Error while loading data", ex) return False except RetryTrackingDataException as ex: if retryException(self, "Problem while loading data", ex): new_opts = dict(opts) new_opts.update(ex.method_args) return self.load_data(**new_opts) return False def ensure_save_data(self, title, reason): if self._data is not None and not self.undo_stack.isClean(): button = QMessageBox.warning( self, title, reason, QMessageBox.Yes | QMessageBox.Save | QMessageBox.Cancel) if button == QMessageBox.Save: return self.save_data() elif button == QMessageBox.Cancel: return False self.undo_stack.clear() return True @pyqtSignature("") def on_action_Change_data_file_triggered(self): if self.ensure_save_data( "Leaving unsaved data", "The last modifications you made were not saved." " Are you sure you want to change the current data file?"): fn = QFileDialog.getOpenFileName( self, "Select a data file to load", self._project.data_dir, "CSV Files (*.csv);;All files (*.*)") if fn: self._project.data_file = str(fn) if self.load_data(): self._previousScene.resetNewPoints() self._currentScene.resetNewPoints() @pyqtSignature("bool") def on_action_Show_vector_toggled(self, value): parameters.instance.show_vector = value self._currentScene.showVector(value) @pyqtSignature("bool") def on_action_Show_template_toggled(self, value): parameters.instance.show_template = value @pyqtSignature("bool") def on_actionShow_id_toggled(self, value): parameters.instance.show_id = value @pyqtSignature("") def on_action_Next_image_triggered(self): cur = self.ui.currentState.currentIndex() pre = self.ui.previousState.currentIndex() l = len(self._data.images_name) if cur < l - 1 and pre < l - 1: self.ui.previousState.setCurrentIndex(pre + 1) self.ui.currentState.setCurrentIndex(cur + 1) @pyqtSignature("") def on_action_Previous_image_triggered(self): cur = self.ui.currentState.currentIndex() pre = self.ui.previousState.currentIndex() if cur > 0 and pre > 0: self.ui.previousState.setCurrentIndex(pre - 1) self.ui.currentState.setCurrentIndex(cur - 1) @pyqtSignature("") def on_copyToPrevious_clicked(self): self._currentScene.copyFromLinked(self._previousScene) @pyqtSignature("") def on_copyToCurrent_clicked(self): self._previousScene.copyToLinked(self._currentScene) @pyqtSignature("bool") def on_action_Estimate_position_toggled(self, value): parameters.instance.estimate = value # @pyqtSignature("") # def on_action_Undo_triggered(self): # print "Undo" # @pyqtSignature("") # def on_action_Redo_triggered(self): # print "Redo" @pyqtSignature("bool") def on_action_Parameters_toggled(self, value): if value: from .parametersdlg import ParametersDlg self._previousScene.showTemplates() self._currentScene.showTemplates() #tracking_scene.saveParameters() parameters.instance.save() max_size = max(self._currentScene.width(), self._currentScene.height(), self._previousScene.width(), self._previousScene.height(), 400) self.param_dlg = ParametersDlg(max_size, self) self.param_dlg.setModal(False) self.ui.action_Pan.setChecked(True) self.ui.actionAdd_point.setEnabled(False) self.ui.action_Move_point.setEnabled(False) self.ui.actionAdd_cell.setEnabled(False) self.ui.actionRemove_cell.setEnabled(False) self.ui.action_Undo.setEnabled(False) self.ui.action_Redo.setEnabled(False) self.ui.action_Open_project.setEnabled(False) self.ui.actionRecent_projects.setEnabled(False) self.ui.action_Change_data_file.setEnabled(False) self.ui.copyToCurrent.setEnabled(False) self.ui.copyToPrevious.setEnabled(False) self.param_dlg.finished[int].connect(self.closeParam) self.param_dlg.show() elif self.param_dlg: self.param_dlg.accept() def closeParam(self, value): if value == QDialog.Rejected: parameters.instance.load() self.ui.actionAdd_point.setEnabled(True) self.ui.action_Move_point.setEnabled(True) self.ui.actionAdd_cell.setEnabled(True) self.ui.actionRemove_cell.setEnabled(True) self.ui.action_Undo.setEnabled(True) self.ui.action_Redo.setEnabled(True) self.ui.action_Open_project.setEnabled(True) self.ui.actionRecent_projects.setEnabled(True) self.ui.action_Change_data_file.setEnabled(True) self.ui.copyToCurrent.setEnabled(True) self.ui.copyToPrevious.setEnabled(True) self._previousScene.showTemplates(False) self._currentScene.showTemplates(False) self._previousScene.update() self._currentScene.update() self.param_dlg = None self.ui.action_Parameters.setChecked(False) @pyqtSignature("bool") def on_actionZoom_in_toggled(self, value): if value: self._previousScene.mode = TrackingScene.ZoomIn self._currentScene.mode = TrackingScene.ZoomIn @pyqtSignature("bool") def on_actionZoom_out_toggled(self, value): if value: self._previousScene.mode = TrackingScene.ZoomOut self._currentScene.mode = TrackingScene.ZoomOut #def resizeEvent(self, event): # self.ensureZoomFit() def ensureZoomFit(self): if self._data: prev_rect = self._previousScene.sceneRect() cur_rect = self._currentScene.sceneRect() prev_wnd = QRectF(self.ui.previousData.childrenRect()) cur_wnd = QRectF(self.ui.currentData.childrenRect()) prev_matrix = self.ui.previousData.matrix() cur_matrix = self.ui.currentData.matrix() prev_mapped_rect = prev_matrix.mapRect(prev_rect) cur_mapped_rect = cur_matrix.mapRect(cur_rect) if (prev_mapped_rect.width() < prev_wnd.width() or prev_mapped_rect.height() < prev_wnd.height() or cur_mapped_rect.width() < cur_wnd.width() or cur_mapped_rect.height() < cur_wnd.height()): self.on_action_Fit_triggered() @pyqtSignature("") def on_action_Fit_triggered(self): prev_rect = self._previousScene.sceneRect() cur_rect = self._currentScene.sceneRect() prev_wnd = self.ui.previousData.childrenRect() cur_wnd = self.ui.currentData.childrenRect() prev_sw = prev_wnd.width() / prev_rect.width() prev_sh = prev_wnd.height() / prev_rect.height() cur_sw = cur_wnd.width() / cur_rect.width() cur_sh = cur_wnd.height() / cur_rect.height() s = max(prev_sw, prev_sh, cur_sw, cur_sh) self.ui.previousData.resetMatrix() self.ui.previousData.scale(s, s) self.ui.currentData.resetMatrix() self.ui.currentData.scale(s, s) self.changeZoom(s) def zoomOut(self, point=None): self.ui.currentData.scale(0.5, 0.5) self.ui.previousData.scale(0.5, 0.5) self.changeZoom(self.ui.previousData.matrix().m11()) if point is not None: self.ui.previousData.centerOn(point) self.ui.currentData.centerOn(point) #self.ensureZoomFit() def zoomIn(self, point=None): self.ui.currentData.scale(2, 2) self.ui.previousData.scale(2, 2) self.changeZoom(self.ui.previousData.matrix().m11()) if point is not None: self.ui.previousData.centerOn(point) self.ui.currentData.centerOn(point) def changeZoom(self, zoom): self._zoom_label.setText("Zoom: %.5g%%" % (100 * zoom)) @pyqtSignature("") def on_actionZoom_100_triggered(self): self.ui.previousData.resetMatrix() self.ui.currentData.resetMatrix() self.changeZoom(1) @pyqtSignature("bool") def on_actionAdd_point_toggled(self, value): if value: self._previousScene.mode = TrackingScene.Add self._currentScene.mode = TrackingScene.Add @pyqtSignature("bool") def on_actionAdd_cell_toggled(self, value): if value: self._previousScene.mode = TrackingScene.AddCell self._currentScene.mode = TrackingScene.AddCell @pyqtSignature("bool") def on_actionRemove_cell_toggled(self, value): if value: self._previousScene.mode = TrackingScene.RemoveCell self._currentScene.mode = TrackingScene.RemoveCell @pyqtSignature("bool") def on_action_Move_point_toggled(self, value): if value: self._previousScene.mode = TrackingScene.Move self._currentScene.mode = TrackingScene.Move @pyqtSignature("bool") def on_action_Pan_toggled(self, value): if value: self._previousScene.mode = TrackingScene.Pan self._currentScene.mode = TrackingScene.Pan @pyqtSignature("bool") def on_linkViews_toggled(self, value): parameters.instance.link_views = value phor = self.ui.previousData.horizontalScrollBar() pver = self.ui.previousData.verticalScrollBar() chor = self.ui.currentData.horizontalScrollBar() cver = self.ui.currentData.verticalScrollBar() if value: phor.valueChanged[int].connect(chor.setValue) pver.valueChanged[int].connect(cver.setValue) chor.valueChanged[int].connect(phor.setValue) cver.valueChanged[int].connect(pver.setValue) self._previousScene.templatePosChange.connect( self._currentScene.setTemplatePos) self._currentScene.templatePosChange.connect( self._previousScene.setTemplatePos) phor.setValue(chor.value()) pver.setValue(cver.value()) else: phor.valueChanged[int].disconnect(chor.setValue) pver.valueChanged[int].disconnect(cver.setValue) chor.valueChanged[int].disconnect(phor.setValue) cver.valueChanged[int].disconnect(pver.setValue) self._previousScene.templatePosChange.disconnect( self._currentScene.setTemplatePos) self._currentScene.templatePosChange.disconnect( self._previousScene.setTemplatePos) def copyFrom(self, start, items): if parameters.instance.estimate: dlg = createForm('copy_progress.ui', None) dlg.buttonBox.clicked["QAbstractButton*"].connect(self.cancelCopy) params = parameters.instance ts = params.template_size ss = params.search_size fs = params.filter_size self.copy_thread = algo.FindInAll(self._data, start, items, ts, ss, fs, self) dlg.imageProgress.setMaximum(self.copy_thread.num_images) self.copy_thread.start() self.copy_dlg = dlg dlg.exec_() else: algo.copyFromImage(self._data, start, items, self.undo_stack) def cancelCopy(self, *args): self.copy_thread.stop = True dlg = self.copy_dlg dlg.buttonBox.clicked['QAbstractButton*)'].disconnect(self.cancelCopy) self._previousScene.changeImage(None) self._currentScene.changeImage(None) def event(self, event): if isinstance(event, algo.NextImage): dlg = self.copy_dlg if dlg is not None: dlg.imageProgress.setValue(event.currentImage) dlg.pointProgress.setMaximum(event.nbPoints) dlg.pointProgress.setValue(0) return True elif isinstance(event, algo.NextPoint): dlg = self.copy_dlg if dlg is not None: dlg.pointProgress.setValue(event.currentPoint) return True elif isinstance(event, algo.FoundAll): dlg = self.copy_dlg if dlg is not None: self.cancelCopy() dlg.accept() return True elif isinstance(event, algo.Aborted): dlg = self.copy_dlg if dlg is not None: self.cancelCopy() dlg.accept() return True return QMainWindow.event(self, event) def itemsToCopy(self, scene): items = scene.getSelectedIds() if items: answer = QMessageBox.question( self, "Copy of points", "Some points were selected in the previous data window." " Do you want to copy only these point on the successive images?", QMessageBox.Yes, QMessageBox.No) if answer == QMessageBox.Yes: return items return scene.getAllIds() @pyqtSignature("") def on_actionCopy_from_previous_triggered(self): items = self.itemsToCopy(self._previousScene) if items: self.copyFrom(self.ui.previousState.currentIndex(), items) @pyqtSignature("") def on_actionCopy_from_current_triggered(self): items = self.itemsToCopy(self._currentScene) if items: self.copyFrom(self.ui.currentState.currentIndex(), items) @pyqtSignature("") def on_actionSelectPreviousAll_triggered(self): self._previousScene.selectAll() @pyqtSignature("") def on_actionSelectPreviousNew_triggered(self): self._previousScene.selectNew() @pyqtSignature("") def on_actionSelectPreviousNone_triggered(self): self._previousScene.selectNone() @pyqtSignature("") def on_actionSelectPreviousNon_associated_triggered(self): self._previousScene.selectNonAssociated() @pyqtSignature("") def on_actionSelectPreviousAssociated_triggered(self): self._previousScene.selectAssociated() @pyqtSignature("") def on_actionSelectPreviousInvert_triggered(self): self._previousScene.selectInvert() @pyqtSignature("") def on_actionSelectCurrentAll_triggered(self): self._currentScene.selectAll() @pyqtSignature("") def on_actionSelectCurrentNew_triggered(self): self._currentScene.selectNew() @pyqtSignature("") def on_actionSelectCurrentNone_triggered(self): self._currentScene.selectNone() @pyqtSignature("") def on_actionSelectCurrentNon_associated_triggered(self): self._currentScene.selectNonAssociated() @pyqtSignature("") def on_actionSelectCurrentAssociated_triggered(self): self._currentScene.selectAssociated() @pyqtSignature("") def on_actionSelectCurrentInvert_triggered(self): self._currentScene.selectInvert() def whichDelete(self): """ Returns a function deleting what the user wants """ dlg = createForm("deletedlg.ui", None) ret = dlg.exec_() if ret: if dlg.inAllImages.isChecked(): return TrackingScene.deleteInAllImages if dlg.toImage.isChecked(): return TrackingScene.deleteToImage if dlg.fromImage.isChecked(): return TrackingScene.deleteFromImage return lambda x: None @pyqtSignature("") def on_actionDelete_Previous_triggered(self): del_fct = self.whichDelete() del_fct(self._previousScene) @pyqtSignature("") def on_actionDelete_Current_triggered(self): del_fct = self.whichDelete() del_fct(self._currentScene) @pyqtSignature("") def on_actionMerge_points_triggered(self): if self._previousScene.mode == TrackingScene.AddCell: old_cell = self._previousScene.selected_cell new_cell = self._currentScene.selected_cell if old_cell is None or new_cell is None: QMessageBox.critical( self, "Cannot merge cells", "You have to select exactly one cell in the old state " "and one in the new state to merge them.") return try: if old_cell != new_cell: self.undo_stack.push( MergeCells(self._data, self._previousScene.image_name, old_cell, new_cell)) else: self.undo_stack.push( SplitCells(self._data, self._previousScene.image_name, old_cell, new_cell)) except AssertionError as error: QMessageBox.critical(self, "Cannot merge the cells", str(error)) else: old_pts = self._previousScene.getSelectedIds() new_pts = self._currentScene.getSelectedIds() if len(old_pts) != 1 or len(new_pts) != 1: QMessageBox.critical( self, "Cannot merge points", "You have to select exactly one point in the old state " "and one in the new state to link them.") return try: if old_pts != new_pts: self.undo_stack.push( ChangePointsId(self._data, self._previousScene.image_name, old_pts, new_pts)) else: log_debug("Splitting point of id %d" % old_pts[0]) self.undo_stack.push( SplitPointsId(self._data, self._previousScene.image_name, old_pts)) except AssertionError as error: QMessageBox.critical(self, "Cannot merge the points", str(error)) @pyqtSignature("") def on_actionCopy_selection_from_Current_triggered(self): cur_sel = self._currentScene.getSelectedIds() self._previousScene.setSelectedIds(cur_sel) @pyqtSignature("") def on_actionCopy_selection_from_Previous_triggered(self): cur_sel = self._previousScene.getSelectedIds() self._currentScene.setSelectedIds(cur_sel) @pyqtSignature("") def on_actionNew_data_file_triggered(self): if self.ensure_save_data( "Leaving unsaved data", "The last modifications you made were not saved." " Are you sure you want to change the current data file?"): fn = QFileDialog.getSaveFileName( self, "Select a new data file to create", self._project.data_dir, "CSV Files (*.csv);;All files (*.*)") if fn: fn = path(fn) if fn.exists(): button = QMessageBox.question( self, "Erasing existing file", "Are you sure yo want to empty the file '%s' ?" % fn, QMessageBox.Yes, QMessageBox.No) if button == QMessageBox.No: return fn.remove() self._data.clear() self._previousScene.resetNewPoints() self._currentScene.resetNewPoints() self._project.data_file = fn self.initFromData() log_debug("Data file = %s" % (self._project.data_file, )) @pyqtSignature("") def on_actionAbout_triggered(self): dlg = QMessageBox(self) dlg.setWindowTitle("About Point Tracker") dlg.setIconPixmap(self.windowIcon().pixmap(64, 64)) #dlg.setTextFormat(Qt.RichText) dlg.setText("""Point Tracker Tool version %s rev %s Developper: Pierre Barbier de Reuille <*****@*****.**> Copyright 2008 """ % (__version__, __revision__)) img_read = ", ".join( str(s) for s in QImageReader.supportedImageFormats()) img_write = ", ".join( str(s) for s in QImageWriter.supportedImageFormats()) dlg.setDetailedText("""Supported image formats: - For reading: %s - For writing: %s """ % (img_read, img_write)) dlg.exec_() @pyqtSignature("") def on_actionAbout_Qt_triggered(self): QMessageBox.aboutQt(self, "About Qt") @pyqtSignature("") def on_actionReset_alignment_triggered(self): self.undo_stack.push(ResetAlignment(self._data)) @pyqtSignature("") def on_actionAlign_images_triggered(self): fn = QFileDialog.getOpenFileName(self, "Select a data file for alignment", self._project.data_dir, "CSV Files (*.csv);;All files (*.*)") if fn: d = self._data.copy() fn = path(fn) try: d.load(fn) except TrackingDataException as ex: showException(self, "Error while loading data file", ex) return if d._last_pt_id > 0: dlg = AlignmentDlg(d._last_pt_id + 1, self) if dlg.exec_(): ref = dlg.ui.referencePoint.currentText() try: ref = int(ref) except ValueError: ref = str(ref) if dlg.ui.twoPointsRotation.isChecked(): r1 = int(dlg.ui.rotationPt1.currentText()) r2 = int(dlg.ui.rotationPt2.currentText()) rotation = ("TwoPoint", r1, r2) else: rotation = None else: return else: ref = 0 rotation = None try: shifts, angles = algo.alignImages(self._data, d, ref, rotation) self.undo_stack.push(AlignImages(self._data, shifts, angles)) except algo.AlgoException as ex: showException(self, "Error while aligning images", ex) def sceneSizeChanged(self): previous_rect = self._previousScene.real_scene_rect current_rect = self._currentScene.real_scene_rect rect = previous_rect | current_rect self._previousScene.setSceneRect(rect) self._currentScene.setSceneRect(rect) @pyqtSignature("") def on_actionEdit_timing_triggered(self): data = self._data dlg = TimeEditDlg(data.images_name, data.images_time, [data.image_path(n) for n in data.images_name], self) self.current_dlg = dlg if dlg.exec_() == QDialog.Accepted: self.undo_stack.push(ChangeTiming(data, [t for n, t in dlg.model])) del self.current_dlg @pyqtSignature("") def on_actionEdit_scales_triggered(self): data = self._data dlg = EditResDlg(data.images_name, data.images_scale, [data.image_path(n) for n in data.images_name], self) self.current_dlg = dlg if dlg.exec_() == QDialog.Accepted: self.undo_stack.push( ChangeScales(data, [sc for n, sc in dlg.model])) del self.current_dlg @pyqtSignature("") def on_actionCompute_growth_triggered(self): data = self._data dlg = GrowthComputationDlg(data, self) self.current_dlg = dlg dlg.exec_() del self.current_dlg @pyqtSignature("") def on_actionPlot_growth_triggered(self): data = self._data dlg = PlottingDlg(data, self) self.current_dlg = dlg dlg.exec_() del self.current_dlg @pyqtSignature("") def on_actionClean_cells_triggered(self): self.undo_stack.push(CleanCells(self._data)) @pyqtSignature("") def on_actionGotoCell_triggered(self): cells = [str(cid) for cid in self._data.cells] selected, ok = QInputDialog.getItem(self, "Goto cell", "Select the cell to go to", cells, 0) if ok: cid = int(selected) self.ui.actionAdd_cell.setChecked(True) data = self._data if cid not in data.cells: return ls = data.cells_lifespan[cid] prev_pos = self._previousScene.current_data._current_index cur_pos = self._currentScene.current_data._current_index full_poly = data.cells[cid] poly = [pid for pid in full_poly if pid in data[prev_pos]] #log_debug("Cell %d on time %d: %s" % (cid, prev_pos, poly)) if prev_pos < ls.start or prev_pos >= ls.end or not poly: for i in range(*ls.slice().indices(len(data))): poly = [pid for pid in full_poly if pid in data[i]] if poly: log_debug("Found cell %d on image %d with polygon %s" % (cid, i, poly)) new_prev_pos = i break else: log_debug("Cell %d found nowhere in range %s!!!" % (cid, ls.slice())) else: new_prev_pos = prev_pos new_cur_pos = min(max(cur_pos + new_prev_pos - prev_pos, 0), len(data)) self.ui.previousState.setCurrentIndex(new_prev_pos) self.ui.currentState.setCurrentIndex(new_cur_pos) self._previousScene.current_cell = cid self._currentScene.current_cell = cid prev_data = self._previousScene.current_data poly = data.cells[cid] prev_poly = QPolygonF( [prev_data[ptid] for ptid in poly if ptid in prev_data]) prev_bbox = prev_poly.boundingRect() log_debug("Previous bounding box = %dx%d+%d+%d" % (prev_bbox.width(), prev_bbox.height(), prev_bbox.left(), prev_bbox.top())) self.ui.previousData.ensureVisible(prev_bbox) @pyqtSignature("") def on_actionGotoPoint_triggered(self): data = self._data points = [str(pid) for pid in data.cell_points] selected, ok = QInputDialog.getItem(self, "Goto point", "Select the point to go to", points, 0) if ok: pid = int(selected) self.ui.action_Move_point.setChecked(True) if pid not in data.cell_points: return prev_pos = self._previousScene.current_data._current_index cur_pos = self._currentScene.current_data._current_index prev_data = self._previousScene.current_data if not pid in prev_data: closest = -1 best_dist = len(data) + 1 for img_data in data: if pid in img_data: dist = abs(img_data._current_index - prev_pos) if dist < best_dist: best_dist = dist closest = img_data._current_index new_prev_pos = closest else: new_prev_pos = prev_pos new_cur_pos = min(max(cur_pos + new_prev_pos - prev_pos, 0), len(data)) self.ui.previousState.setCurrentIndex(new_prev_pos) self.ui.currentState.setCurrentIndex(new_cur_pos) self._previousScene.setSelectedIds([pid]) self._currentScene.setSelectedIds([pid]) self.ui.previousData.centerOn( self._previousScene.current_data[pid])