예제 #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 ToolGrid(QFrame):
    """
    A widget containing a grid of actions/buttons.

    Actions can be added using standard :func:`QWidget.addAction(QAction)`
    and :func:`QWidget.insertAction(int, QAction)` methods.

    Parameters
    ----------
    parent : :class:`QWidget`
        Parent widget.
    columns : int
        Number of columns in the grid layout.
    buttonSize : :class:`QSize`, optional
        Size of tool buttons in the grid.
    iconSize : :class:`QSize`, optional
        Size of icons in the buttons.
    toolButtonStyle : :class:`Qt.ToolButtonStyle`
        Tool button style.

    """

    actionTriggered = Signal(QAction)
    actionHovered = Signal(QAction)

    def __init__(self,
                 parent=None,
                 columns=4,
                 buttonSize=None,
                 iconSize=None,
                 toolButtonStyle=Qt.ToolButtonTextUnderIcon):
        QFrame.__init__(self, parent)

        if buttonSize is not None:
            buttonSize = QSize(buttonSize)

        if iconSize is not None:
            iconSize = QSize(iconSize)

        self.__columns = columns
        self.__buttonSize = buttonSize or QSize(50, 50)
        self.__iconSize = iconSize or QSize(26, 26)
        self.__toolButtonStyle = toolButtonStyle

        self.__gridSlots = []

        self.__buttonListener = ToolButtonEventListener(self)
        self.__buttonListener.buttonRightClicked.connect(
            self.__onButtonRightClick)

        self.__buttonListener.buttonEnter.connect(self.__onButtonEnter)

        self.__mapper = QSignalMapper()
        self.__mapper.mapped[QObject].connect(self.__onClicked)

        self.__setupUi()

    def __setupUi(self):
        layout = QGridLayout()
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(0)
        layout.setSizeConstraint(QGridLayout.SetFixedSize)
        self.setLayout(layout)
        self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.MinimumExpanding)

    def setButtonSize(self, size):
        """
        Set the button size.
        """
        if self.__buttonSize != size:
            self.__buttonSize = size
            for slot in self.__gridSlots:
                slot.button.setFixedSize(size)

    def buttonSize(self):
        """
        Return the button size.
        """
        return QSize(self.__buttonSize)

    def setIconSize(self, size):
        """
        Set the button icon size.
        """
        if self.__iconSize != size:
            self.__iconSize = size
            for slot in self.__gridSlots:
                slot.button.setIconSize(size)

    def iconSize(self):
        """
        Return the icon size
        """
        return QSize(self.__iconSize)

    def setToolButtonStyle(self, style):
        """
        Set the tool button style.
        """
        if self.__toolButtonStyle != style:
            self.__toolButtonStyle = style
            for slot in self.__gridSlots:
                slot.button.setToolButtonStyle(style)

    def toolButtonStyle(self):
        """
        Return the tool button style.
        """
        return self.__toolButtonStyle

    def setColumnCount(self, columns):
        """
        Set the number of button/action columns.
        """
        if self.__columns != columns:
            self.__columns = columns
            self.__relayout()

    def columns(self):
        """
        Return the number of columns in the grid.
        """
        return self.__columns

    def clear(self):
        """
        Clear all actions/buttons.
        """
        for slot in reversed(list(self.__gridSlots)):
            self.removeAction(slot.action)
        self.__gridSlots = []

    def insertAction(self, before, action):
        """
        Insert a new action at the position currently occupied
        by `before` (can also be an index).

        Parameters
        ----------
        before : :class:`QAction` or int
            Position where the `action` should be inserted.
        action : :class:`QAction`
            Action to insert

        """
        if isinstance(before, int):
            actions = list(self.actions())
            if len(actions) == 0 or before >= len(actions):
                # Insert as the first action or the last action.
                return self.addAction(action)

            before = actions[before]

        return QFrame.insertAction(self, before, action)

    def setActions(self, actions):
        """
        Clear the grid and add `actions`.
        """
        self.clear()

        for action in actions:
            self.addAction(action)

    def buttonForAction(self, action):
        """
        Return the :class:`QToolButton` instance button for `action`.
        """
        actions = [slot.action for slot in self.__gridSlots]
        index = actions.index(action)
        return self.__gridSlots[index].button

    def createButtonForAction(self, action):
        """
        Create and return a :class:`QToolButton` for action.
        """
        button = _ToolGridButton(self)
        button.setDefaultAction(action)

        if self.__buttonSize.isValid():
            button.setFixedSize(self.__buttonSize)
        if self.__iconSize.isValid():
            button.setIconSize(self.__iconSize)

        button.setToolButtonStyle(self.__toolButtonStyle)
        button.setProperty("tool-grid-button", QVariant(True))
        return button

    def count(self):
        """
        Return the number of buttons/actions in the grid.
        """
        return len(self.__gridSlots)

    def actionEvent(self, event):
        QFrame.actionEvent(self, event)

        if event.type() == QEvent.ActionAdded:
            # Note: the action is already in the self.actions() list.
            actions = list(self.actions())
            index = actions.index(event.action())
            self.__insertActionButton(index, event.action())

        elif event.type() == QEvent.ActionRemoved:
            self.__removeActionButton(event.action())

    def __insertActionButton(self, index, action):
        """Create a button for the action and add it to the layout
        at index.

        """
        self.__shiftGrid(index, 1)
        button = self.createButtonForAction(action)

        row = index / self.__columns
        column = index % self.__columns

        self.layout().addWidget(button, row, column,
                                Qt.AlignLeft | Qt.AlignTop)

        self.__gridSlots.insert(index,
                                _ToolGridSlot(button, action, row, column))

        self.__mapper.setMapping(button, action)
        button.clicked.connect(self.__mapper.map)
        button.installEventFilter(self.__buttonListener)
        button.installEventFilter(self)

    def __removeActionButton(self, action):
        """Remove the button for the action from the layout and delete it.
        """
        actions = [slot.action for slot in self.__gridSlots]
        index = actions.index(action)
        slot = self.__gridSlots.pop(index)

        slot.button.removeEventFilter(self.__buttonListener)
        slot.button.removeEventFilter(self)
        self.__mapper.removeMappings(slot.button)

        self.layout().removeWidget(slot.button)
        self.__shiftGrid(index + 1, -1)

        slot.button.deleteLater()

    def __shiftGrid(self, start, count=1):
        """Shift all buttons starting at index `start` by `count` cells.
        """
        button_count = self.layout().count()
        direction = 1 if count >= 0 else -1
        if direction == 1:
            start, end = button_count - 1, start - 1
        else:
            start, end = start, button_count

        for index in range(start, end, -direction):
            item = self.layout().itemAtPosition(index / self.__columns,
                                                index % self.__columns)
            if item:
                button = item.widget()
                new_index = index + count
                self.layout().addWidget(button, new_index / self.__columns,
                                        new_index % self.__columns,
                                        Qt.AlignLeft | Qt.AlignTop)

    def __relayout(self):
        """Relayout the buttons.
        """
        for i in reversed(range(self.layout().count())):
            self.layout().takeAt(i)

        self.__gridSlots = [
            _ToolGridSlot(slot.button, slot.action, i / self.__columns,
                          i % self.__columns)
            for i, slot in enumerate(self.__gridSlots)
        ]

        for slot in self.__gridSlots:
            self.layout().addWidget(slot.button, slot.row, slot.column,
                                    Qt.AlignLeft | Qt.AlignTop)

    def __indexOf(self, button):
        """Return the index of button widget.
        """
        buttons = [slot.button for slot in self.__gridSlots]
        return buttons.index(button)

    def __onButtonRightClick(self, button):
        pass

    def __onButtonEnter(self, button):
        action = button.defaultAction()
        self.actionHovered.emit(action)

    def __onClicked(self, action):
        self.actionTriggered.emit(action)

    def paintEvent(self, event):
        return utils.StyledWidget_paintEvent(self, event)

    def eventFilter(self, obj, event):
        etype = event.type()
        if etype == QEvent.KeyPress and obj.hasFocus():
            key = event.key()
            if key in [Qt.Key_Up, Qt.Key_Down, Qt.Key_Left, Qt.Key_Right]:
                if self.__focusMove(obj, key):
                    event.accept()
                    return True

        return QFrame.eventFilter(self, obj, event)

    def __focusMove(self, focus, key):
        assert (focus is self.focusWidget())
        try:
            index = self.__indexOf(focus)
        except IndexError:
            return False

        if key == Qt.Key_Down:
            index += self.__columns
        elif key == Qt.Key_Up:
            index -= self.__columns
        elif key == Qt.Key_Left:
            index -= 1
        elif key == Qt.Key_Right:
            index += 1

        if index >= 0 and index < self.count():
            button = self.__gridSlots[index].button
            button.setFocus(Qt.TabFocusReason)
            return True
        else:
            return False
