Exemplo n.º 1
0
class MainWindow(QMainWindow):
    def __init__(self, labeltool, parent=None):
        QMainWindow.__init__(self, parent)

        self.idletimer = QTimer()
        self.loader = None

        self.labeltool = labeltool
        self.setupGui()
        self.loadApplicationSettings()
        self.onAnnotationsLoaded()

    # Slots
    def onPluginLoaded(self, action):
        self.ui.menuPlugins.addAction(action)

    def onStatusMessage(self, message=''):
        self.statusBar().showMessage(message, 5000)

    def onModelDirtyChanged(self, dirty):
        postfix = "[+]" if dirty else ""
        if self.labeltool.getCurrentFilename() is not None:
            self.setWindowTitle("%s - %s %s" % \
                (APP_NAME, QFileInfo(self.labeltool.getCurrentFilename()).fileName(), postfix))
        else:
            self.setWindowTitle("%s - Unnamed %s" % (APP_NAME, postfix))

    def onMousePositionChanged(self, x, y):
        self.posinfo.setText("%d, %d" % (x, y))

    def startBackgroundLoading(self):
        self.stopBackgroundLoading(forced=True)
        self.loader = BackgroundLoader(self.labeltool.model(),
                                       self.statusBar(), self.sb_progress)
        self.idletimer.timeout.connect(self.loader.load)
        self.loader.finished.connect(self.stopBackgroundLoading)
        self.statusBar().addWidget(self.sb_progress)
        self.sb_progress.show()
        self.idletimer.start()

    def stopBackgroundLoading(self, forced=False):
        if not forced:
            self.statusBar().showMessage("Background loading finished", 5000)
        self.idletimer.stop()
        if self.loader is not None:
            self.idletimer.timeout.disconnect(self.loader.load)
            self.statusBar().removeWidget(self.sb_progress)
            self.loader = None

    def onAnnotationsLoaded(self):
        self.labeltool.model().dirtyChanged.connect(self.onModelDirtyChanged)
        self.onModelDirtyChanged(self.labeltool.model().dirty())
        self.treeview.setModel(self.labeltool.model())
        self.scene.setModel(self.labeltool.model())
        self.selectionmodel = QItemSelectionModel(self.labeltool.model())
        self.treeview.setSelectionModel(self.selectionmodel)
        self.treeview.selectionModel().currentChanged.connect(
            self.labeltool.setCurrentImage)
        self.property_editor.onModelChanged(self.labeltool.model())
        self.startBackgroundLoading()

    def onCurrentImageChanged(self):
        new_image = self.labeltool.currentImage()
        self.scene.setCurrentImage(new_image)
        self.onFitToWindowModeChanged()
        self.treeview.scrollTo(new_image.index())

        img = self.labeltool.getImage(new_image)

        if img == None:
            self.controls.setFilename("")
            self.selectionmodel.setCurrentIndex(
                new_image.index(),
                QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows)
            return

        h = img.shape[0]
        w = img.shape[1]
        self.image_resolution.setText("%dx%d" % (w, h))

        # TODO: This info should be obtained from AnnotationModel or LabelTool
        if isinstance(new_image, FrameModelItem):
            self.controls.setFrameNumAndTimestamp(new_image.framenum(),
                                                  new_image.timestamp())
        elif isinstance(new_image, ImageFileModelItem):
            self.controls.setFilename(os.path.basename(new_image['filename']))

        self.selectionmodel.setCurrentIndex(
            new_image.index(),
            QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows)

    def onFitToWindowModeChanged(self):
        if self.options["Fit-to-window mode"].isChecked():
            self.view.fitInView()

    def onEnumerateCornersModeChanged(self):
        if self.options["Enumerate-corners mode"].isChecked():
            self.scene.enumerateCorners()
            self.onCurrentImageChanged()
        else:
            self.scene.removeCorners()
            self.onCurrentImageChanged()

    def onCopyAnnotationsModeChanged(self):
        if self.annotationMenu["Copy from previous"].isChecked():
            self.copyAnnotations.copy()
            self.annotationMenu["Copy from previous"].setChecked(False)

    def onInterpolateRangeModeChanged(self):
        if self.annotationMenu["Interpolate range"].isChecked():
            self.interpolateRange.interpolateRange()
            self.annotationMenu["Interpolate range"].setChecked(False)

    def onScaleChanged(self, scale):
        self.zoominfo.setText("%.2f%%" % (100 * scale, ))

    def initShortcuts(self, HOTKEYS):
        self.shortcuts = []

        for hotkey in HOTKEYS:
            assert len(hotkey) >= 2
            key = hotkey[0]
            fun = hotkey[1]
            desc = ""
            if len(hotkey) > 2:
                desc = hotkey[2]
            if type(fun) == str:
                fun = import_callable(fun)

            hk = QAction(desc, self)
            hk.setShortcut(QKeySequence(key))
            hk.setEnabled(True)
            if hasattr(fun, '__call__'):
                hk.triggered.connect(bind(fun, self.labeltool))
            else:
                hk.triggered.connect(
                    compose_noargs([bind(f, self.labeltool) for f in fun]))
            self.ui.menuShortcuts.addAction(hk)
            self.shortcuts.append(hk)

    def initOptions(self):
        self.options = {}
        for o in ["Fit-to-window mode"]:
            action = QAction(o, self)
            action.setCheckable(True)
            self.ui.menuOptions.addAction(action)
            self.options[o] = action

        for o in ["Enumerate-corners mode"]:
            action = QAction(o, self)
            action.setCheckable(True)
            self.ui.menuOptions.addAction(action)
            self.options[o] = action

    def initAnnotationMenu(self):
        self.annotationMenu = {}
        for a in ["Copy from previous"]:
            action = QAction(a, self)
            action.setCheckable(True)
            self.ui.menuAnnotation.addAction(action)
            self.annotationMenu[a] = action

        for a in ["Interpolate range"]:
            action = QAction(a, self)
            action.setCheckable(True)
            self.ui.menuAnnotation.addAction(action)
            self.annotationMenu[a] = action

    ###
    ### GUI/Application setup
    ###___________________________________________________________________________________________
    def setupGui(self):
        self.ui = uic.loadUi(os.path.join(GUIDIR, "labeltool.ui"), self)

        # get inserters and items from labels
        # FIXME for handling the new-style config correctly
        inserters = dict([
            (label['attributes']['class'], label['inserter'])
            for label in config.LABELS
            if 'class' in label.get('attributes', {}) and 'inserter' in label
        ])
        items = dict([
            (label['attributes']['class'], label['item'])
            for label in config.LABELS
            if 'class' in label.get('attributes', {}) and 'item' in label
        ])

        # Property Editor
        self.property_editor = PropertyEditor(config.LABELS)
        self.ui.dockProperties.setWidget(self.property_editor)

        # Scene
        self.scene = AnnotationScene(self.labeltool,
                                     items=items,
                                     inserters=inserters)
        self.property_editor.insertionModeStarted.connect(
            self.scene.onInsertionModeStarted)
        self.property_editor.insertionModeEnded.connect(
            self.scene.onInsertionModeEnded)

        # SceneView
        self.view = GraphicsView(self)
        self.view.setSizePolicy(QSizePolicy.MinimumExpanding,
                                QSizePolicy.MinimumExpanding)
        self.view.setScene(self.scene)

        self.central_widget = QWidget()
        self.central_layout = QVBoxLayout()
        self.controls = ControlButtonWidget()
        self.controls.back_button.clicked.connect(self.labeltool.gotoPrevious)
        self.controls.forward_button.clicked.connect(self.labeltool.gotoNext)

        self.central_layout.addWidget(self.controls)
        self.central_layout.addWidget(self.view)
        self.central_widget.setLayout(self.central_layout)
        self.setCentralWidget(self.central_widget)

        self.initShortcuts(config.HOTKEYS)
        self.initOptions()
        self.initAnnotationMenu()

        self.treeview = AnnotationTreeView()
        self.treeview.setSizePolicy(QSizePolicy.MinimumExpanding,
                                    QSizePolicy.Preferred)
        self.ui.dockAnnotations.setWidget(self.treeview)

        self.scene.selectionChanged.connect(self.scene.onSelectionChanged)
        self.treeview.selectedItemsChanged.connect(
            self.scene.onSelectionChangedInTreeView)

        self.posinfo = QLabel("-1, -1")
        self.posinfo.setFrameStyle(QFrame.StyledPanel)
        self.statusBar().addPermanentWidget(self.posinfo)
        self.scene.mousePositionChanged.connect(self.onMousePositionChanged)

        self.image_resolution = QLabel("[no image]")
        self.image_resolution.setFrameStyle(QFrame.StyledPanel)
        self.statusBar().addPermanentWidget(self.image_resolution)

        self.zoominfo = QLabel()
        self.zoominfo.setFrameStyle(QFrame.StyledPanel)
        self.statusBar().addPermanentWidget(self.zoominfo)
        self.view.scaleChanged.connect(self.onScaleChanged)
        self.onScaleChanged(self.view.getScale())

        self.sb_progress = QProgressBar()

        # View menu
        self.ui.menu_Views.addAction(self.ui.dockProperties.toggleViewAction())
        self.ui.menu_Views.addAction(
            self.ui.dockAnnotations.toggleViewAction())

        # Annotation menu
        self.copyAnnotations = CopyAnnotations(self.labeltool)
        self.interpolateRange = InterpolateRange(self.labeltool)

        # Show the UI.  It is important that this comes *after* the above
        # adding of custom widgets, especially the central widget.  Otherwise the
        # dock widgets would be far to large.
        self.ui.show()

        ## connect action signals
        self.connectActions()

    def connectActions(self):
        ## File menu
        self.ui.actionNew.triggered.connect(self.fileNew)
        self.ui.actionOpen.triggered.connect(self.fileOpen)
        self.ui.actionSave.triggered.connect(self.fileSave)
        self.ui.actionSave_As.triggered.connect(self.fileSaveAs)
        self.ui.actionExit.triggered.connect(self.close)

        ## View menu
        self.ui.actionLocked.toggled.connect(self.onViewsLockedChanged)

        ## Help menu
        self.ui.action_About.triggered.connect(self.about)

        ## Navigation
        self.ui.action_Add_Image.triggered.connect(self.addMediaFile)
        self.ui.actionNext.triggered.connect(self.labeltool.gotoNext)
        self.ui.actionPrevious.triggered.connect(self.labeltool.gotoPrevious)
        self.ui.actionZoom_In.triggered.connect(
            functools.partial(self.view.setScaleRelative, 1.2))
        self.ui.actionZoom_Out.triggered.connect(
            functools.partial(self.view.setScaleRelative, 1 / 1.2))

        ## Connections to LabelTool
        self.labeltool.pluginLoaded.connect(self.onPluginLoaded)
        self.labeltool.statusMessage.connect(self.onStatusMessage)
        self.labeltool.annotationsLoaded.connect(self.onAnnotationsLoaded)
        self.labeltool.currentImageChanged.connect(self.onCurrentImageChanged)

        ## options menu
        self.options["Fit-to-window mode"].changed.connect(
            self.onFitToWindowModeChanged)
        self.options["Enumerate-corners mode"].changed.connect(
            self.onEnumerateCornersModeChanged)

        ## annotation menu
        self.annotationMenu["Copy from previous"].changed.connect(
            self.onCopyAnnotationsModeChanged)
        self.annotationMenu["Interpolate range"].changed.connect(
            self.onInterpolateRangeModeChanged)

    def loadApplicationSettings(self):
        settings = QSettings()
        size = settings.value("MainWindow/Size", QSize(800, 600))
        pos = settings.value("MainWindow/Position", QPoint(10, 10))
        state = settings.value("MainWindow/State")
        locked = settings.value("MainWindow/ViewsLocked", False)
        if isinstance(size, QVariant): size = size.toSize()
        if isinstance(pos, QVariant): pos = pos.toPoint()
        if isinstance(state, QVariant): state = state.toByteArray()
        if isinstance(locked, QVariant): locked = locked.toBool()
        self.resize(size)
        self.move(pos)
        if state is not None:
            self.restoreState(state)
        self.ui.actionLocked.setChecked(bool(locked))

    def saveApplicationSettings(self):
        settings = QSettings()
        settings.setValue("MainWindow/Size", self.size())
        settings.setValue("MainWindow/Position", self.pos())
        settings.setValue("MainWindow/State", self.saveState())
        settings.setValue("MainWindow/ViewsLocked",
                          self.ui.actionLocked.isChecked())
        if self.labeltool.getCurrentFilename() is not None:
            filename = self.labeltool.getCurrentFilename()
        else:
            filename = None
        settings.setValue("LastFile", filename)

    def okToContinue(self):
        if self.labeltool.model().dirty():
            reply = QMessageBox.question(
                self, "%s - Unsaved Changes" % (APP_NAME),
                "Save unsaved changes?",
                QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel)
            if reply == QMessageBox.Cancel:
                return False
            elif reply == QMessageBox.Yes:
                return self.fileSave()
        return True

    def fileNew(self):
        if self.okToContinue():
            self.labeltool.clearAnnotations()

    def fileOpen(self):
        if not self.okToContinue():
            return
        path = '.'
        filename = self.labeltool.getCurrentFilename()
        if (filename is not None) and (len(filename) > 0):
            path = QFileInfo(filename).path()

        format_str = ' '.join(self.labeltool.getAnnotationFilePatterns())
        fname = QFileDialog.getOpenFileName(
            self, "%s - Load Annotations" % APP_NAME, path,
            "%s annotation files (%s)" % (APP_NAME, format_str))
        if len(str(fname)) > 0:
            self.labeltool.loadAnnotations(fname)

    def fileSave(self):
        filename = self.labeltool.getCurrentFilename()
        if filename is None:
            return self.fileSaveAs()
        return self.labeltool.saveAnnotations(filename)

    def fileSaveAs(self):
        fname = '.'  # self.annotations.filename() or '.'
        format_str = ' '.join(self.labeltool.getAnnotationFilePatterns())
        fname = QFileDialog.getSaveFileName(
            self, "%s - Save Annotations" % APP_NAME, fname,
            "%s annotation files (%s)" % (APP_NAME, format_str))

        if len(str(fname)) > 0:
            return self.labeltool.saveAnnotations(str(fname))
        return False

    def addMediaFile(self):
        filename = self.labeltool.getCurrentFilename()

        image_types = [
            '*.jpg', '*.bmp', '*.png', '*.pgm', '*.ppm', '*.ppm', '*.tif',
            '*.gif'
        ]
        video_types = [
            '*.mp4', '*.mpg', '*.mpeg', '*.avi', '*.mov', '*.vob', '*.json'
        ]
        format_str = ' '.join(image_types + video_types)
        fnames = QFileDialog.getOpenFileNames(
            self, "%s - Add Media File" % APP_NAME, path,
            "Media files (%s)" % (format_str, ))

        item = None
        numFiles = len(fnames)
        progress_bar = QProgressDialog('Importing files...', 'Cancel import',
                                       0, numFiles, self)
        for fname, c in zip(fnames, range(numFiles)):
            if len(str(fname)) == 0:
                continue

            fname = str(fname)

            if os.path.isabs(fname):
                fname = os.path.relpath(fname)

            for pattern in image_types:
                if fnmatch.fnmatch(fname, pattern):
                    item = self.labeltool.addImageFile(fname)

            progress_bar.setValue(c)

        if item is None:
            item = self.labeltool.addVideoFile(fname, progress_bar)

        progress_bar.close()

        return item

    def onViewsLockedChanged(self, checked):
        features = QDockWidget.AllDockWidgetFeatures
        if checked:
            features = QDockWidget.NoDockWidgetFeatures

        self.ui.dockProperties.setFeatures(features)
        self.ui.dockAnnotations.setFeatures(features)

    ###
    ### global event handling
    ###______________________________________________________________________________
    def closeEvent(self, event):
        if self.okToContinue():
            self.saveApplicationSettings()
        else:
            event.ignore()

    def about(self):
        QMessageBox.about(
            self, "About %s" % APP_NAME, """<b>%s</b> version %s
             <p>This labeling application for computer vision research
             was developed at the CVHCI research group at KIT.
             <p>For more details, visit our homepage: <a href="%s">%s</a>""" %
            (APP_NAME, __version__, ORGANIZATION_DOMAIN, ORGANIZATION_DOMAIN))
