Пример #1
0
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])
Пример #2
0
class Table(QAbstractTableModel):

    def __init__(self, ob, parent):
        QAbstractTableModel.__init__(self)
        # store things
        self.ob = ob
        self.stack = QUndoStack()
        self._parent = parent
        # Make sure we have Name information first
        # _header contains the row headers
        self._header = [
            QVariant('X'), QVariant('#'), QVariant(getattr(self.ob, 'UniqueName', 'name'))]
        # _tooltips contains the tooltips
        self._tooltips = [
            QVariant('An X indicates row is disabled %s'%bool),
            QVariant('Comment for row'),
            QVariant('Object name %s'%str)]
        # _required is a list of required columns
        self._required = []
        # _defaults is a dict of column -> default values
        self._defaults = {0: QVariant(False), 1: QVariant("")}
        # _optional is a list of optional columns
        self._optional = [2]
        # _cItems is a dict of column -> QVariant QStringList items, returned
        # to combo box
        self._cItems = {}
        # _cValues is a dict of column -> list of QVariant values, stored when
        # corresponding label stored by combobox
        self._cValues = {}
        # _idents is a list of identifier lookup fields
        self._idents = []
        # _types is a list of types for validation
        self._types = [bool, str, str]
        # rows is a list of rows. each row is a list of QVariants
        self.rows = []
        # work out the header and descriptions from the ArgInfo object
        a = ob.ArgInfo
        # for required names just process the ArgType object
        for name in a.required_names:
            self.__processArgType(name, a.descriptions[name])
        # for defaulted names give it a default too
        for name, default in zip(a.default_names, a.default_values):
            self.__processArgType(name, a.descriptions[name], default = default)
        # for optional names flag it as optional
        for name in a.optional_names:
            self.__processArgType(name, a.descriptions[name], optional = True)
        # maps (filt, without, upto) -> (timestamp, stringList)
        self._cachedNameList = {}
        # this is the top left item visible in the TableView widget
        self.topLeftIndex = None

    def __processArgType(self, name, ob, **args):
        # this is the column index
        col = len(self._header)
        # If it's a name then be careful not to add it twice
        if name == getattr(self.ob, 'UniqueName', 'name'):
            assert ob.typ == str, 'Object name must be a string'
            self._tooltips[2] = QVariant(ob.desc)
        else:
            # add the header, type and tooltip
            self._header.append(QVariant(name))
            self._types.append(ob.typ)
            self._tooltips.append(QVariant(ob.desc))
        # if we have a default value, set it
        if 'default' in args:
            if args['default'] is None:
                self._defaults[col] = QVariant('None')
            else:
                self._defaults[col] = QVariant(args['default'])
        # if this is optional
        elif 'optional' in args:
            self._optional.append(col)
        # it must be required
        else:
            self._required.append(col)
        # if we have combo box items
        if hasattr(ob, 'labels'):
            self._cItems[col] = QVariant(
                QStringList([QString(str(x)) for x in ob.labels]))
        # if we have combo box values
        if hasattr(ob, 'values'):
            self._cValues[col] = [QVariant(x) for x in ob.values]
        # if it's an ident
        if hasattr(ob, 'ident'):
            self._idents.append(col)

    def __convert(self, variant, typ):
        # convert to the requested type
        val = variant.toString()
        if typ == bool:
            if val.toLower() == QString("true"):
                return (True, True)
            elif val.toLower() == QString("false"):
                return (False, True)
            elif "$(" in val:
                return (val, True)
            return (val, False)
        elif typ == int:
            if "$(" in val:
                return (val, True)
            return variant.toInt()
        elif typ == float:
            if "$(" in val:
                return (val, True)
            _, ret = variant.toDouble()
            return (val, ret)
        elif typ == str:
            return (variant.toString(), True)
        else:
            return (variant, False)

    def createElements(self, doc, name):
        # create xml elements from this table
        header = [ str(x.toString()) for x in self._header ]
        for row in self.rows:
            el = doc.createElement(name)
            # lookup and add attributes
            for i in range(2, len(row)):
                if not row[i].isNull():
                    val = str(row[i].toString())
                    # We want True and False to be capitalised in the xml file
                    if self._types[i] == bool and val in ["true", "false"]:
                        val = val.title()
                    el.setAttribute(header[i], val)
            if len(row[1].toString()) > 0:
                doc.documentElement.appendChild(doc.createComment(str(row[1].toString()).strip()))
            if row[0].toBool() == True:
                # can't put -- in a comment unfortunately...
                el = doc.createComment(el.toxml().replace("--", "&dashdash;"))
            doc.documentElement.appendChild(el)

    def addNode(self, node, commented = False, commentText = ""):
        # add xml nodes as rows in the table
        w = []
        row = [ QVariant() ] * len(self._header)
        self.rows.append(row)
        if commented:
            row[0] = QVariant(True)
        else:
            row[0] = QVariant(False)
        for attr, value in node.attributes.items():
            attr = str(attr)
            value = str(value)
            index = -1
            for i, item in enumerate(self._header):
                if str(item.toString()) == attr:
                    index = i
                    break
            if index == -1:
                w.append('%s doesn\'t have attr %s' % (node.nodeName, attr))
                continue
            typ = self._types[index]
            row[index] = QVariant(self.__convert(QVariant(value), typ)[0])
            if not commented:
                invalid = self._isInvalid(row[index], len(self.rows)-1, index)
                if invalid:
                    w.append('%s.%s: %s' %(node.nodeName, attr, invalid))
        # add the row to the table
        if commentText:
            row[1] = QVariant(commentText)
        return w

    def flags(self, index):
        return Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable

    def rowCount(self, parent = None):
        return len(self.rows)

    def columnCount(self, parent = None):
        return len(self._header)

    def headerData(self, section, orientation, role = Qt.DisplayRole ):
        if role == Qt.DisplayRole:
            if orientation == Qt.Horizontal:
                return self._header[section]
            elif self.rows and self.rows[section] and \
                    not self.rows[section][2].isNull():
                return self.rows[section][2]
            else:
                return QVariant('[row %s]' % (section+1))
        elif role == Qt.ToolTipRole and orientation == Qt.Horizontal:
            return self._tooltips[section]
        else:
            return QVariant()


    # put the change request on the undo stack
    def setData(self, index, val, role=Qt.EditRole):
        if role == Qt.EditRole and \
                val != self.rows[index.row()][index.column()]:
            col = index.column()
            if col in self._cValues:
                # lookup the display in the list of _cItems
                for i, v in enumerate(self._cItems[col].toStringList()):
                    if v.toLower() == val.toString().toLower():
                        val = self._cValues[col][i]
            self.stack.push(
                ChangeValueCommand(index.row(), index.column(), val, self))
            return True
        else:
            return False

    def insertRows(self, row, count, parent = QModelIndex()):
        if count > 1:
            self.stack.beginMacro('Insert rows %d..%d'%(row+1, row+count))
        for row in range(row, row+count):
            self.stack.push(RowCommand(row, self, parent))
        if count > 1:
            self.stack.endMacro()

    def removeRows(self, row, count, parent = QModelIndex()):
        if count > 1:
            self.stack.beginMacro('Remove rows %d..%d'%(row+1, row+count))
        for row in reversed(range(row, row+count)):
            self.stack.push(RowCommand(row, self, parent, False))
        if count > 1:
            self.stack.endMacro()

    def sectionMoved(self, logicalIndex, oldVisualIndex, newVisualIndex, parent = QModelIndex()):
        assert oldVisualIndex == logicalIndex, \
            "oldVisualIndex %d should be equal to logicalIndex %d" % (
                oldVisualIndex, logicalIndex)
        self.stack.beginMacro('Move row %d to %d'%(oldVisualIndex, newVisualIndex))
        # grab the old data
        olddata = [QVariant(x) for x in self.rows[oldVisualIndex]]
        # delete the old row
        self.stack.push(RowCommand(oldVisualIndex, self, parent, False))
        # create a command to make a new row with the old data        
        cmd = RowCommand(newVisualIndex, self, parent)
        cmd.rowdata = olddata
        self.stack.push(cmd)
        self.stack.endMacro()

    def _isCommented(self, row):
        return self.rows[row][0].toBool()

    def _nameList(self, filt = None, without = None, upto = None):
        # need to search all tables for a string list of object names
        # filt is a ModuleBase subclass to filter by
        # without is a row number to exclude from the current table
        # upto means only look at objects up to "upto" row in the current table
        if (filt, without, upto) in self._cachedNameList:
            timestamp, sl = self._cachedNameList[(filt, without, upto)]
            if self._parent.lastModified() < timestamp:
                return sl
        sl = QStringList()
        for name in self._parent.getTableNames():
            table = self._parent._tables[name]
            # if we have a filter, then make sure this table is a subclass of it
            if filt is not None and type(filt) == types.ClassType and \
                    type(table.ob) == types.ClassType and \
                    not issubclass(table.ob, filt):
                # if we are only going up to a certain table and this is it
                if table == self and upto is not None:
                    return sl
                continue
            for i,trow in enumerate(table.rows):
                if table == self:
                    # if the current table is self, make sure we are excluding
                    # the without row
                    if without is not None and without == i:
                        continue
                    # make sure we only go up to upto
                    if upto is not None and upto == i:
                        return sl
                # add a non-null name, which is not commented out to the list
                if not trow[2].isNull() and not \
                        (not trow[0].isNull() and trow[0].toBool() == True):
                    sl.append(trow[2].toString())
        # store the cached value
        self._cachedNameList[(filt, without, upto)] = (time.time(), sl)
        return sl

    def _isInvalid(self, value, row, col):
        # check that required rows are filled in
        if value.isNull():
            if col in self._required:
                return 'Required argument not filled in'
            else:
                return False
        # check that names are unique
        elif col == 2:
            name = value.toString()
            index = self._nameList(without = row).indexOf(name)
            if index != -1:
                return 'Object with name "%s" already exists' % name
        # check that idents are valid
        elif col in self._idents:
            name = value.toString()
            ob = self._types[col]
            index = self._nameList(filt = ob, upto = row).indexOf(name)
            if index == -1:
                return 'Can\'t perform identifier lookup on "%s"' % name
        # check that enums are valid
        elif col in self._cValues:
            if not max([value == x for x in self._cValues[col]]):
                return '"%s" is not a supported enum' % value.toString()
        # check that choices are valid
        elif col in self._cItems:
            if not max(
                [value == QVariant(x)
                 for x in self._cItems[col].toStringList()]):
                return '"%s" is not a supported choice' % value.toString()
        # check the type of basetypes
        else:
            typ = self._types[col]
            v, ret = self.__convert(value, typ)
            if ret != True:
                return 'Cannot convert "%s" to %s' % (value.toString(), typ)
        return False

    def _isDefault(self, value, col):
        return value.isNull() and col in self._defaults

    def data(self, index, role):
        col = index.column()
        row = index.row()
        value = self.rows[row][col]
        # default view
        if role == Qt.DisplayRole:
            # comment row
            if col == 1:
                if len(value.toString()) > 0:
                    return QVariant("#..")
                else:
                    return QVariant()
            # if the cell is defaulted, display the default value
            elif self._isDefault(value, col):
                value = self._defaults[col]
            # if we've got a combo box lookup the appropriate value for the enum
            if col in self._cValues:
                # lookup the display in the list of _cItems
                for i, v in enumerate(self._cValues[col]):
                    if v.toString() == value.toString():
                        return QVariant(self._cItems[col].toStringList()[i])
            # display commented out rows as X
            elif col == 0:
                if value.toBool():
                    return QVariant(QString('X'))
                else:
                    return QVariant(QString(''))
            # empty string rows should be ""
            elif not value.isNull() and self._types[col] == str and \
                    str(value.toString()) == '' and col != 1:
                value = QVariant(QString('""'))
            return value
        # text editor
        elif role == Qt.EditRole:
            # if the cell is defaulted, display the default value
            if self._isDefault(value, col):
                value = self._defaults[col]
            # if we've got a combo box lookup the appropriate value for the enum
            if col in self._cValues:
                # lookup the display in the list of _cItems
                for i, v in enumerate(self._cValues[col]):
                    if v.toString() == value.toString():
                        return QVariant(self._cItems[col].toStringList()[i])
            # empty string rows should be ""
            elif not value.isNull() and self._types[col] == str and \
                    str(value.toString()) == '' and col != 1:
                value = QVariant(QString('""'))
            return value
        elif role == Qt.ToolTipRole:
            # tooltip
            error = self._isInvalid(value, row, col)
            text = str(self._tooltips[col].toString())
            if error:
                text = '***Error: %s\n%s'%(error, text)
            if col in self._idents:
                lines = ['\nPossible Values: ']
                for name in self._nameList(filt = self._types[col], upto = row):
                    if len(lines[-1]) > 80:
                        lines.append('')
                    lines[-1] += str(name) + ', '
                text += '\n'.join(lines).rstrip(' ,')
            if col == 1 and len(value.toString()) > 0:
                text += ":\n\n" + value.toString()
            return QVariant(text)
        elif role == Qt.ForegroundRole:
            # cell foreground
            if self._isCommented(row):
                # comment
                return QVariant(QColor(120,140,180))
            if self._isDefault(value, col):
                # is default arg (always valid)
                return QVariant(QColor(160,160,160))
            elif self._isInvalid(value, row, col):
                # invalid
                return QVariant(QColor(255,0,0))
            else:
                # valid
                return QVariant(QColor(0,0,0))
        elif role == Qt.BackgroundRole:
            # cell background
            if self._isCommented(row):
                # commented
                return QVariant(QColor(160,180,220))
            elif self._isInvalid(value, row, col):
                # invalid
                return QVariant(QColor(255,200,200))
            elif col in self._defaults:
                # has default
                return QVariant(QColor(255,255,240))
            elif col in self._optional:
                #is optional
                return QVariant(QColor(180,180,180))
            else:
                # valid
                return QVariant(QColor(250,250,250))
        elif role == Qt.UserRole:
            # combo box asking for list of items
            if col in self._idents:
                return QVariant(
                    self._nameList(filt = self._types[col], upto = row))
            elif col in self._cItems:
                return self._cItems[col]
            else:
                return QVariant()
        else:
            return QVariant()

    def clearIndexes(self, indexes):
        # clear cells from a list of QModelIndex's
        begun = False
        for item in indexes:
            if not self.rows[item.row()][item.column()].isNull():
                if not begun:
                    begun = True
                    celltexts = [ '(%s, %d)' %
                        (self._header[c.column()].toString(), c.row() + 1) \
                        for c in indexes ]
                    self.stack.beginMacro('Cleared Cells: '+' '.join(celltexts))
                self.setData(item, QVariant(), Qt.EditRole)
        if begun:
            self.stack.endMacro()