예제 #3
0
class CanvasScene(QGraphicsScene):
    """
    A Graphics Scene for displaying an :class:`~.scheme.Scheme` instance.
    """

    #: Signal emitted when a :class:`NodeItem` has been added to the scene.
    node_item_added = Signal(object)

    #: Signal emitted when a :class:`NodeItem` has been removed from the
    #: scene.
    node_item_removed = Signal(object)

    #: Signal emitted when a new :class:`LinkItem` has been added to the
    #: scene.
    link_item_added = Signal(object)

    #: Signal emitted when a :class:`LinkItem` has been removed.
    link_item_removed = Signal(object)

    #: Signal emitted when a :class:`Annotation` item has been added.
    annotation_added = Signal(object)

    #: Signal emitted when a :class:`Annotation` item has been removed.
    annotation_removed = Signal(object)

    #: Signal emitted when the position of a :class:`NodeItem` has changed.
    node_item_position_changed = Signal(object, QPointF)

    #: Signal emitted when an :class:`NodeItem` has been double clicked.
    node_item_double_clicked = Signal(object)

    #: An node item has been activated (clicked)
    node_item_activated = Signal(object)

    #: An node item has been hovered
    node_item_hovered = Signal(object)

    #: Link item has been hovered
    link_item_hovered = Signal(object)

    def __init__(self, *args, **kwargs):
        QGraphicsScene.__init__(self, *args, **kwargs)

        self.scheme = None
        self.registry = None

        # All node items
        self.__node_items = []
        # Mapping from SchemeNodes to canvas items
        self.__item_for_node = {}
        # All link items
        self.__link_items = []
        # Mapping from SchemeLinks to canvas items.
        self.__item_for_link = {}

        # All annotation items
        self.__annotation_items = []
        # Mapping from SchemeAnnotations to canvas items.
        self.__item_for_annotation = {}

        # Is the scene editable
        self.editable = True

        # Anchor Layout
        self.__anchor_layout = AnchorLayout()
        self.addItem(self.__anchor_layout)

        self.__channel_names_visible = True
        self.__node_animation_enabled = True

        self.user_interaction_handler = None

        self.activated_mapper = QSignalMapper(self)
        self.activated_mapper.mapped[QObject].connect(
            lambda node: self.node_item_activated.emit(node)
        )
        self.hovered_mapper = QSignalMapper(self)
        self.hovered_mapper.mapped[QObject].connect(
            lambda node: self.node_item_hovered.emit(node)
        )
        self.position_change_mapper = QSignalMapper(self)
        self.position_change_mapper.mapped[QObject].connect(
            self._on_position_change
        )
        log.info("'%s' intitialized." % self)

    def clear_scene(self):
        """
        Clear (reset) the scene.
        """
        if self.scheme is not None:
            self.scheme.node_added.disconnect(self.add_node)
            self.scheme.node_removed.disconnect(self.remove_node)

            self.scheme.link_added.disconnect(self.add_link)
            self.scheme.link_removed.disconnect(self.remove_link)

            self.scheme.annotation_added.disconnect(self.add_annotation)
            self.scheme.annotation_removed.disconnect(self.remove_annotation)

            self.scheme.node_state_changed.disconnect(
                self.on_widget_state_change
            )
            self.scheme.channel_state_changed.disconnect(
                self.on_link_state_change
            )

            # Remove all items to make sure all signals from scheme items
            # to canvas items are disconnected.

            for annot in self.scheme.annotations:
                if annot in self.__item_for_annotation:
                    self.remove_annotation(annot)

            for link in self.scheme.links:
                if link in self.__item_for_link:
                    self.remove_link(link)

            for node in self.scheme.nodes:
                if node in self.__item_for_node:
                    self.remove_node(node)

        self.scheme = None
        self.__node_items = []
        self.__item_for_node = {}
        self.__link_items = []
        self.__item_for_link = {}
        self.__annotation_items = []
        self.__item_for_annotation = {}

        self.__anchor_layout.deleteLater()

        self.user_interaction_handler = None

        self.clear()
        log.info("'%s' cleared." % self)

    def set_scheme(self, scheme):
        """
        Set the scheme to display. Populates the scene with nodes and links
        already in the scheme. Any further change to the scheme will be
        reflected in the scene.

        Parameters
        ----------
        scheme : :class:`~.scheme.Scheme`

        """
        if self.scheme is not None:
            # Clear the old scheme
            self.clear_scene()

        log.info("Setting scheme '%s' on '%s'" % (scheme, self))

        self.scheme = scheme
        if self.scheme is not None:
            self.scheme.node_added.connect(self.add_node)
            self.scheme.node_removed.connect(self.remove_node)

            self.scheme.link_added.connect(self.add_link)
            self.scheme.link_removed.connect(self.remove_link)

            self.scheme.annotation_added.connect(self.add_annotation)
            self.scheme.annotation_removed.connect(self.remove_annotation)

            self.scheme.node_state_changed.connect(
                self.on_widget_state_change
            )
            self.scheme.channel_state_changed.connect(
                self.on_link_state_change
            )

            self.scheme.topology_changed.connect(self.on_scheme_change)

        for node in scheme.nodes:
            self.add_node(node)

        for link in scheme.links:
            self.add_link(link)

        for annot in scheme.annotations:
            self.add_annotation(annot)

    def set_registry(self, registry):
        """
        Set the widget registry.
        """
        # TODO: Remove/Deprecate. Is used only to get the category/background
        # color. That should be part of the SchemeNode/WidgetDescription.
        log.info("Setting registry '%s on '%s'." % (registry, self))
        self.registry = registry

    def set_anchor_layout(self, layout):
        """
        Set an :class:`~.layout.AnchorLayout`
        """
        if self.__anchor_layout != layout:
            if self.__anchor_layout:
                self.__anchor_layout.deleteLater()
                self.__anchor_layout = None

            self.__anchor_layout = layout

    def anchor_layout(self):
        """
        Return the anchor layout instance.
        """
        return self.__anchor_layout

    def set_channel_names_visible(self, visible):
        """
        Set the channel names visibility.
        """
        self.__channel_names_visible = visible
        for link in self.__link_items:
            link.setChannelNamesVisible(visible)

    def channel_names_visible(self):
        """
        Return the channel names visibility state.
        """
        return self.__channel_names_visible

    def set_node_animation_enabled(self, enabled):
        """
        Set node animation enabled state.
        """
        if self.__node_animation_enabled != enabled:
            self.__node_animation_enabled = enabled

            for node in self.__node_items:
                node.setAnimationEnabled(enabled)

    def add_node_item(self, item):
        """
        Add a :class:`.NodeItem` instance to the scene.
        """
        if item in self.__node_items:
            raise ValueError("%r is already in the scene." % item)

        if item.pos().isNull():
            if self.__node_items:
                pos = self.__node_items[-1].pos() + QPointF(150, 0)
            else:
                pos = QPointF(150, 150)

            item.setPos(pos)

        item.setFont(self.font())

        # Set signal mappings
        self.activated_mapper.setMapping(item, item)
        item.activated.connect(self.activated_mapper.map)

        self.hovered_mapper.setMapping(item, item)
        item.hovered.connect(self.hovered_mapper.map)

        self.position_change_mapper.setMapping(item, item)
        item.positionChanged.connect(self.position_change_mapper.map)

        self.addItem(item)

        self.__node_items.append(item)

        self.node_item_added.emit(item)

        log.info("Added item '%s' to '%s'" % (item, self))
        return item

    def add_node(self, node):
        """
        Add and return a default constructed :class:`.NodeItem` for a
        :class:`SchemeNode` instance `node`. If the `node` is already in
        the scene do nothing and just return its item.

        """
        if node in self.__item_for_node:
            # Already added
            return self.__item_for_node[node]

        item = self.new_node_item(node.description)

        if node.position:
            pos = QPointF(*node.position)
            item.setPos(pos)

        item.setTitle(node.title)
        item.setProcessingState(node.processing_state)
        item.setProgress(node.progress)

        for message in node.state_messages():
            item.setStateMessage(message)

        item.setStatusMessage(node.status_message())

        self.__item_for_node[node] = item

        node.position_changed.connect(self.__on_node_pos_changed)
        node.title_changed.connect(item.setTitle)
        node.progress_changed.connect(item.setProgress)
        node.processing_state_changed.connect(item.setProcessingState)
        node.state_message_changed.connect(item.setStateMessage)
        node.status_message_changed.connect(item.setStatusMessage)

        return self.add_node_item(item)

    def new_node_item(self, widget_desc, category_desc=None):
        """
        Construct an new :class:`.NodeItem` from a `WidgetDescription`.
        Optionally also set `CategoryDescription`.

        """
        item = items.NodeItem()
        item.setWidgetDescription(widget_desc)

        if category_desc is None and self.registry and widget_desc.category:
            category_desc = self.registry.category(widget_desc.category)

        if category_desc is None and self.registry is not None:
            try:
                category_desc = self.registry.category(widget_desc.category)
            except KeyError:
                pass

        if category_desc is not None:
            item.setWidgetCategory(category_desc)

        item.setAnimationEnabled(self.__node_animation_enabled)
        return item

    def remove_node_item(self, item):
        """
        Remove `item` (:class:`.NodeItem`) from the scene.
        """
        self.activated_mapper.removeMappings(item)
        self.hovered_mapper.removeMappings(item)
        self.position_change_mapper.removeMappings(item)

        item.hide()
        self.removeItem(item)
        self.__node_items.remove(item)

        self.node_item_removed.emit(item)

        log.info("Removed item '%s' from '%s'" % (item, self))

    def remove_node(self, node):
        """
        Remove the :class:`.NodeItem` instance that was previously
        constructed for a :class:`SchemeNode` `node` using the `add_node`
        method.

        """
        item = self.__item_for_node.pop(node)

        node.position_changed.disconnect(self.__on_node_pos_changed)
        node.title_changed.disconnect(item.setTitle)
        node.progress_changed.disconnect(item.setProgress)
        node.processing_state_changed.disconnect(item.setProcessingState)
        node.state_message_changed.disconnect(item.setStateMessage)

        self.remove_node_item(item)

    def node_items(self):
        """
        Return all :class:`.NodeItem` instances in the scene.
        """
        return list(self.__node_items)

    def add_link_item(self, item):
        """
        Add a link (:class:`.LinkItem`) to the scene.
        """
        if item.scene() is not self:
            self.addItem(item)

        item.setFont(self.font())
        self.__link_items.append(item)

        self.link_item_added.emit(item)

        log.info("Added link %r -> %r to '%s'" % \
                 (item.sourceItem.title(), item.sinkItem.title(), self))

        self.__anchor_layout.invalidateLink(item)

        return item

    def add_link(self, scheme_link):
        """
        Create and add a :class:`.LinkItem` instance for a
        :class:`SchemeLink` instance. If the link is already in the scene
        do nothing and just return its :class:`.LinkItem`.

        """
        if scheme_link in self.__item_for_link:
            return self.__item_for_link[scheme_link]

        source = self.__item_for_node[scheme_link.source_node]
        sink = self.__item_for_node[scheme_link.sink_node]

        item = self.new_link_item(source, scheme_link.source_channel,
                                  sink, scheme_link.sink_channel)

        item.setEnabled(scheme_link.enabled)
        scheme_link.enabled_changed.connect(item.setEnabled)

        if scheme_link.is_dynamic():
            item.setDynamic(True)
            item.setDynamicEnabled(scheme_link.dynamic_enabled)
            scheme_link.dynamic_enabled_changed.connect(item.setDynamicEnabled)

        item.setRuntimeState(scheme_link.runtime_state())
        scheme_link.state_changed.connect(item.setRuntimeState)

        self.add_link_item(item)
        self.__item_for_link[scheme_link] = item
        return item

    def new_link_item(self, source_item, source_channel,
                      sink_item, sink_channel):
        """
        Construct and return a new :class:`.LinkItem`
        """
        item = items.LinkItem()
        item.setSourceItem(source_item)
        item.setSinkItem(sink_item)

        def channel_name(channel):
            if isinstance(channel, str):
                return channel
            else:
                return channel.name

        source_name = channel_name(source_channel)
        sink_name = channel_name(sink_channel)

        fmt = "<b>{0}</b>&nbsp; \u2192 &nbsp;<b>{1}</b>"
        item.setToolTip(
            fmt.format(escape(source_name),
                       escape(sink_name))
        )

        item.setSourceName(source_name)
        item.setSinkName(sink_name)
        item.setChannelNamesVisible(self.__channel_names_visible)

        return item

    def remove_link_item(self, item):
        """
        Remove a link (:class:`.LinkItem`) from the scene.
        """
        # Invalidate the anchor layout.
        self.__anchor_layout.invalidateAnchorItem(
            item.sourceItem.outputAnchorItem
        )
        self.__anchor_layout.invalidateAnchorItem(
            item.sinkItem.inputAnchorItem
        )

        self.__link_items.remove(item)

        # Remove the anchor points.
        item.removeLink()
        self.removeItem(item)

        self.link_item_removed.emit(item)

        log.info("Removed link '%s' from '%s'" % (item, self))

        return item

    def remove_link(self, scheme_link):
        """
        Remove a :class:`.LinkItem` instance that was previously constructed
        for a :class:`SchemeLink` instance `link` using the `add_link` method.

        """
        item = self.__item_for_link.pop(scheme_link)
        scheme_link.enabled_changed.disconnect(item.setEnabled)

        if scheme_link.is_dynamic():
            scheme_link.dynamic_enabled_changed.disconnect(
                item.setDynamicEnabled
            )
        scheme_link.state_changed.disconnect(item.setRuntimeState)
        self.remove_link_item(item)

    def link_items(self):
        """
        Return all :class:`.LinkItem`\s in the scene.
        """
        return list(self.__link_items)

    def add_annotation_item(self, annotation):
        """
        Add an :class:`.Annotation` item to the scene.
        """
        self.__annotation_items.append(annotation)
        self.addItem(annotation)
        self.annotation_added.emit(annotation)
        return annotation

    def add_annotation(self, scheme_annot):
        """
        Create a new item for :class:`SchemeAnnotation` and add it
        to the scene. If the `scheme_annot` is already in the scene do
        nothing and just return its item.

        """
        if scheme_annot in self.__item_for_annotation:
            # Already added
            return self.__item_for_annotation[scheme_annot]

        if isinstance(scheme_annot, scheme.SchemeTextAnnotation):
            item = items.TextAnnotation()
            item.setPlainText(scheme_annot.text)
            x, y, w, h = scheme_annot.rect
            item.setPos(x, y)
            item.resize(w, h)
            item.setTextInteractionFlags(Qt.TextEditorInteraction)

            font = font_from_dict(scheme_annot.font, item.font())
            item.setFont(font)
            scheme_annot.text_changed.connect(item.setPlainText)

        elif isinstance(scheme_annot, scheme.SchemeArrowAnnotation):
            item = items.ArrowAnnotation()
            start, end = scheme_annot.start_pos, scheme_annot.end_pos
            item.setLine(QLineF(QPointF(*start), QPointF(*end)))
            item.setColor(QColor(scheme_annot.color))

        scheme_annot.geometry_changed.connect(
            self.__on_scheme_annot_geometry_change
        )

        self.add_annotation_item(item)
        self.__item_for_annotation[scheme_annot] = item

        return item

    def remove_annotation_item(self, annotation):
        """
        Remove an :class:`.Annotation` instance from the scene.

        """
        self.__annotation_items.remove(annotation)
        self.removeItem(annotation)
        self.annotation_removed.emit(annotation)

    def remove_annotation(self, scheme_annotation):
        """
        Remove an :class:`.Annotation` instance that was previously added
        using :func:`add_anotation`.

        """
        item = self.__item_for_annotation.pop(scheme_annotation)

        scheme_annotation.geometry_changed.disconnect(
            self.__on_scheme_annot_geometry_change
        )

        if isinstance(scheme_annotation, scheme.SchemeTextAnnotation):
            scheme_annotation.text_changed.disconnect(
                item.setPlainText
            )

        self.remove_annotation_item(item)

    def annotation_items(self):
        """
        Return all :class:`.Annotation` items in the scene.
        """
        return self.__annotation_items

    def item_for_annotation(self, scheme_annotation):
        return self.__item_for_annotation[scheme_annotation]

    def annotation_for_item(self, item):
        rev = dict(reversed(item) \
                   for item in self.__item_for_annotation.items())
        return rev[item]

    def commit_scheme_node(self, node):
        """
        Commit the `node` into the scheme.
        """
        if not self.editable:
            raise Exception("Scheme not editable.")

        if node not in self.__item_for_node:
            raise ValueError("No 'NodeItem' for node.")

        item = self.__item_for_node[node]

        try:
            self.scheme.add_node(node)
        except Exception:
            log.error("An error occurred while committing node '%s'",
                      node, exc_info=True)
            # Cleanup (remove the node item)
            self.remove_node_item(item)
            raise

        log.info("Commited node '%s' from '%s' to '%s'" % \
                 (node, self, self.scheme))

    def commit_scheme_link(self, link):
        """
        Commit a scheme link.
        """
        if not self.editable:
            raise Exception("Scheme not editable")

        if link not in self.__item_for_link:
            raise ValueError("No 'LinkItem' for link.")

        self.scheme.add_link(link)
        log.info("Commited link '%s' from '%s' to '%s'" % \
                 (link, self, self.scheme))

    def node_for_item(self, item):
        """
        Return the `SchemeNode` for the `item`.
        """
        rev = dict([(v, k) for k, v in self.__item_for_node.items()])
        return rev[item]

    def item_for_node(self, node):
        """
        Return the :class:`NodeItem` instance for a :class:`SchemeNode`.
        """
        return self.__item_for_node[node]

    def link_for_item(self, item):
        """
        Return the `SchemeLink for `item` (:class:`LinkItem`).
        """
        rev = dict([(v, k) for k, v in self.__item_for_link.items()])
        return rev[item]

    def item_for_link(self, link):
        """
        Return the :class:`LinkItem` for a :class:`SchemeLink`
        """
        return self.__item_for_link[link]

    def selected_node_items(self):
        """
        Return the selected :class:`NodeItem`'s.
        """
        return [item for item in self.__node_items if item.isSelected()]

    def selected_annotation_items(self):
        """
        Return the selected :class:`Annotation`'s
        """
        return [item for item in self.__annotation_items if item.isSelected()]

    def node_links(self, node_item):
        """
        Return all links from the `node_item` (:class:`NodeItem`).
        """
        return self.node_output_links(node_item) + \
               self.node_input_links(node_item)

    def node_output_links(self, node_item):
        """
        Return a list of all output links from `node_item`.
        """
        return [link for link in self.__link_items
                if link.sourceItem == node_item]

    def node_input_links(self, node_item):
        """
        Return a list of all input links for `node_item`.
        """
        return [link for link in self.__link_items
                if link.sinkItem == node_item]

    def neighbor_nodes(self, node_item):
        """
        Return a list of `node_item`'s (class:`NodeItem`) neighbor nodes.
        """
        neighbors = list(map(attrgetter("sourceItem"),
                             self.node_input_links(node_item)))

        neighbors.extend(map(attrgetter("sinkItem"),
                             self.node_output_links(node_item)))
        return neighbors

    def on_widget_state_change(self, widget, state):
        pass

    def on_link_state_change(self, link, state):
        pass

    def on_scheme_change(self, ):
        pass

    def _on_position_change(self, item):
        # Invalidate the anchor point layout and schedule a layout.
        self.__anchor_layout.invalidateNode(item)

        self.node_item_position_changed.emit(item, item.pos())

    def __on_node_pos_changed(self, pos):
        node = self.sender()
        item = self.__item_for_node[node]
        item.setPos(*pos)

    def __on_scheme_annot_geometry_change(self):
        annot = self.sender()
        item = self.__item_for_annotation[annot]
        if isinstance(annot, scheme.SchemeTextAnnotation):
            item.setGeometry(QRectF(*annot.rect))
        elif isinstance(annot, scheme.SchemeArrowAnnotation):
            p1 = item.mapFromScene(QPointF(*annot.start_pos))
            p2 = item.mapFromScene(QPointF(*annot.end_pos))
            item.setLine(QLineF(p1, p2))
        else:
            pass

    def item_at(self, pos, type_or_tuple=None, buttons=0):
        """Return the item at `pos` that is an instance of the specified
        type (`type_or_tuple`). If `buttons` (`Qt.MouseButtons`) is given
        only return the item if it is the top level item that would
        accept any of the buttons (`QGraphicsItem.acceptedMouseButtons`).

        """
        rect = QRectF(pos, QSizeF(1, 1))
        items = self.items(rect)

        if buttons:
            items = itertools.dropwhile(
                lambda item: not item.acceptedMouseButtons() & buttons,
                items
            )
            items = list(items)[:1]

        if type_or_tuple:
            items = [i for i in items if isinstance(i, type_or_tuple)]

        return items[0] if items else None

    if PYQT_VERSION < 0x40900:
        # For QGraphicsObject subclasses items, itemAt ... return a
        # QGraphicsItem wrapper instance and not the actual class instance.
        def itemAt(self, *args, **kwargs):
            item = QGraphicsScene.itemAt(self, *args, **kwargs)
            return toGraphicsObjectIfPossible(item)

        def items(self, *args, **kwargs):
            items = QGraphicsScene.items(self, *args, **kwargs)
            return list(map(toGraphicsObjectIfPossible, items))

        def selectedItems(self, *args, **kwargs):
            return list(map(toGraphicsObjectIfPossible,
                       QGraphicsScene.selectedItems(self, *args, **kwargs)))

        def collidingItems(self, *args, **kwargs):
            return list(map(toGraphicsObjectIfPossible,
                       QGraphicsScene.collidingItems(self, *args, **kwargs)))

        def focusItem(self, *args, **kwargs):
            item = QGraphicsScene.focusItem(self, *args, **kwargs)
            return toGraphicsObjectIfPossible(item)

        def mouseGrabberItem(self, *args, **kwargs):
            item = QGraphicsScene.mouseGrabberItem(self, *args, **kwargs)
            return toGraphicsObjectIfPossible(item)

    def mousePressEvent(self, event):
        if self.user_interaction_handler and \
                self.user_interaction_handler.mousePressEvent(event):
            return

        # Right (context) click on the node item. If the widget is not
        # in the current selection then select the widget (only the widget).
        # Else simply return and let customContextMenuReqested signal
        # handle it
        shape_item = self.item_at(event.scenePos(), items.NodeItem)
        if shape_item and event.button() == Qt.RightButton and \
                shape_item.flags() & QGraphicsItem.ItemIsSelectable:
            if not shape_item.isSelected():
                self.clearSelection()
                shape_item.setSelected(True)

        return QGraphicsScene.mousePressEvent(self, event)

    def mouseMoveEvent(self, event):
        if self.user_interaction_handler and \
                self.user_interaction_handler.mouseMoveEvent(event):
            return

        return QGraphicsScene.mouseMoveEvent(self, event)

    def mouseReleaseEvent(self, event):
        if self.user_interaction_handler and \
                self.user_interaction_handler.mouseReleaseEvent(event):
            return
        return QGraphicsScene.mouseReleaseEvent(self, event)

    def mouseDoubleClickEvent(self, event):
        if self.user_interaction_handler and \
                self.user_interaction_handler.mouseDoubleClickEvent(event):
            return

        return QGraphicsScene.mouseDoubleClickEvent(self, event)

    def keyPressEvent(self, event):
        if self.user_interaction_handler and \
                self.user_interaction_handler.keyPressEvent(event):
            return
        return QGraphicsScene.keyPressEvent(self, event)

    def keyReleaseEvent(self, event):
        if self.user_interaction_handler and \
                self.user_interaction_handler.keyReleaseEvent(event):
            return
        return QGraphicsScene.keyReleaseEvent(self, event)

    def set_user_interaction_handler(self, handler):
        if self.user_interaction_handler and \
                not self.user_interaction_handler.isFinished():
            self.user_interaction_handler.cancel()

        log.info("Setting interaction '%s' to '%s'" % (handler, self))

        self.user_interaction_handler = handler
        if handler:
            handler.start()

    def event(self, event):
        # TODO: change the base class of Node/LinkItem to QGraphicsWidget.
        # It already handles font changes.
        if event.type() == QEvent.FontChange:
            self.__update_font()

        return QGraphicsScene.event(self, event)

    def __update_font(self):
        font = self.font()
        for item in self.__node_items + self.__link_items:
            item.setFont(font)

    def __str__(self):
        return "%s(objectName=%r, ...)" % \
                (type(self).__name__, str(self.objectName()))