Exemplo n.º 2
0
class MainWindow(QMainWindow):
    def __init__(self, labeltool, parent=None):
        QMainWindow.__init__(self, parent)

        self.idletimer = QTimer()
        self.loader = None

        self.labeltool = labeltool
        self.setupGui()
        self.loadApplicationSettings()
        self.onAnnotationsLoaded()

    # Slots
    def onPluginLoaded(self, action):
        self.ui.menuPlugins.addAction(action)

    def onStatusMessage(self, message=''):
        self.statusBar().showMessage(message, 5000)

    def onModelDirtyChanged(self, dirty):
        postfix = "[+]" if dirty else ""
        if self.labeltool.getCurrentFilename() is not None:
            self.setWindowTitle("%s - %s %s" % \
                (APP_NAME, QFileInfo(self.labeltool.getCurrentFilename()).fileName(), postfix))
        else:
            self.setWindowTitle("%s - Unnamed %s" % (APP_NAME, postfix))

    def onMousePositionChanged(self, x, y):
        self.posinfo.setText("%d, %d" % (x, y))

    def startBackgroundLoading(self):
        self.stopBackgroundLoading(forced=True)
        self.loader = BackgroundLoader(self.labeltool.model(), self.statusBar(), self.sb_progress)
        self.idletimer.timeout.connect(self.loader.load)
        self.loader.finished.connect(self.stopBackgroundLoading)
        self.statusBar().addWidget(self.sb_progress)
        self.sb_progress.show()
        self.idletimer.start()

    def stopBackgroundLoading(self, forced=False):
        if not forced:
            self.statusBar().showMessage("Background loading finished", 5000)
        self.idletimer.stop()
        if self.loader is not None:
            self.idletimer.timeout.disconnect(self.loader.load)
            self.statusBar().removeWidget(self.sb_progress)
            self.loader = None

    def onAnnotationsLoaded(self):
        self.labeltool.model().dirtyChanged.connect(self.onModelDirtyChanged)
        self.onModelDirtyChanged(self.labeltool.model().dirty())
        self.treeview.setModel(self.labeltool.model())
        self.scene.setModel(self.labeltool.model())
        self.selectionmodel = QItemSelectionModel(self.labeltool.model())
        self.treeview.setSelectionModel(self.selectionmodel)
        self.treeview.selectionModel().currentChanged.connect(self.labeltool.setCurrentImage)
        self.property_editor.onModelChanged(self.labeltool.model())
        self.startBackgroundLoading()

    def onCurrentImageChanged(self):
        new_image = self.labeltool.currentImage()
        self.scene.setCurrentImage(new_image)
        self.onFitToWindowModeChanged()
        self.treeview.scrollTo(new_image.index())

        img = self.labeltool.getImage(new_image)

        if img == None:
            self.controls.setFilename("")
            self.selectionmodel.setCurrentIndex(new_image.index(), QItemSelectionModel.ClearAndSelect|QItemSelectionModel.Rows)
            return

        h = img.shape[0]
        w = img.shape[1]
        self.image_resolution.setText("%dx%d" % (w, h))

        # TODO: This info should be obtained from AnnotationModel or LabelTool
        if isinstance(new_image, FrameModelItem):
            self.controls.setFrameNumAndTimestamp(new_image.framenum(), new_image.timestamp())
        elif isinstance(new_image, ImageFileModelItem):
            self.controls.setFilename(os.path.basename(new_image['filename']))

        self.selectionmodel.setCurrentIndex(new_image.index(), QItemSelectionModel.ClearAndSelect|QItemSelectionModel.Rows)

    def onFitToWindowModeChanged(self):
        if self.options["Fit-to-window mode"].isChecked():
            self.view.fitInView()

    def onScaleChanged(self, scale):
        self.zoominfo.setText("%.2f%%" % (100 * scale, ))

    def initShortcuts(self, HOTKEYS):
        self.shortcuts = []

        for hotkey in HOTKEYS:
            assert len(hotkey) >= 2
            key = hotkey[0]
            fun = hotkey[1]
            desc = ""
            if len(hotkey) > 2:
                desc = hotkey[2]
            if type(fun) == str:
                fun = import_callable(fun)

            hk = QAction(desc, self)
            hk.setShortcut(QKeySequence(key))
            hk.setEnabled(True)
            if hasattr(fun, '__call__'):
                hk.triggered.connect(bind(fun, self.labeltool))
            else:
                hk.triggered.connect(compose_noargs([bind(f, self.labeltool) for f in fun]))
            self.ui.menuShortcuts.addAction(hk)
            self.shortcuts.append(hk)

    def initOptions(self):
        self.options = {}
        for o in ["Fit-to-window mode"]:
            action = QAction(o, self)
            action.setCheckable(True)
            self.ui.menuOptions.addAction(action)
            self.options[o] = action

    ###
    ### GUI/Application setup
    ###___________________________________________________________________________________________
    def setupGui(self):
        self.ui = uic.loadUi(os.path.join(GUIDIR, "labeltool.ui"), self)

        # get inserters and items from labels
        # FIXME for handling the new-style config correctly
        inserters = dict([(label['attributes']['class'], label['inserter']) 
                          for label in config.LABELS
                          if 'class' in label.get('attributes', {}) and 'inserter' in label])
        items = dict([(label['attributes']['class'], label['item']) 
                      for label in config.LABELS
                      if 'class' in label.get('attributes', {}) and 'item' in label])

        # Property Editor
        self.property_editor = PropertyEditor(config.LABELS)
        self.ui.dockProperties.setWidget(self.property_editor)

        # Scene
        self.scene = AnnotationScene(self.labeltool, items=items, inserters=inserters)
        self.property_editor.insertionModeStarted.connect(self.scene.onInsertionModeStarted)
        self.property_editor.insertionModeEnded.connect(self.scene.onInsertionModeEnded)

        # SceneView
        self.view = GraphicsView(self)
        self.view.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)
        self.view.setScene(self.scene)

        self.central_widget = QWidget()
        self.central_layout = QVBoxLayout()
        self.controls = ControlButtonWidget()
        self.controls.back_button.clicked.connect(self.labeltool.gotoPrevious)
        self.controls.forward_button.clicked.connect(self.labeltool.gotoNext)

        self.central_layout.addWidget(self.controls)
        self.central_layout.addWidget(self.view)
        self.central_widget.setLayout(self.central_layout)
        self.setCentralWidget(self.central_widget)

        self.initShortcuts(config.HOTKEYS)
        self.initOptions()

        self.treeview = AnnotationTreeView()
        self.treeview.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred)
        self.ui.dockAnnotations.setWidget(self.treeview)

        self.scene.selectionChanged.connect(self.scene.onSelectionChanged)
        self.treeview.selectedItemsChanged.connect(self.scene.onSelectionChangedInTreeView)

        self.posinfo = QLabel("-1, -1")
        self.posinfo.setFrameStyle(QFrame.StyledPanel)
        self.statusBar().addPermanentWidget(self.posinfo)
        self.scene.mousePositionChanged.connect(self.onMousePositionChanged)

        self.image_resolution = QLabel("[no image]")
        self.image_resolution.setFrameStyle(QFrame.StyledPanel)
        self.statusBar().addPermanentWidget(self.image_resolution)

        self.zoominfo = QLabel()
        self.zoominfo.setFrameStyle(QFrame.StyledPanel)
        self.statusBar().addPermanentWidget(self.zoominfo)
        self.view.scaleChanged.connect(self.onScaleChanged)
        self.onScaleChanged(self.view.getScale())

        self.sb_progress = QProgressBar()

        # View menu
        self.ui.menu_Views.addAction(self.ui.dockProperties.toggleViewAction())
        self.ui.menu_Views.addAction(self.ui.dockAnnotations.toggleViewAction())

        # Show the UI.  It is important that this comes *after* the above 
        # adding of custom widgets, especially the central widget.  Otherwise the
        # dock widgets would be far to large.
        self.ui.show()

        ## connect action signals
        self.connectActions()

    def connectActions(self):
        ## File menu
        self.ui.actionNew.    triggered.connect(self.fileNew)
        self.ui.actionOpen.   triggered.connect(self.fileOpen)
        self.ui.actionSave.   triggered.connect(self.fileSave)
        self.ui.actionSave_As.triggered.connect(self.fileSaveAs)
        self.ui.actionExit.   triggered.connect(self.close)

        ## View menu
        self.ui.actionLocked.toggled.connect(self.onViewsLockedChanged)

        ## Help menu
        self.ui.action_About.triggered.connect(self.about)

        ## Navigation
        self.ui.action_Add_Image.triggered.connect(self.addMediaFile)
        self.ui.actionNext.      triggered.connect(self.labeltool.gotoNext)
        self.ui.actionPrevious.  triggered.connect(self.labeltool.gotoPrevious)
        self.ui.actionZoom_In.   triggered.connect(functools.partial(self.view.setScaleRelative, 1.2))
        self.ui.actionZoom_Out.  triggered.connect(functools.partial(self.view.setScaleRelative, 1/1.2))

        ## Connections to LabelTool
        self.labeltool.pluginLoaded.       connect(self.onPluginLoaded)
        self.labeltool.statusMessage.      connect(self.onStatusMessage)
        self.labeltool.annotationsLoaded.  connect(self.onAnnotationsLoaded)
        self.labeltool.currentImageChanged.connect(self.onCurrentImageChanged)

        ## options menu
        self.options["Fit-to-window mode"].changed.connect(self.onFitToWindowModeChanged)

    def loadApplicationSettings(self):
        settings = QSettings()
        size   = settings.value("MainWindow/Size", QSize(800, 600))
        pos    = settings.value("MainWindow/Position", QPoint(10, 10))
        state  = settings.value("MainWindow/State")
        locked = settings.value("MainWindow/ViewsLocked", False)
        if isinstance(size,   QVariant): size  = size.toSize()
        if isinstance(pos,    QVariant): pos   = pos.toPoint()
        if isinstance(state,  QVariant): state = state.toByteArray()
        if isinstance(locked, QVariant): locked = locked.toBool()
        self.resize(size)
        self.move(pos)
        if state is not None:
                self.restoreState(state)
        self.ui.actionLocked.setChecked(bool(locked))

    def saveApplicationSettings(self):
        settings = QSettings()
        settings.setValue("MainWindow/Size",        self.size())
        settings.setValue("MainWindow/Position",    self.pos())
        settings.setValue("MainWindow/State",       self.saveState())
        settings.setValue("MainWindow/ViewsLocked", self.ui.actionLocked.isChecked())
        if self.labeltool.getCurrentFilename() is not None:
            filename = self.labeltool.getCurrentFilename()
        else:
            filename = None
        settings.setValue("LastFile", filename)

    def okToContinue(self):
        if self.labeltool.model().dirty():
            reply = QMessageBox.question(self,
                    "%s - Unsaved Changes" % (APP_NAME),
                    "Save unsaved changes?",
                    QMessageBox.Yes|QMessageBox.No|QMessageBox.Cancel)
            if reply == QMessageBox.Cancel:
                return False
            elif reply == QMessageBox.Yes:
                return self.fileSave()
        return True

    def fileNew(self):
        if self.okToContinue():
            self.labeltool.clearAnnotations()

    def fileOpen(self):
        if not self.okToContinue():
            return
        path = '.'
        filename = self.labeltool.getCurrentFilename()
        if (filename is not None) and (len(filename) > 0):
            path = QFileInfo(filename).path()

        format_str = ' '.join(self.labeltool.getAnnotationFilePatterns())
        fname = QFileDialog.getOpenFileName(self, 
                "%s - Load Annotations" % APP_NAME, path,
                "%s annotation files (%s)" % (APP_NAME, format_str))
        if len(str(fname)) > 0:
            self.labeltool.loadAnnotations(fname)

    def fileSave(self):
        filename = self.labeltool.getCurrentFilename()
        if filename is None:
            return self.fileSaveAs()
        return self.labeltool.saveAnnotations(filename)

    def fileSaveAs(self):
        fname = '.'  # self.annotations.filename() or '.'
        format_str = ' '.join(self.labeltool.getAnnotationFilePatterns())
        fname = QFileDialog.getSaveFileName(self,
                "%s - Save Annotations" % APP_NAME, fname,
                "%s annotation files (%s)" % (APP_NAME, format_str))

        if len(str(fname)) > 0:
            return self.labeltool.saveAnnotations(str(fname))
        return False

    def addMediaFile(self):
        path = '.'
        filename = self.labeltool.getCurrentFilename()
        if (filename is not None) and (len(filename) > 0):
            path = QFileInfo(filename).path()

        image_types = [ '*.jpg', '*.bmp', '*.png', '*.pgm', '*.ppm', '*.ppm', '*.tif', '*.gif' ]
        video_types = [ '*.mp4', '*.mpg', '*.mpeg', '*.avi', '*.mov', '*.vob' ]
        format_str = ' '.join(image_types + video_types)
        fnames = QFileDialog.getOpenFileNames(self, "%s - Add Media File" % APP_NAME, path, "Media files (%s)" % (format_str, ))

        item = None
        numFiles = len(fnames)
        progress_bar = QProgressDialog('Importing files...', 'Cancel import', 0, numFiles, self)
        for fname,c in zip(fnames, range(numFiles)):
            if len(str(fname)) == 0:
                continue

            fname = str(fname)

            if os.path.isabs(fname):
                fname = os.path.relpath(fname, str(path))

            for pattern in image_types:
                if fnmatch.fnmatch(fname, pattern):
                    item = self.labeltool.addImageFile(fname)
            
            progress_bar.setValue(c)

        if item is None:
            return self.labeltool.addVideoFile(fname)

        progress_bar.close()
        
        return item

    def onViewsLockedChanged(self, checked):
        features = QDockWidget.AllDockWidgetFeatures
        if checked:
            features = QDockWidget.NoDockWidgetFeatures 

        self.ui.dockProperties.setFeatures(features)
        self.ui.dockAnnotations.setFeatures(features)


    ###
    ### global event handling
    ###______________________________________________________________________________
    def closeEvent(self, event):
        if self.okToContinue():
            self.saveApplicationSettings()
        else:
            event.ignore()

    def about(self):
        QMessageBox.about(self, "About %s" % APP_NAME,
             """<b>%s</b> version %s
             <p>This labeling application for computer vision research
             was developed at the CVHCI research group at KIT.
             <p>For more details, visit our homepage: <a href="%s">%s</a>"""
              % (APP_NAME, __version__, ORGANIZATION_DOMAIN, ORGANIZATION_DOMAIN))