Пример #3
0
class UmbraLayer(object):
    count=0

    layer_count=0
    
    def __init__(self,umbra,grid,name=None):
        """
        Does not add the layers to the GUI - call register_layers
        for that.
        """
        # having some trouble getting reliable output from the log...
        if 0:
            self.log=log
        else:
            class DumbLog(object):
                def info(self,*a):
                    s=" ".join(a)
                    with open(os.path.join(os.path.dirname(__file__),'log'),'a') as fp:
                        fp.write(s+"\n")
                        fp.flush()
            log=DumbLog()
            log.debug=log.info
            log.warning=log.info
            log.error=log.info
            self.log=log
            
        self.umbra=umbra

        self.grid=grid

        self.iface=None # gets set in register_layers

        UmbraLayer.layer_count+=1

        if name is None:
            name="grid%d"%UmbraLayer.layer_count
        self.name=name 

        self.layers=[] # SubLayer objects associated with this grid.
        self.umbra.register_grid(self)
        
        self.undo_stack=QUndoStack()

    def match_to_qlayer(self,ql):
        # need to either make the names unique, or find a better test.
        # since we can't really enforce the grouped layout, better to make the names
        # unique.
        for layer in self.layers:
            if layer.qlayer.name() == ql.name():
                return True
        return False
    def grid_name(self):
        """ used to label the group
        """
        UmbraLayer.count+=1
        return "grid%4d"%UmbraLayer.count
        
    @classmethod
    def open_layer(klass,umbra,grid_format,path):
        g=klass.load_grid(path=path,grid_format=grid_format)
        return klass(umbra=umbra,grid=g)

    @classmethod
    def load_grid(klass,grid_format=None,path=None):
        if path is None:
            # for development, load sample data:
            suntans_path=os.path.join( os.path.dirname(__file__),
                                       "sample_data/sfbay" )
            grid=unstructured_grid.SuntansGrid(suntans_path)
        else:
            if grid_format=='SUNTANS':
                grid=unstructured_grid.SuntansGrid(path)
            elif grid_format=='pickle':
                grid=unstructured_grid.UnstructuredGrid.from_pickle(path)
            elif grid_format=='DFM':
                grid=dfm_grid.DFMGrid(fn=path)
            else:
                raise Exception("Need to add other grid types, like %s!"%grid_format)
        return grid


    def on_layer_deleted(self,sublayer):
        self.layers.remove(sublayer)
        self.log.info("signal received: node layer deleted")
        sublayer.unextend_grid()

        # these are the steps which lead to this callback.  don't do them
        # again.
        
        # reg=QgsMapLayerRegistry.instance()
        # reg.removeMapLayers([sublayer.qlayer])
        
    # create the memory layers and populate accordingly
    def register_layer(self,sublayer):
        self.layers.append( sublayer )
        def callback(sublayer=sublayer):
            self.on_layer_deleted(sublayer)
        sublayer.qlayer.layerDeleted.connect(callback)
        
        QgsMapLayerRegistry.instance().addMapLayer(sublayer.qlayer)
        li=self.iface.legendInterface()
        li.moveLayer(sublayer.qlayer,self.group_index)

    def layer_by_tag(self,tag):
        for layer in self.layers:
            if layer.tag == tag:
                return layer
        return None

    def create_group(self):
        # Create a group for the layers -
        li=self.iface.legendInterface()
        grp_name=self.grid_name()
        self.group_index=li.addGroup(grp_name)
        
    def register_layers(self):
        crs="?crs=epsg:26910" # was 4326
        self.iface=self.umbra.iface
        self.create_group()

        self.register_layer( UmbraCellLayer(self.log,
                                            self.grid,
                                            crs=crs,
                                            prefix=self.name,
                                            tag='cells') )

        self.register_layer( UmbraEdgeLayer(self.log,
                                            self.grid,
                                            crs=crs,
                                            prefix=self.name,
                                            tag='edges' ) )

        self.register_layer( UmbraNodeLayer(self.log,
                                            self.grid,
                                            crs=crs,
                                            prefix=self.name,
                                            tag='nodes') )
        
        # set extent to the extent of our layer
        # skip while developing
        # canvas.setExtent(layer.extent())

    def remove_all_qlayers(self):
        layers=[]
        for sublayer in self.layers:
            layers.append( sublayer.qlayer.name() )
        reg=QgsMapLayerRegistry.instance()
        self.log.info("Found %d layers to remove"%len(layers))
        
        reg.removeMapLayers(layers)
        
    def distance_to_node(self,pnt,i):
        """ compute distance from the given point to the given node, returned in
        physical distance units [meters]"""
        if not self.grid:
            return 1e6
        return mag( self.grid.nodes['x'][i] - np.asarray(pnt) )
    
    def distance_to_cell(self,pnt,c):
        if not self.grid:
            return 1e6
        return mag( self.grid.cells_center()[c] - np.asarray(pnt) )
    
    def find_closest_node(self,xy):
        # xy: [x,y]
        print "Finding closest node to ",xy
        return self.grid.select_nodes_nearest(xy)
    
    def find_closest_cell(self,xy):
        # xy: [x,y]
        return self.grid.select_cells_nearest(xy)

    def extent(self):
        xmin,xmax,ymin,ymax = self.grid.bounds()
        print "extent() called on UmbraLayer"
        return QgsRectangle(xmin,ymin,xmax,ymax)
      
    def renumber(self):
        self.grid.renumber()
    
    # thin wrapper to grid editing calls
    # channel them through here to (a) keep consistent interface
    # and (b) track undo stacks at the umbra layer level.

    def undo(self):
        if self.undo_stack.canUndo():
            self.undo_stack.undo()
        else:
            self.log.warning("request for undo, but stack cannot")
    def redo(self):
        if self.undo_stack.canRedo():
            self.undo_stack.redo()
        else:
            self.log.warning("request for undo, but stack cannot")
            
    def modify_node(self,n,**kw):
        cmd=GridCommand(self.grid,
                        "Modify node",
                        lambda: self.grid.modify_node(n,**kw))
        self.undo_stack.push(cmd)

    def toggle_cell_at_point(self,xy):
        def do_toggle():
            self.log.info("umbra_layer: toggle cell at %s"%xy)
            self.grid.toggle_cell_at_point(xy)
        cmd=GridCommand(self.grid,
                        "Toggle cell",
                        lambda: self.grid.toggle_cell_at_point(xy))
        self.undo_stack.push(cmd)
        
    def delete_node(self,n):
        cmd=GridCommand(self.grid,
                        "Delete node",
                        lambda: self.grid.delete_node_cascade(n))
        self.undo_stack.push(cmd)
    def delete_edge(self,e):
        cmd=GridCommand(self.grid,
                        "Delete edge",
                        lambda: self.grid.delete_edge_cascade(e))
        self.undo_stack.push(cmd)
        
    def add_edge(self,nodes):
        if self.grid.nodes_to_edge(*nodes) is not None:
            self.log.info("Edge already existed, probably")
            return
        
        self.add_edge_last_id=None
        def redo():
            j=self.grid.add_edge(nodes=nodes)
            self.add_edge_last_id=j
            
        cmd=GridCommand(self.grid,"Add edge",redo)
        self.undo_stack.push(cmd)

        assert self.add_edge_last_id is not None
        self.log.info("Adding an edge! j=%d"%self.add_edge_last_id)

        return self.add_edge_last_id
    
    def add_node(self,x):
        # awkward jumping through hoops to both use the undo stack
        # and get the id of a node which was just added
        self.add_node_last_id=None
        def redo():
            n=self.grid.add_node(x=x)
            self.add_node_last_id=n
            
        cmd=GridCommand(self.grid,"Add node",redo)
        self.undo_stack.push(cmd)
        
        assert self.add_node_last_id is not None
        return self.add_node_last_id

    def delete_selected(self):
        cell_layer=self.layer_by_tag('cells')
        if cell_layer is not None:
            selected_cells = cell_layer.selection()
            self.log.info("Found %d selected cells"%len(selected_cells))
            def redo(cells=selected_cells): # pass this way b/c of python bindings weirdness
                for c in cells:
                    self.grid.delete_cell(c)
            cmd=GridCommand(self.grid,"Delete cells",redo)
            self.undo_stack.push(cmd)
Пример #4
0
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])