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))
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))
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)
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()
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))
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)