Exemplo n.º 3
0
class LayerStackModel(QAbstractListModel):
    canMoveSelectedUp = pyqtSignal("bool")
    canMoveSelectedDown = pyqtSignal("bool")
    canDeleteSelected = pyqtSignal("bool")

    orderChanged = pyqtSignal()
    layerAdded = pyqtSignal(Layer, int)  # is now in row
    layerRemoved = pyqtSignal(Layer, int)  # was in row
    stackCleared = pyqtSignal()

    def __init__(self, parent=None):
        QAbstractListModel.__init__(self, parent)
        self._layerStack = []
        self.selectionModel = QItemSelectionModel(self)
        self.selectionModel.selectionChanged.connect(self.updateGUI)
        self.selectionModel.selectionChanged.connect(self._onSelectionChanged)
        self._movingRows = False
        QTimer.singleShot(0, self.updateGUI)

        def _handleRemovedLayer(layer):
            # Layerstacks *own* the layers they hold, and thus are
            #  responsible for cleaning them up when they are removed:
            layer.clean_up()

        self.layerRemoved.connect(_handleRemovedLayer)

    ####
    ## High level API to manipulate the layerstack
    ###

    def __len__(self):
        return self.rowCount()

    def __repr__(self):
        return "<LayerStackModel: layerStack='%r'>" % (self._layerStack, )

    def __getitem__(self, i):
        return self._layerStack[i]

    def __iter__(self):
        return self._layerStack.__iter__()

    def layerIndex(self, layer):
        #note that the 'index' function already has a different implementation
        #from Qt side
        return self._layerStack.index(layer)

    def findMatchingIndex(self, func):
        """Call the given function with each layer and return the index of the first layer for which f is True."""
        for index, layer in enumerate(self._layerStack):
            if func(layer):
                return index
        raise ValueError("No matching layer in stack.")

    def append(self, data):
        self.insert(0, data)

    def clear(self):
        if len(self) > 0:
            self.removeRows(0, len(self))
            self.stackCleared.emit()

    def insert(self, index, data):
        """
        Insert a layer into this layer stack, which *takes ownership* of the layer.
        """
        assert isinstance(
            data, Layer), "Only Layers can be added to a LayerStackModel"
        self.insertRow(index)
        self.setData(self.index(index), data)
        if self.selectedRow() >= 0:
            self.selectionModel.select(self.index(self.selectedRow()),
                                       QItemSelectionModel.Deselect)
        self.selectionModel.select(self.index(index),
                                   QItemSelectionModel.Select)

        data.changed.connect(
            functools.partial(self._onLayerChanged, self.index(index)))
        index = self._layerStack.index(data)
        self.layerAdded.emit(data, index)

        self.updateGUI()

    def selectRow(self, row):
        already_selected_rows = self.selectionModel.selectedRows()
        if len(already_selected_rows) == 1 and row == already_selected_rows[0]:
            # Nothing to do if this row is already selected
            return
        self.selectionModel.clear()
        self.selectionModel.setCurrentIndex(self.index(row),
                                            QItemSelectionModel.SelectCurrent)

    @pyqtSignature("deleteSelected()")
    def deleteSelected(self):
        num_rows = len(self.selectionModel.selectedRows())
        assert num_rows == 1, "Can't delete selected row: {} layers are currently selected.".format(
            num_rows)
        row = self.selectionModel.selectedRows()[0]
        layer = self._layerStack[row.row()]
        assert not layer._cleaned_up, "This layer ({}) has already been cleaned up.  Shouldn't it already be removed from the layerstack?".format(
            layer.name)
        self.removeRow(row.row())
        if self.rowCount() > 0:
            self.selectionModel.select(self.index(0),
                                       QItemSelectionModel.Select)
        self.layerRemoved.emit(layer, row.row())
        self.updateGUI()

    @pyqtSignature("moveSelectedUp()")
    def moveSelectedUp(self):
        assert len(self.selectionModel.selectedRows()) == 1
        row = self.selectionModel.selectedRows()[0]
        if row.row() != 0:
            oldRow = row.row()
            newRow = oldRow - 1
            self._moveToRow(oldRow, newRow)

    @pyqtSignature("moveSelectedDown()")
    def moveSelectedDown(self):
        assert len(self.selectionModel.selectedRows()) == 1
        row = self.selectionModel.selectedRows()[0]
        if row.row() != self.rowCount() - 1:
            oldRow = row.row()
            newRow = oldRow + 1
            self._moveToRow(oldRow, newRow)

    @pyqtSignature("moveSelectedToTop()")
    def moveSelectedToTop(self):
        assert len(self.selectionModel.selectedRows()) == 1
        row = self.selectionModel.selectedRows()[0]
        if row.row() != 0:
            oldRow = row.row()
            newRow = 0
            self._moveToRow(oldRow, newRow)

    @pyqtSignature("moveSelectedToBottom()")
    def moveSelectedToBottom(self):
        assert len(self.selectionModel.selectedRows()) == 1
        row = self.selectionModel.selectedRows()[0]
        if row.row() != self.rowCount() - 1:
            oldRow = row.row()
            newRow = self.rowCount() - 1
            self._moveToRow(oldRow, newRow)

    def moveSelectedToRow(self, newRow):
        assert len(self.selectionModel.selectedRows()) == 1
        row = self.selectionModel.selectedRows()[0]
        if row.row() != newRow:
            oldRow = row.row()
            self._moveToRow(oldRow, newRow)

    def _moveToRow(self, oldRow, newRow):
        d = self._layerStack[oldRow]
        self.removeRow(oldRow)
        self.insertRow(newRow)
        self.setData(self.index(newRow), d)
        self.selectionModel.select(self.index(newRow),
                                   QItemSelectionModel.Select)
        self.orderChanged.emit()
        self.updateGUI()

    ####
    ## Low level API. To add, remove etc. layers use the high level API from above.
    ####

    def updateGUI(self):
        self.canMoveSelectedUp.emit(self.selectedRow() > 0)
        self.canMoveSelectedDown.emit(self.selectedRow() < self.rowCount() - 1)
        self.canDeleteSelected.emit(self.rowCount() > 0)
        self.wantsUpdate()

    def selectedRow(self):
        selected = self.selectionModel.selectedRows()
        if len(selected) == 1:
            return selected[0].row()
        return -1

    def selectedIndex(self):
        row = self.selectedRow()
        if row >= 0:
            return self.index(self.selectedRow())
        else:
            return QModelIndex()

    def rowCount(self, parent=QModelIndex()):
        if not parent.isValid():
            return len(self._layerStack)
        return 0

    def insertRows(self, row, count, parent=QModelIndex()):
        '''Insert empty rows in the stack. 
        
        DO NOT USE THIS METHOD TO INSERT NEW LAYERS!
        Always use the insert() or append() method.
        
        '''
        if parent.isValid():
            return False
        oldRowCount = self.rowCount()
        #for some reason, row can be negative!
        beginRow = max(0, row)
        endRow = min(beginRow + count - 1, len(self._layerStack))
        self.beginInsertRows(parent, beginRow, endRow)
        while (beginRow <= endRow):
            self._layerStack.insert(row, Layer(datasources=[]))
            beginRow += 1
        self.endInsertRows()
        assert self.rowCount(
        ) == oldRowCount + 1, "oldRowCount = %d, self.rowCount() = %d" % (
            oldRowCount, self.rowCount())
        return True

    def removeRows(self, row, count, parent=QModelIndex()):
        '''Remove rows from the stack. 
        
        DO NOT USE THIS METHOD TO REMOVE LAYERS!
        Use the deleteSelected() method instead.
        
        '''

        if parent.isValid():
            return False
        if row + count <= 0 or row >= len(self._layerStack):
            return False
        oldRowCount = self.rowCount()
        beginRow = max(0, row)
        endRow = min(row + count - 1, len(self._layerStack) - 1)
        self.beginRemoveRows(parent, beginRow, endRow)
        while (beginRow <= endRow):
            del self._layerStack[row]
            beginRow += 1
        self.endRemoveRows()
        return True

    def flags(self, index):
        defaultFlags = Qt.ItemIsSelectable | Qt.ItemIsEditable | Qt.ItemIsEnabled
        if index.isValid():
            return Qt.ItemIsDragEnabled | defaultFlags
        else:
            return Qt.ItemIsDropEnabled | defaultFlags

    def supportedDropActions(self):
        return Qt.MoveAction

    def data(self, index, role=Qt.DisplayRole):
        if not index.isValid():
            return None
        if index.row() > len(self._layerStack):
            return None

        if role == Qt.DisplayRole or role == Qt.EditRole:
            return self._layerStack[index.row()]
        elif role == Qt.ToolTipRole:
            return self._layerStack[index.row()].toolTip()
        else:
            return None

    def setData(self, index, value, role=Qt.EditRole):
        '''Replace one layer with another. 
        
        DO NOT USE THIS METHOD TO INSERT NEW LAYERS!
        Use deleteSelected() followed by insert() or append().
        
        '''
        if role == Qt.EditRole:
            layer = value
            if not isinstance(value, Layer):
                layer = value.toPyObject()
            self._layerStack[index.row()] = layer
            self.dataChanged.emit(index, index)
            return True
        elif role == Qt.ToolTipRole:
            self._layerStack[index.row()].setToolTip()
            return True
        return False

    def headerData(self, section, orientation, role=Qt.DisplayRole):
        if role != Qt.DisplayRole:
            return None
        if orientation == Qt.Horizontal:
            return "Column %r" % section
        else:
            return "Row %r" % section

    def wantsUpdate(self):
        self.layoutChanged.emit()

    def _onLayerChanged(self, idx):
        self.dataChanged.emit(idx, idx)
        self.updateGUI()

    def _onSelectionChanged(self, selected, deselected):
        for idx in deselected.indexes():
            self[idx.row()].setActive(False)
        for idx in selected.indexes():
            self[idx.row()].setActive(True)