예제 #4
0
class ToolGrid(QFrame):
    """
    A widget containing a grid of actions/buttons.

    Actions can be added using standard :ref:`QWidget.addAction(QAction)`
    and :ref:`QWidget.insertAction(int, QAction)` methods.

    Parameters
    ----------
    parent : :class:`QWidget`
        Parent widget.
    columns : int
        Number of columns in the grid layout.
    buttonSize : :class:`QSize`, optional
        Size of tool buttons in the grid.
    iconSize : :class:`QSize`, optional
        Size of icons in the buttons.
    toolButtonStyle : :class:`Qt.ToolButtonStyle`
        Tool button style.

    """

    actionTriggered = Signal(QAction)
    actionHovered = Signal(QAction)

    def __init__(self, parent=None, columns=4, buttonSize=None,
                 iconSize=None, toolButtonStyle=Qt.ToolButtonTextUnderIcon):
        QFrame.__init__(self, parent)

        if buttonSize is not None:
            buttonSize = QSize(buttonSize)

        if iconSize is not None:
            iconSize = QSize(iconSize)

        self.__columns = columns
        self.__buttonSize = buttonSize or QSize(50, 50)
        self.__iconSize = iconSize or QSize(26, 26)
        self.__toolButtonStyle = toolButtonStyle

        self.__gridSlots = []

        self.__buttonListener = ToolButtonEventListener(self)
        self.__buttonListener.buttonRightClicked.connect(
                self.__onButtonRightClick)

        self.__buttonListener.buttonEnter.connect(
                self.__onButtonEnter)

        self.__mapper = QSignalMapper()
        self.__mapper.mapped[QObject].connect(self.__onClicked)

        self.__setupUi()

    def __setupUi(self):
        layout = QGridLayout()
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(0)
        layout.setSizeConstraint(QGridLayout.SetFixedSize)
        self.setLayout(layout)
        self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.MinimumExpanding)

    def setButtonSize(self, size):
        """
        Set the button size.
        """
        if self.__buttonSize != size:
            self.__buttonSize = size
            for slot in self.__gridSlots:
                slot.button.setFixedSize(size)

    def buttonSize(self):
        """
        Return the button size.
        """
        return QSize(self.__buttonSize)

    def setIconSize(self, size):
        """
        Set the button icon size.
        """
        if self.__iconSize != size:
            self.__iconSize = size
            for slot in self.__gridSlots:
                slot.button.setIconSize(size)

    def iconSize(self):
        """
        Return the icon size
        """
        return QSize(self.__iconSize)

    def setToolButtonStyle(self, style):
        """
        Set the tool button style.
        """
        if self.__toolButtonStyle != style:
            self.__toolButtonStyle = style
            for slot in self.__gridSlots:
                slot.button.setToolButtonStyle(style)

    def toolButtonStyle(self):
        """
        Return the tool button style.
        """
        return self.__toolButtonStyle

    def setColumnCount(self, columns):
        """
        Set the number of button/action columns.
        """
        if self.__columns != columns:
            self.__columns = columns
            self.__relayout()

    def columns(self):
        """
        Return the number of columns in the grid.
        """
        return self.__columns

    def clear(self):
        """
        Clear all actions/buttons.
        """
        for slot in reversed(list(self.__gridSlots)):
            self.removeAction(slot.action)
        self.__gridSlots = []

    def insertAction(self, before, action):
        """
        Insert a new action at the position currently occupied
        by `before` (can also be an index).

        Parameters
        ----------
        before : :class:`QAction` or int
            Position where the `action` should be inserted.
        action : :class:`QAction`
            Action to insert

        """
        if isinstance(before, int):
            actions = list(self.actions())
            if len(actions) == 0 or before >= len(actions):
                # Insert as the first action or the last action.
                return self.addAction(action)

            before = actions[before]

        return QFrame.insertAction(self, before, action)

    def setActions(self, actions):
        """
        Clear the grid and add `actions`.
        """
        self.clear()

        for action in actions:
            self.addAction(action)

    def buttonForAction(self, action):
        """
        Return the :class:`QToolButton` instance button for `action`.
        """
        actions = [slot.action for slot in self.__gridSlots]
        index = actions.index(action)
        return self.__gridSlots[index].button

    def createButtonForAction(self, action):
        """
        Create and return a :class:`QToolButton` for action.
        """
        button = _ToolGridButton(self)
        button.setDefaultAction(action)

        if self.__buttonSize.isValid():
            button.setFixedSize(self.__buttonSize)
        if self.__iconSize.isValid():
            button.setIconSize(self.__iconSize)

        button.setToolButtonStyle(self.__toolButtonStyle)
        button.setProperty("tool-grid-button", qtcompat.qwrap(True))
        return button

    def count(self):
        """
        Return the number of buttons/actions in the grid.
        """
        return len(self.__gridSlots)

    def actionEvent(self, event):
        QFrame.actionEvent(self, event)

        if event.type() == QEvent.ActionAdded:
            # Note: the action is already in the self.actions() list.
            actions = list(self.actions())
            index = actions.index(event.action())
            self.__insertActionButton(index, event.action())

        elif event.type() == QEvent.ActionRemoved:
            self.__removeActionButton(event.action())

    def __insertActionButton(self, index, action):
        """Create a button for the action and add it to the layout
        at index.

        """
        self.__shiftGrid(index, 1)
        button = self.createButtonForAction(action)

        row = index / self.__columns
        column = index % self.__columns

        self.layout().addWidget(
            button, row, column,
            Qt.AlignLeft | Qt.AlignTop
        )

        self.__gridSlots.insert(
            index, _ToolGridSlot(button, action, row, column)
        )

        self.__mapper.setMapping(button, action)
        button.clicked.connect(self.__mapper.map)
        button.installEventFilter(self.__buttonListener)
        button.installEventFilter(self)

    def __removeActionButton(self, action):
        """Remove the button for the action from the layout and delete it.
        """
        actions = [slot.action for slot in self.__gridSlots]
        index = actions.index(action)
        slot = self.__gridSlots.pop(index)

        slot.button.removeEventFilter(self.__buttonListener)
        slot.button.removeEventFilter(self)
        self.__mapper.removeMappings(slot.button)

        self.layout().removeWidget(slot.button)
        self.__shiftGrid(index + 1, -1)

        slot.button.deleteLater()

    def __shiftGrid(self, start, count=1):
        """Shift all buttons starting at index `start` by `count` cells.
        """
        button_count = self.layout().count()
        direction = 1 if count >= 0 else -1
        if direction == 1:
            start, end = button_count - 1, start - 1
        else:
            start, end = start, button_count

        for index in range(start, end, -direction):
            item = self.layout().itemAtPosition(index / self.__columns,
                                                index % self.__columns)
            if item:
                button = item.widget()
                new_index = index + count
                self.layout().addWidget(button, new_index / self.__columns,
                                        new_index % self.__columns,
                                        Qt.AlignLeft | Qt.AlignTop)

    def __relayout(self):
        """Relayout the buttons.
        """
        for i in reversed(range(self.layout().count())):
            self.layout().takeAt(i)

        self.__gridSlots = [_ToolGridSlot(slot.button, slot.action,
                                          i / self.__columns,
                                          i % self.__columns)
                            for i, slot in enumerate(self.__gridSlots)]

        for slot in self.__gridSlots:
            self.layout().addWidget(slot.button, slot.row, slot.column,
                                    Qt.AlignLeft | Qt.AlignTop)

    def __indexOf(self, button):
        """Return the index of button widget.
        """
        buttons = [slot.button for slot in self.__gridSlots]
        return buttons.index(button)

    def __onButtonRightClick(self, button):
        pass

    def __onButtonEnter(self, button):
        action = button.defaultAction()
        self.actionHovered.emit(action)

    def __onClicked(self, action):
        self.actionTriggered.emit(action)

    def paintEvent(self, event):
        return utils.StyledWidget_paintEvent(self, event)

    def eventFilter(self, obj, event):
        etype = event.type()
        if etype == QEvent.KeyPress and obj.hasFocus():
            key = event.key()
            if key in [Qt.Key_Up, Qt.Key_Down, Qt.Key_Left, Qt.Key_Right]:
                if self.__focusMove(obj, key):
                    event.accept()
                    return True

        return QFrame.eventFilter(self, obj, event)

    def __focusMove(self, focus, key):
        assert(focus is self.focusWidget())
        try:
            index = self.__indexOf(focus)
        except IndexError:
            return False

        if key == Qt.Key_Down:
            index += self.__columns
        elif key == Qt.Key_Up:
            index -= self.__columns
        elif key == Qt.Key_Left:
            index -= 1
        elif key == Qt.Key_Right:
            index += 1

        if index >= 0 and index < self.count():
            button = self.__gridSlots[index].button
            button.setFocus(Qt.TabFocusReason)
            return True
        else:
            return False