Exemplo n.º 4
0
class LayerStackModel(QAbstractListModel):
    canMoveSelectedUp = pyqtSignal("bool")
    canMoveSelectedDown = pyqtSignal("bool")
    canDeleteSelected = pyqtSignal("bool")
    
    orderChanged = pyqtSignal()
    layerAdded = pyqtSignal( Layer, int ) # is now in row
    layerRemoved = pyqtSignal( Layer, int ) # was in row
    stackCleared = pyqtSignal()
        
    def __init__(self, parent = None):
        QAbstractListModel.__init__(self, parent)
        self._layerStack = []
        self.selectionModel = QItemSelectionModel(self)
        self.selectionModel.selectionChanged.connect(self.updateGUI)
        self._movingRows = False
        QTimer.singleShot(0, self.updateGUI)

    ####
    ## High level API to manipulate the layerstack
    ###
    
    def __len__(self):
        return self.rowCount()
        
    def __repr__(self):
        return "<LayerStackModel: layerStack='%r'>" % (self._layerStack,)  
    
    def __getitem__(self, i):
        return self._layerStack[i]
    
    def __iter__(self):
        return self._layerStack.__iter__()
    
    def layerIndex(self, layer):
        #note that the 'index' function already has a different implementation
        #from Qt side
        return self._layerStack.index(layer)

    def append(self, data):
        self.insert(0, data)
   
    def clear(self):
        if len(self) > 0:
            self.removeRows(0,len(self))
            self.stackCleared.emit()

    def insert(self, index, data):
        self.insertRow(index)
        self.setData(self.index(index), data)
        if self.selectedRow() >= 0:
            self.selectionModel.select(self.index(self.selectedRow()), QItemSelectionModel.Deselect)
        self.selectionModel.select(self.index(index), QItemSelectionModel.Select)
        
        def onChanged():
            #assumes that data is unique!
            idx = self.index(self._layerStack.index(data))
            self.dataChanged.emit(idx, idx)
            self.updateGUI()
        data.changed.connect(onChanged)
        self.layerAdded.emit( data, self._layerStack.index(data))
        self.updateGUI()

    def selectRow( self, row ):
        self.selectionModel.setCurrentIndex( self.index(row), QItemSelectionModel.SelectCurrent)

    @pyqtSignature("deleteSelected()")
    def deleteSelected(self):
        assert len(self.selectionModel.selectedRows()) == 1
        row = self.selectionModel.selectedRows()[0]
        layer = self._layerStack[row.row()]
        self.removeRow(row.row())
        if self.rowCount() > 0:
            self.selectionModel.select(self.index(0), QItemSelectionModel.Select)
        self.layerRemoved.emit( layer, row.row() )
        self.updateGUI()

    @pyqtSignature("moveSelectedUp()")
    def moveSelectedUp(self):
        assert len(self.selectionModel.selectedRows()) == 1
        row = self.selectionModel.selectedRows()[0]
        if row.row() != 0:
            oldRow = row.row()
            newRow = oldRow - 1
            d = self._layerStack[oldRow]
            self.removeRow(oldRow)
            self.insertRow(newRow)
            self.setData(self.index(newRow), d)
            self.selectionModel.select(self.index(newRow), QItemSelectionModel.Select)
            self.orderChanged.emit()
            self.updateGUI()
    
    @pyqtSignature("moveSelectedDown()")
    def moveSelectedDown(self):
        assert len(self.selectionModel.selectedRows()) == 1
        row = self.selectionModel.selectedRows()[0]
        if row.row() != self.rowCount() - 1:
            oldRow = row.row()
            newRow = oldRow + 1
            d = self._layerStack[oldRow]
            self.removeRow(oldRow)
            self.insertRow(newRow)
            self.setData(self.index(newRow), d)
            self.selectionModel.select(self.index(newRow), QItemSelectionModel.Select)
            self.orderChanged.emit()
            self.updateGUI()
       
    ####
    ## Low level API. To add, remove etc. layers use the high level API from above.
    ####
 
    def updateGUI(self):
        self.canMoveSelectedUp.emit(self.selectedRow()>0)
        self.canMoveSelectedDown.emit(self.selectedRow()<self.rowCount()-1)
        self.canDeleteSelected.emit(self.rowCount() > 0)
        self.wantsUpdate()
        
    def selectedRow(self):
        selected = self.selectionModel.selectedRows()
        if len(selected) == 1:
            return selected[0].row()
        return -1
    
    def selectedIndex(self):
        row = self.selectedRow()
        if row >= 0:
            return self.index(self.selectedRow())
        else:
            return QModelIndex()
    
    def rowCount(self, parent = QModelIndex()):
        if not parent.isValid():
            return len(self._layerStack)
        return 0
    
    def insertRows(self, row, count, parent = QModelIndex()):
        '''Insert empty rows in the stack. 
        
        DO NOT USE THIS METHOD TO INSERT NEW LAYERS!
        Always use the insert() or append() method.
        
        '''
        if parent.isValid():
            return False
        oldRowCount = self.rowCount()
        #for some reason, row can be negative!
        beginRow = max(0,row)
        endRow   = min(beginRow+count-1, len(self._layerStack))
        self.beginInsertRows(parent, beginRow, endRow) 
        while(beginRow <= endRow):
            self._layerStack.insert(row, Layer())
            beginRow += 1
        self.endInsertRows()
        assert self.rowCount() == oldRowCount+1, "oldRowCount = %d, self.rowCount() = %d" % (oldRowCount, self.rowCount())
        return True
            
    def removeRows(self, row, count, parent = QModelIndex()):
        '''Remove rows from the stack. 
        
        DO NOT USE THIS METHOD TO REMOVE LAYERS!
        Use the deleteSelected() method instead.
        
        '''

        if parent.isValid():
            return False
        if row+count <= 0 or row >= len(self._layerStack):
            return False
        oldRowCount = self.rowCount()
        beginRow = max(0,row)
        endRow   = min(row+count-1, len(self._layerStack)-1)
        self.beginRemoveRows(parent, beginRow, endRow)
        while(beginRow <= endRow):
            del self._layerStack[row]
            beginRow += 1
        self.endRemoveRows()
        return True
    
    def flags(self, index):
        defaultFlags = Qt.ItemIsSelectable | Qt.ItemIsEditable | Qt.ItemIsEnabled
        if index.isValid():
            return Qt.ItemIsDragEnabled | defaultFlags
        else:
            return Qt.ItemIsDropEnabled | defaultFlags
    
    def supportedDropActions(self):
        return Qt.MoveAction
    
    def data(self, index, role = Qt.DisplayRole):
        if not index.isValid():
            return None
        if index.row() > len(self._layerStack):
            return None
        
        if role == Qt.DisplayRole or role == Qt.EditRole:
            return self._layerStack[index.row()]
        
        return None
    
    def setData(self, index, value, role = Qt.EditRole):
        '''Replace one layer with another. 
        
        DO NOT USE THIS METHOD TO INSERT NEW LAYERS!
        Use deleteSelected() followed by insert() or append().
        
        '''
        layer = value
        if not isinstance(value, Layer):
            layer = value.toPyObject()
        self._layerStack[index.row()] = layer
        self.dataChanged.emit(index, index)
        return True
    
    def headerData(self, section, orientation, role = Qt.DisplayRole):
        if role != Qt.DisplayRole:
            return None
        if orientation == Qt.Horizontal:
            return "Column %r" % section
        else:
            return "Row %r" % section
        
    def wantsUpdate(self):
        self.layoutChanged.emit()
Exemplo n.º 5
0
class Window(QMainWindow):

  def __init__(self):
    super(Window, self).__init__()

    central_widget = QWidget()

    self._current_path = None
    self._use_suffix = False

    self._file_model = QFileSystemModel()
    self._file_model.setNameFilters(['*.jpg', '*.png'])
    self._file_model.setNameFilterDisables(False)
    self._file_model.setRootPath(QDir.rootPath())

    self._file_selection_model = QItemSelectionModel(self._file_model)
    self._file_selection_model.currentChanged.connect(self._on_current_file_changed)

    self._file_tree = QTreeView(parent=self)
    self._file_tree.collapsed.connect(self._on_tree_expanded_collapsed)
    self._file_tree.expanded.connect(self._on_tree_expanded_collapsed)
    self._file_tree.setModel(self._file_model)
    self._file_tree.setSelectionModel(self._file_selection_model)
    self._file_tree.setColumnHidden(1, True)
    self._file_tree.setColumnHidden(2, True)
    self._file_tree.setColumnHidden(3, True)
    self._file_tree.header().hide()

    self._viewer = Viewer(Loader(24))

    self._splitter = QSplitter();
    self._splitter.addWidget(self._file_tree)
    self._splitter.addWidget(self._viewer)
    self._splitter.setStretchFactor(0, 0)
    self._splitter.setStretchFactor(1, 1)
    self._splitter.setCollapsible(0, False)

    self._layout = QGridLayout()
    self._layout.addWidget(self._splitter)
    self._switch_to_normal()
    central_widget.setLayout(self._layout)

    self._file_tree.installEventFilter(self);

    self.resize(800, 600)
    self.setWindowTitle('pyQtures')
    self.setCentralWidget(central_widget)
    self.show()

  def eventFilter(self, widget, event):
    if event.type() == QEvent.KeyPress:
      if event.key() == Qt.Key_Tab:
        self._toggle_path_suffix()
        return True
    return QMainWindow.eventFilter(self, widget, event)

  def _toggle_path_suffix(self):
    self._use_suffix = not self._use_suffix
    self._update_path()

  def _switch_to_fullscreen(self):
    self._splitter.widget(0).hide()
    self._layout.setMargin(0)
    self.showFullScreen()

  def _switch_to_normal(self):
    self._splitter.widget(0).show()
    self._layout.setMargin(4)
    self.showNormal()

  def keyPressEvent(self, key_event):  # Signal handler.
    key = key_event.key()
    if self.isFullScreen():
      self._full_screen_key_handler(key)
    else:
      self._normal_key_handler(key)

  def _full_screen_key_handler(self, key):
    if Qt.Key_Escape == key:
      self._switch_to_normal()
    elif Qt.Key_Return == key:
      self._switch_to_normal()
    elif Qt.Key_Up == key:
      self._go_to_sibling_image(-1)
    elif Qt.Key_Down == key:
      self._go_to_sibling_image(1)
    elif Qt.Key_Tab == key:
      self._toggle_path_suffix()

  def _go_to_sibling_image(self, offset):
    current = self._file_selection_model.currentIndex()
    nxt = current.sibling(current.row() + offset, current.column())
    if (nxt.parent() != current.parent()):
      return
    # TODO(eustas): Iterate through dirs?
    self._file_selection_model.setCurrentIndex(nxt, QItemSelectionModel.SelectCurrent)

  def _normal_key_handler(self, key):
    if Qt.Key_Escape == key:
      QCoreApplication.instance().quit()
    elif Qt.Key_Return == key:
      self._switch_to_fullscreen()

  def _on_current_file_changed(self, new_current):
    new_path = self._file_model.filePath(new_current)
    if not self._current_path == new_path:
        self._current_path = new_path
        self._update_path()

  def _update_path(self):
    if not self._use_suffix:
      self._viewer.set_path(self._current_path)
      return

    self._viewer.reset_path()
    if not self._current_path:
      return

    selected_file = QFileInfo(self._current_path)
    if not selected_file.exists():
      return

    selected_dir = selected_file.absoluteDir()
    file_name = selected_file.fileName()
    if not selected_dir.exists():
      return

    if not selected_dir.cd('converted'):
      return

    suffixed_path = selected_dir.absoluteFilePath(file_name)
    self._viewer.set_path(suffixed_path)

  def _on_tree_expanded_collapsed(self, unused_index):
    QTimer.singleShot(1, lambda: self._file_tree.resizeColumnToContents(0))