예제 #5
0
파일: scene.py 프로젝트: 675801717/orange3
class CanvasScene(QGraphicsScene):
    """
    A Graphics Scene for displaying an :class:`~.scheme.Scheme` instance.
    """

    #: Signal emitted when a :class:`NodeItem` has been added to the scene.
    node_item_added = Signal(object)

    #: Signal emitted when a :class:`NodeItem` has been removed from the
    #: scene.
    node_item_removed = Signal(object)

    #: Signal emitted when a new :class:`LinkItem` has been added to the
    #: scene.
    link_item_added = Signal(object)

    #: Signal emitted when a :class:`LinkItem` has been removed.
    link_item_removed = Signal(object)

    #: Signal emitted when a :class:`Annotation` item has been added.
    annotation_added = Signal(object)

    #: Signal emitted when a :class:`Annotation` item has been removed.
    annotation_removed = Signal(object)

    #: Signal emitted when the position of a :class:`NodeItem` has changed.
    node_item_position_changed = Signal(object, QPointF)

    #: Signal emitted when an :class:`NodeItem` has been double clicked.
    node_item_double_clicked = Signal(object)

    #: An node item has been activated (clicked)
    node_item_activated = Signal(object)

    #: An node item has been hovered
    node_item_hovered = Signal(object)

    #: Link item has been hovered
    link_item_hovered = Signal(object)

    def __init__(self, *args, **kwargs):
        QGraphicsScene.__init__(self, *args, **kwargs)

        self.scheme = None
        self.registry = None

        # All node items
        self.__node_items = []
        # Mapping from SchemeNodes to canvas items
        self.__item_for_node = {}
        # All link items
        self.__link_items = []
        # Mapping from SchemeLinks to canvas items.
        self.__item_for_link = {}

        # All annotation items
        self.__annotation_items = []
        # Mapping from SchemeAnnotations to canvas items.
        self.__item_for_annotation = {}

        # Is the scene editable
        self.editable = True

        # Anchor Layout
        self.__anchor_layout = AnchorLayout()
        self.addItem(self.__anchor_layout)

        self.__channel_names_visible = True
        self.__node_animation_enabled = True

        self.user_interaction_handler = None

        self.activated_mapper = QSignalMapper(self)
        self.activated_mapper.mapped[QObject].connect(
            lambda node: self.node_item_activated.emit(node)
        )
        self.hovered_mapper = QSignalMapper(self)
        self.hovered_mapper.mapped[QObject].connect(
            lambda node: self.node_item_hovered.emit(node)
        )
        self.position_change_mapper = QSignalMapper(self)
        self.position_change_mapper.mapped[QObject].connect(
            self._on_position_change
        )
        log.info("'%s' intitialized." % self)

    def clear_scene(self):
        """
        Clear (reset) the scene.
        """
        if self.scheme is not None:
            self.scheme.node_added.disconnect(self.add_node)
            self.scheme.node_removed.disconnect(self.remove_node)

            self.scheme.link_added.disconnect(self.add_link)
            self.scheme.link_removed.disconnect(self.remove_link)

            self.scheme.annotation_added.disconnect(self.add_annotation)
            self.scheme.annotation_removed.disconnect(self.remove_annotation)

            self.scheme.node_state_changed.disconnect(
                self.on_widget_state_change
            )
            self.scheme.channel_state_changed.disconnect(
                self.on_link_state_change
            )

            # Remove all items to make sure all signals from scheme items
            # to canvas items are disconnected.

            for annot in self.scheme.annotations:
                if annot in self.__item_for_annotation:
                    self.remove_annotation(annot)

            for link in self.scheme.links:
                if link in self.__item_for_link:
                    self.remove_link(link)

            for node in self.scheme.nodes:
                if node in self.__item_for_node:
                    self.remove_node(node)

        self.scheme = None
        self.__node_items = []
        self.__item_for_node = {}
        self.__link_items = []
        self.__item_for_link = {}
        self.__annotation_items = []
        self.__item_for_annotation = {}

        self.__anchor_layout.deleteLater()

        self.user_interaction_handler = None

        self.clear()
        log.info("'%s' cleared." % self)

    def set_scheme(self, scheme):
        """
        Set the scheme to display. Populates the scene with nodes and links
        already in the scheme. Any further change to the scheme will be
        reflected in the scene.

        Parameters
        ----------
        scheme : :class:`~.scheme.Scheme`

        """
        if self.scheme is not None:
            # Clear the old scheme
            self.clear_scene()

        log.info("Setting scheme '%s' on '%s'" % (scheme, self))

        self.scheme = scheme
        if self.scheme is not None:
            self.scheme.node_added.connect(self.add_node)
            self.scheme.node_removed.connect(self.remove_node)

            self.scheme.link_added.connect(self.add_link)
            self.scheme.link_removed.connect(self.remove_link)

            self.scheme.annotation_added.connect(self.add_annotation)
            self.scheme.annotation_removed.connect(self.remove_annotation)

            self.scheme.node_state_changed.connect(
                self.on_widget_state_change
            )
            self.scheme.channel_state_changed.connect(
                self.on_link_state_change
            )

            self.scheme.topology_changed.connect(self.on_scheme_change)

        for node in scheme.nodes:
            self.add_node(node)

        for link in scheme.links:
            self.add_link(link)

        for annot in scheme.annotations:
            self.add_annotation(annot)

    def set_registry(self, registry):
        """
        Set the widget registry.
        """
        # TODO: Remove/Deprecate. Is used only to get the category/background
        # color. That should be part of the SchemeNode/WidgetDescription.
        log.info("Setting registry '%s on '%s'." % (registry, self))
        self.registry = registry

    def set_anchor_layout(self, layout):
        """
        Set an :class:`~.layout.AnchorLayout`
        """
        if self.__anchor_layout != layout:
            if self.__anchor_layout:
                self.__anchor_layout.deleteLater()
                self.__anchor_layout = None

            self.__anchor_layout = layout

    def anchor_layout(self):
        """
        Return the anchor layout instance.
        """
        return self.__anchor_layout

    def set_channel_names_visible(self, visible):
        """
        Set the channel names visibility.
        """
        self.__channel_names_visible = visible
        for link in self.__link_items:
            link.setChannelNamesVisible(visible)

    def channel_names_visible(self):
        """
        Return the channel names visibility state.
        """
        return self.__channel_names_visible

    def set_node_animation_enabled(self, enabled):
        """
        Set node animation enabled state.
        """
        if self.__node_animation_enabled != enabled:
            self.__node_animation_enabled = enabled

            for node in self.__node_items:
                node.setAnimationEnabled(enabled)

    def add_node_item(self, item):
        """
        Add a :class:`.NodeItem` instance to the scene.
        """
        if item in self.__node_items:
            raise ValueError("%r is already in the scene." % item)

        if item.pos().isNull():
            if self.__node_items:
                pos = self.__node_items[-1].pos() + QPointF(150, 0)
            else:
                pos = QPointF(150, 150)

            item.setPos(pos)

        item.setFont(self.font())

        # Set signal mappings
        self.activated_mapper.setMapping(item, item)
        item.activated.connect(self.activated_mapper.map)

        self.hovered_mapper.setMapping(item, item)
        item.hovered.connect(self.hovered_mapper.map)

        self.position_change_mapper.setMapping(item, item)
        item.positionChanged.connect(self.position_change_mapper.map)

        self.addItem(item)

        self.__node_items.append(item)

        self.node_item_added.emit(item)

        log.info("Added item '%s' to '%s'" % (item, self))
        return item

    def add_node(self, node):
        """
        Add and return a default constructed :class:`.NodeItem` for a
        :class:`SchemeNode` instance `node`. If the `node` is already in
        the scene do nothing and just return its item.

        """
        if node in self.__item_for_node:
            # Already added
            return self.__item_for_node[node]

        item = self.new_node_item(node.description)

        if node.position:
            pos = QPointF(*node.position)
            item.setPos(pos)

        item.setTitle(node.title)
        item.setProcessingState(node.processing_state)
        item.setProgress(node.progress)

        for message in node.state_messages():
            item.setStateMessage(message)

        item.setStatusMessage(node.status_message())

        self.__item_for_node[node] = item

        node.position_changed.connect(self.__on_node_pos_changed)
        node.title_changed.connect(item.setTitle)
        node.progress_changed.connect(item.setProgress)
        node.processing_state_changed.connect(item.setProcessingState)
        node.state_message_changed.connect(item.setStateMessage)
        node.status_message_changed.connect(item.setStatusMessage)

        return self.add_node_item(item)

    def new_node_item(self, widget_desc, category_desc=None):
        """
        Construct an new :class:`.NodeItem` from a `WidgetDescription`.
        Optionally also set `CategoryDescription`.

        """
        item = items.NodeItem()
        item.setWidgetDescription(widget_desc)

        if category_desc is None and self.registry and widget_desc.category:
            category_desc = self.registry.category(widget_desc.category)

        if category_desc is None and self.registry is not None:
            try:
                category_desc = self.registry.category(widget_desc.category)
            except KeyError:
                pass

        if category_desc is not None:
            item.setWidgetCategory(category_desc)

        item.setAnimationEnabled(self.__node_animation_enabled)
        return item

    def remove_node_item(self, item):
        """
        Remove `item` (:class:`.NodeItem`) from the scene.
        """
        self.activated_mapper.removeMappings(item)
        self.hovered_mapper.removeMappings(item)
        self.position_change_mapper.removeMappings(item)

        item.hide()
        self.removeItem(item)
        self.__node_items.remove(item)

        self.node_item_removed.emit(item)

        log.info("Removed item '%s' from '%s'" % (item, self))

    def remove_node(self, node):
        """
        Remove the :class:`.NodeItem` instance that was previously
        constructed for a :class:`SchemeNode` `node` using the `add_node`
        method.

        """
        item = self.__item_for_node.pop(node)

        node.position_changed.disconnect(self.__on_node_pos_changed)
        node.title_changed.disconnect(item.setTitle)
        node.progress_changed.disconnect(item.setProgress)
        node.processing_state_changed.disconnect(item.setProcessingState)
        node.state_message_changed.disconnect(item.setStateMessage)

        self.remove_node_item(item)

    def node_items(self):
        """
        Return all :class:`.NodeItem` instances in the scene.
        """
        return list(self.__node_items)

    def add_link_item(self, item):
        """
        Add a link (:class:`.LinkItem`) to the scene.
        """
        if item.scene() is not self:
            self.addItem(item)

        item.setFont(self.font())
        self.__link_items.append(item)

        self.link_item_added.emit(item)

        log.info("Added link %r -> %r to '%s'" % \
                 (item.sourceItem.title(), item.sinkItem.title(), self))

        self.__anchor_layout.invalidateLink(item)

        return item

    def add_link(self, scheme_link):
        """
        Create and add a :class:`.LinkItem` instance for a
        :class:`SchemeLink` instance. If the link is already in the scene
        do nothing and just return its :class:`.LinkItem`.

        """
        if scheme_link in self.__item_for_link:
            return self.__item_for_link[scheme_link]

        source = self.__item_for_node[scheme_link.source_node]
        sink = self.__item_for_node[scheme_link.sink_node]

        item = self.new_link_item(source, scheme_link.source_channel,
                                  sink, scheme_link.sink_channel)

        item.setEnabled(scheme_link.enabled)
        scheme_link.enabled_changed.connect(item.setEnabled)

        if scheme_link.is_dynamic():
            item.setDynamic(True)
            item.setDynamicEnabled(scheme_link.dynamic_enabled)
            scheme_link.dynamic_enabled_changed.connect(item.setDynamicEnabled)

        self.add_link_item(item)
        self.__item_for_link[scheme_link] = item
        return item

    def new_link_item(self, source_item, source_channel,
                      sink_item, sink_channel):
        """
        Construct and return a new :class:`.LinkItem`
        """
        item = items.LinkItem()
        item.setSourceItem(source_item)
        item.setSinkItem(sink_item)

        def channel_name(channel):
            if isinstance(channel, str):
                return channel
            else:
                return channel.name

        source_name = channel_name(source_channel)
        sink_name = channel_name(sink_channel)

        fmt = "<b>{0}</b>&nbsp; \u2192 &nbsp;<b>{1}</b>"
        item.setToolTip(
            fmt.format(escape(source_name),
                       escape(sink_name))
        )

        item.setSourceName(source_name)
        item.setSinkName(sink_name)
        item.setChannelNamesVisible(self.__channel_names_visible)

        return item

    def remove_link_item(self, item):
        """
        Remove a link (:class:`.LinkItem`) from the scene.
        """
        # Invalidate the anchor layout.
        self.__anchor_layout.invalidateAnchorItem(
            item.sourceItem.outputAnchorItem
        )
        self.__anchor_layout.invalidateAnchorItem(
            item.sinkItem.inputAnchorItem
        )

        self.__link_items.remove(item)

        # Remove the anchor points.
        item.removeLink()
        self.removeItem(item)

        self.link_item_removed.emit(item)

        log.info("Removed link '%s' from '%s'" % (item, self))

        return item

    def remove_link(self, scheme_link):
        """
        Remove a :class:`.LinkItem` instance that was previously constructed
        for a :class:`SchemeLink` instance `link` using the `add_link` method.

        """
        item = self.__item_for_link.pop(scheme_link)
        scheme_link.enabled_changed.disconnect(item.setEnabled)

        if scheme_link.is_dynamic():
            scheme_link.dynamic_enabled_changed.disconnect(
                item.setDynamicEnabled
            )

        self.remove_link_item(item)

    def link_items(self):
        """
        Return all :class:`.LinkItem`\s in the scene.
        """
        return list(self.__link_items)

    def add_annotation_item(self, annotation):
        """
        Add an :class:`.Annotation` item to the scene.
        """
        self.__annotation_items.append(annotation)
        self.addItem(annotation)
        self.annotation_added.emit(annotation)
        return annotation

    def add_annotation(self, scheme_annot):
        """
        Create a new item for :class:`SchemeAnnotation` and add it
        to the scene. If the `scheme_annot` is already in the scene do
        nothing and just return its item.

        """
        if scheme_annot in self.__item_for_annotation:
            # Already added
            return self.__item_for_annotation[scheme_annot]

        if isinstance(scheme_annot, scheme.SchemeTextAnnotation):
            item = items.TextAnnotation()
            item.setPlainText(scheme_annot.text)
            x, y, w, h = scheme_annot.rect
            item.setPos(x, y)
            item.resize(w, h)
            item.setTextInteractionFlags(Qt.TextEditorInteraction)

            font = font_from_dict(scheme_annot.font, item.font())
            item.setFont(font)
            scheme_annot.text_changed.connect(item.setPlainText)

        elif isinstance(scheme_annot, scheme.SchemeArrowAnnotation):
            item = items.ArrowAnnotation()
            start, end = scheme_annot.start_pos, scheme_annot.end_pos
            item.setLine(QLineF(QPointF(*start), QPointF(*end)))
            item.setColor(QColor(scheme_annot.color))

        scheme_annot.geometry_changed.connect(
            self.__on_scheme_annot_geometry_change
        )

        self.add_annotation_item(item)
        self.__item_for_annotation[scheme_annot] = item

        return item

    def remove_annotation_item(self, annotation):
        """
        Remove an :class:`.Annotation` instance from the scene.

        """
        self.__annotation_items.remove(annotation)
        self.removeItem(annotation)
        self.annotation_removed.emit(annotation)

    def remove_annotation(self, scheme_annotation):
        """
        Remove an :class:`.Annotation` instance that was previously added
        using :func:`add_anotation`.

        """
        item = self.__item_for_annotation.pop(scheme_annotation)

        scheme_annotation.geometry_changed.disconnect(
            self.__on_scheme_annot_geometry_change
        )

        if isinstance(scheme_annotation, scheme.SchemeTextAnnotation):
            scheme_annotation.text_changed.disconnect(
                item.setPlainText
            )

        self.remove_annotation_item(item)

    def annotation_items(self):
        """
        Return all :class:`.Annotation` items in the scene.
        """
        return self.__annotation_items

    def item_for_annotation(self, scheme_annotation):
        return self.__item_for_annotation[scheme_annotation]

    def annotation_for_item(self, item):
        rev = dict(reversed(item) \
                   for item in self.__item_for_annotation.items())
        return rev[item]

    def commit_scheme_node(self, node):
        """
        Commit the `node` into the scheme.
        """
        if not self.editable:
            raise Exception("Scheme not editable.")

        if node not in self.__item_for_node:
            raise ValueError("No 'NodeItem' for node.")

        item = self.__item_for_node[node]

        try:
            self.scheme.add_node(node)
        except Exception:
            log.error("An error occurred while committing node '%s'",
                      node, exc_info=True)
            # Cleanup (remove the node item)
            self.remove_node_item(item)
            raise

        log.info("Commited node '%s' from '%s' to '%s'" % \
                 (node, self, self.scheme))

    def commit_scheme_link(self, link):
        """
        Commit a scheme link.
        """
        if not self.editable:
            raise Exception("Scheme not editable")

        if link not in self.__item_for_link:
            raise ValueError("No 'LinkItem' for link.")

        self.scheme.add_link(link)
        log.info("Commited link '%s' from '%s' to '%s'" % \
                 (link, self, self.scheme))

    def node_for_item(self, item):
        """
        Return the `SchemeNode` for the `item`.
        """
        rev = dict([(v, k) for k, v in self.__item_for_node.items()])
        return rev[item]

    def item_for_node(self, node):
        """
        Return the :class:`NodeItem` instance for a :class:`SchemeNode`.
        """
        return self.__item_for_node[node]

    def link_for_item(self, item):
        """
        Return the `SchemeLink for `item` (:class:`LinkItem`).
        """
        rev = dict([(v, k) for k, v in self.__item_for_link.items()])
        return rev[item]

    def item_for_link(self, link):
        """
        Return the :class:`LinkItem` for a :class:`SchemeLink`
        """
        return self.__item_for_link[link]

    def selected_node_items(self):
        """
        Return the selected :class:`NodeItem`'s.
        """
        return [item for item in self.__node_items if item.isSelected()]

    def selected_annotation_items(self):
        """
        Return the selected :class:`Annotation`'s
        """
        return [item for item in self.__annotation_items if item.isSelected()]

    def node_links(self, node_item):
        """
        Return all links from the `node_item` (:class:`NodeItem`).
        """
        return self.node_output_links(node_item) + \
               self.node_input_links(node_item)

    def node_output_links(self, node_item):
        """
        Return a list of all output links from `node_item`.
        """
        return [link for link in self.__link_items
                if link.sourceItem == node_item]

    def node_input_links(self, node_item):
        """
        Return a list of all input links for `node_item`.
        """
        return [link for link in self.__link_items
                if link.sinkItem == node_item]

    def neighbor_nodes(self, node_item):
        """
        Return a list of `node_item`'s (class:`NodeItem`) neighbor nodes.
        """
        neighbors = list(map(attrgetter("sourceItem"),
                             self.node_input_links(node_item)))

        neighbors.extend(map(attrgetter("sinkItem"),
                             self.node_output_links(node_item)))
        return neighbors

    def on_widget_state_change(self, widget, state):
        pass

    def on_link_state_change(self, link, state):
        pass

    def on_scheme_change(self, ):
        pass

    def _on_position_change(self, item):
        # Invalidate the anchor point layout and schedule a layout.
        self.__anchor_layout.invalidateNode(item)

        self.node_item_position_changed.emit(item, item.pos())

    def __on_node_pos_changed(self, pos):
        node = self.sender()
        item = self.__item_for_node[node]
        item.setPos(*pos)

    def __on_scheme_annot_geometry_change(self):
        annot = self.sender()
        item = self.__item_for_annotation[annot]
        if isinstance(annot, scheme.SchemeTextAnnotation):
            item.setGeometry(QRectF(*annot.rect))
        elif isinstance(annot, scheme.SchemeArrowAnnotation):
            p1 = item.mapFromScene(QPointF(*annot.start_pos))
            p2 = item.mapFromScene(QPointF(*annot.end_pos))
            item.setLine(QLineF(p1, p2))
        else:
            pass

    def item_at(self, pos, type_or_tuple=None, buttons=0):
        """Return the item at `pos` that is an instance of the specified
        type (`type_or_tuple`). If `buttons` (`Qt.MouseButtons`) is given
        only return the item if it is the top level item that would
        accept any of the buttons (`QGraphicsItem.acceptedMouseButtons`).

        """
        rect = QRectF(pos, QSizeF(1, 1))
        items = self.items(rect)

        if buttons:
            items = itertools.dropwhile(
                lambda item: not item.acceptedMouseButtons() & buttons,
                items
            )
            items = list(items)[:1]

        if type_or_tuple:
            items = [i for i in items if isinstance(i, type_or_tuple)]

        return items[0] if items else None

    if PYQT_VERSION < 0x40900:
        # For QGraphicsObject subclasses items, itemAt ... return a
        # QGraphicsItem wrapper instance and not the actual class instance.
        def itemAt(self, *args, **kwargs):
            item = QGraphicsScene.itemAt(self, *args, **kwargs)
            return toGraphicsObjectIfPossible(item)

        def items(self, *args, **kwargs):
            items = QGraphicsScene.items(self, *args, **kwargs)
            return list(map(toGraphicsObjectIfPossible, items))

        def selectedItems(self, *args, **kwargs):
            return list(map(toGraphicsObjectIfPossible,
                       QGraphicsScene.selectedItems(self, *args, **kwargs)))

        def collidingItems(self, *args, **kwargs):
            return list(map(toGraphicsObjectIfPossible,
                       QGraphicsScene.collidingItems(self, *args, **kwargs)))

        def focusItem(self, *args, **kwargs):
            item = QGraphicsScene.focusItem(self, *args, **kwargs)
            return toGraphicsObjectIfPossible(item)

        def mouseGrabberItem(self, *args, **kwargs):
            item = QGraphicsScene.mouseGrabberItem(self, *args, **kwargs)
            return toGraphicsObjectIfPossible(item)

    def mousePressEvent(self, event):
        if self.user_interaction_handler and \
                self.user_interaction_handler.mousePressEvent(event):
            return

        # Right (context) click on the node item. If the widget is not
        # in the current selection then select the widget (only the widget).
        # Else simply return and let customContextMenuReqested signal
        # handle it
        shape_item = self.item_at(event.scenePos(), items.NodeItem)
        if shape_item and event.button() == Qt.RightButton and \
                shape_item.flags() & QGraphicsItem.ItemIsSelectable:
            if not shape_item.isSelected():
                self.clearSelection()
                shape_item.setSelected(True)

        return QGraphicsScene.mousePressEvent(self, event)

    def mouseMoveEvent(self, event):
        if self.user_interaction_handler and \
                self.user_interaction_handler.mouseMoveEvent(event):
            return

        return QGraphicsScene.mouseMoveEvent(self, event)

    def mouseReleaseEvent(self, event):
        if self.user_interaction_handler and \
                self.user_interaction_handler.mouseReleaseEvent(event):
            return
        return QGraphicsScene.mouseReleaseEvent(self, event)

    def mouseDoubleClickEvent(self, event):
        if self.user_interaction_handler and \
                self.user_interaction_handler.mouseDoubleClickEvent(event):
            return

        return QGraphicsScene.mouseDoubleClickEvent(self, event)

    def keyPressEvent(self, event):
        if self.user_interaction_handler and \
                self.user_interaction_handler.keyPressEvent(event):
            return
        return QGraphicsScene.keyPressEvent(self, event)

    def keyReleaseEvent(self, event):
        if self.user_interaction_handler and \
                self.user_interaction_handler.keyReleaseEvent(event):
            return
        return QGraphicsScene.keyReleaseEvent(self, event)

    def set_user_interaction_handler(self, handler):
        if self.user_interaction_handler and \
                not self.user_interaction_handler.isFinished():
            self.user_interaction_handler.cancel()

        log.info("Setting interaction '%s' to '%s'" % (handler, self))

        self.user_interaction_handler = handler
        if handler:
            handler.start()

    def event(self, event):
        # TODO: change the base class of Node/LinkItem to QGraphicsWidget.
        # It already handles font changes.
        if event.type() == QEvent.FontChange:
            self.__update_font()

        return QGraphicsScene.event(self, event)

    def __update_font(self):
        font = self.font()
        for item in self.__node_items + self.__link_items:
            item.setFont(font)

    def __str__(self):
        return "%s(objectName=%r, ...)" % \
                (type(self).__name__, str(self.objectName()))
예제 #6
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])