Exemplo n.º 6
0
class LayerStackModel(QAbstractListModel):
    canMoveSelectedUp = pyqtSignal("bool")
    canMoveSelectedDown = pyqtSignal("bool")
    canDeleteSelected = pyqtSignal("bool")
    
    orderChanged = pyqtSignal()
    layerAdded = pyqtSignal( Layer, int ) # is now in row
    layerRemoved = pyqtSignal( Layer, int ) # was in row
    stackCleared = pyqtSignal()
        
    def __init__(self, parent = None):
        QAbstractListModel.__init__(self, parent)
        self._layerStack = []
        self.selectionModel = QItemSelectionModel(self)
        self.selectionModel.selectionChanged.connect(self.updateGUI)
        self.selectionModel.selectionChanged.connect(self._onSelectionChanged)
        self._movingRows = False
        QTimer.singleShot(0, self.updateGUI)
        
        def _handleRemovedLayer(layer):
            # Layerstacks *own* the layers they hold, and thus are 
            #  responsible for cleaning them up when they are removed:
            layer.clean_up()
        self.layerRemoved.connect( _handleRemovedLayer )

    ####
    ## High level API to manipulate the layerstack
    ###
    
    def __len__(self):
        return self.rowCount()
        
    def __repr__(self):
        return "<LayerStackModel: layerStack='%r'>" % (self._layerStack,)  
    
    def __getitem__(self, i):
        return self._layerStack[i]
    
    def __iter__(self):
        return self._layerStack.__iter__()
    
    def layerIndex(self, layer):
        #note that the 'index' function already has a different implementation
        #from Qt side
        return self._layerStack.index(layer)

    def findMatchingIndex(self, func):
        """Call the given function with each layer and return the index of the first layer for which f is True."""
        for index, layer in enumerate(self._layerStack):
            if func(layer):
                return index
        raise ValueError("No matching layer in stack.")

    def append(self, data):
        self.insert(0, data)
   
    def clear(self):
        if len(self) > 0:
            self.removeRows(0,len(self))
            self.stackCleared.emit()

    def insert(self, index, data):
        """
        Insert a layer into this layer stack, which *takes ownership* of the layer.
        """
        assert isinstance(data, Layer), "Only Layers can be added to a LayerStackModel"
        self.insertRow(index)
        self.setData(self.index(index), data)
        if self.selectedRow() >= 0:
            self.selectionModel.select(self.index(self.selectedRow()), QItemSelectionModel.Deselect)
        self.selectionModel.select(self.index(index), QItemSelectionModel.Select)
        
        data.changed.connect(functools.partial(self._onLayerChanged, self.index(index)))
        index = self._layerStack.index(data)
        self.layerAdded.emit(data, index)

        self.updateGUI()

    def selectRow( self, row ):
        self.selectionModel.setCurrentIndex( self.index(row), QItemSelectionModel.SelectCurrent)

    @pyqtSignature("deleteSelected()")
    def deleteSelected(self):
        num_rows = len(self.selectionModel.selectedRows())
        assert num_rows == 1, "Can't delete selected row: {} layers are currently selected.".format( num_rows )
        row = self.selectionModel.selectedRows()[0]
        layer = self._layerStack[row.row()]
        assert not layer._cleaned_up, "This layer ({}) has already been cleaned up.  Shouldn't it already be removed from the layerstack?".format( layer.name )
        self.removeRow(row.row())
        if self.rowCount() > 0:
            self.selectionModel.select(self.index(0), QItemSelectionModel.Select)
        self.layerRemoved.emit( layer, row.row() )
        self.updateGUI()
        
    @pyqtSignature("moveSelectedUp()")
    def moveSelectedUp(self):
        assert len(self.selectionModel.selectedRows()) == 1
        row = self.selectionModel.selectedRows()[0]
        if row.row() != 0:
            oldRow = row.row()
            newRow = oldRow - 1
            self._moveToRow(oldRow, newRow)
    

    @pyqtSignature("moveSelectedDown()")
    def moveSelectedDown(self):
        assert len(self.selectionModel.selectedRows()) == 1
        row = self.selectionModel.selectedRows()[0]
        if row.row() != self.rowCount() - 1:
            oldRow = row.row()
            newRow = oldRow + 1
            self._moveToRow(oldRow, newRow)
            
    @pyqtSignature("moveSelectedToTop()")
    def moveSelectedToTop(self):
        assert len(self.selectionModel.selectedRows()) == 1
        row = self.selectionModel.selectedRows()[0]
        if row.row() != 0:
            oldRow = row.row()
            newRow = 0
            self._moveToRow(oldRow, newRow)
    
    @pyqtSignature("moveSelectedToBottom()")
    def moveSelectedToBottom(self):
        assert len(self.selectionModel.selectedRows()) == 1
        row = self.selectionModel.selectedRows()[0]
        if row.row() != self.rowCount() - 1:
            oldRow = row.row()
            newRow = self.rowCount() - 1
            self._moveToRow(oldRow, newRow)

    def moveSelectedToRow(self, newRow):
        assert len(self.selectionModel.selectedRows()) == 1
        row = self.selectionModel.selectedRows()[0]
        if row.row() != newRow:
            oldRow = row.row()
            self._moveToRow(oldRow, newRow)

    def _moveToRow(self, oldRow, newRow):
        d = self._layerStack[oldRow]
        self.removeRow(oldRow)
        self.insertRow(newRow)
        self.setData(self.index(newRow), d)
        self.selectionModel.select(self.index(newRow), QItemSelectionModel.Select)
        self.orderChanged.emit()
        self.updateGUI()
    ####
    ## Low level API. To add, remove etc. layers use the high level API from above.
    ####
 
    def updateGUI(self):
        self.canMoveSelectedUp.emit(self.selectedRow()>0)
        self.canMoveSelectedDown.emit(self.selectedRow()<self.rowCount()-1)
        self.canDeleteSelected.emit(self.rowCount() > 0)
        self.wantsUpdate()
        
    def selectedRow(self):
        selected = self.selectionModel.selectedRows()
        if len(selected) == 1:
            return selected[0].row()
        return -1
    
    def selectedIndex(self):
        row = self.selectedRow()
        if row >= 0:
            return self.index(self.selectedRow())
        else:
            return QModelIndex()
    
    def rowCount(self, parent = QModelIndex()):
        if not parent.isValid():
            return len(self._layerStack)
        return 0
    
    def insertRows(self, row, count, parent = QModelIndex()):
        '''Insert empty rows in the stack. 
        
        DO NOT USE THIS METHOD TO INSERT NEW LAYERS!
        Always use the insert() or append() method.
        
        '''
        if parent.isValid():
            return False
        oldRowCount = self.rowCount()
        #for some reason, row can be negative!
        beginRow = max(0,row)
        endRow   = min(beginRow+count-1, len(self._layerStack))
        self.beginInsertRows(parent, beginRow, endRow) 
        while(beginRow <= endRow):
            self._layerStack.insert(row, Layer(datasources=[]))
            beginRow += 1
        self.endInsertRows()
        assert self.rowCount() == oldRowCount+1, "oldRowCount = %d, self.rowCount() = %d" % (oldRowCount, self.rowCount())
        return True
            
    def removeRows(self, row, count, parent = QModelIndex()):
        '''Remove rows from the stack. 
        
        DO NOT USE THIS METHOD TO REMOVE LAYERS!
        Use the deleteSelected() method instead.
        
        '''

        if parent.isValid():
            return False
        if row+count <= 0 or row >= len(self._layerStack):
            return False
        oldRowCount = self.rowCount()
        beginRow = max(0,row)
        endRow   = min(row+count-1, len(self._layerStack)-1)
        self.beginRemoveRows(parent, beginRow, endRow)
        while(beginRow <= endRow):
            del self._layerStack[row]
            beginRow += 1
        self.endRemoveRows()
        return True
    
    def flags(self, index):
        defaultFlags = Qt.ItemIsSelectable | Qt.ItemIsEditable | Qt.ItemIsEnabled
        if index.isValid():
            return Qt.ItemIsDragEnabled | defaultFlags
        else:
            return Qt.ItemIsDropEnabled | defaultFlags
    
    def supportedDropActions(self):
        return Qt.MoveAction

    def data(self, index, role = Qt.DisplayRole):
        if not index.isValid():
            return None
        if index.row() > len(self._layerStack):
            return None
        
        if role == Qt.DisplayRole or role == Qt.EditRole:
            return self._layerStack[index.row()]
        elif role == Qt.ToolTipRole:
            return self._layerStack[index.row()].toolTip()
        else:
            return None
    
    def setData(self, index, value, role = Qt.EditRole):
        '''Replace one layer with another. 
        
        DO NOT USE THIS METHOD TO INSERT NEW LAYERS!
        Use deleteSelected() followed by insert() or append().
        
        '''
        if role == Qt.EditRole:
            layer = value
            if not isinstance(value, Layer):
                layer = value.toPyObject()
            self._layerStack[index.row()] = layer
            self.dataChanged.emit(index, index)
            return True
        elif role == Qt.ToolTipRole:
            self._layerStack[index.row()].setToolTip()
            return True
        return False
            
    
    def headerData(self, section, orientation, role = Qt.DisplayRole):
        if role != Qt.DisplayRole:
            return None
        if orientation == Qt.Horizontal:
            return "Column %r" % section
        else:
            return "Row %r" % section
        
    def wantsUpdate(self):
        self.layoutChanged.emit()

    def _onLayerChanged( self, idx ):
        self.dataChanged.emit(idx, idx)
        self.updateGUI()
        
    def _onSelectionChanged(self, selected, deselected):
        for idx in deselected.indexes():
            self[idx.row()].setActive(False) 
        for idx in selected.indexes():
            self[idx.row()].setActive(True)