def selectionChanged(self, selected, deselected): """QAbstractItemView slot """ debug_print('SummaryView.selectionChanged') self._updated( self.model().rowCount(), self.selectionModel().selectedIndexes() )
def dataChanged(self, topLeft, bottomRight, roles=[]): """QAbstractItemView virtual """ debug_print('SummaryView.dataChanged') self._updated( self.model().rowCount(), self.selectionModel().selectedIndexes() )
def mouseMoveEvent(self, event): """QGraphicsItem virtual """ debug_print('ResizeHandle.mouseMoveEvent') # Vector difference between the mouse's position and current position, # both in client coordinates parent = self.parentItem() v = event.pos() - self.mapFromItem(parent, self.pos()) if v.isNull(): debug_print('v.isNull') else: # Compute adjustments to parent item's rect dx1 = dy1 = dx2 = dy2 = 0.0 if Qt.TopLeftCorner == self._corner: dx1, dy1 = v.x(), v.y() elif Qt.TopRightCorner == self._corner: dx2, dy1 = v.x(), v.y() elif Qt.BottomLeftCorner == self._corner: dx1, dy2 = v.x(), v.y() else: dx2, dy2 = v.x(), v.y() rect = parent.rect().adjusted(dx1, dy1, dx2, dy2) if not rect.isValid(): # A valid rectangle has a width() > 0 and height() > 0 # rect will be invalid if the user has attempted to drag the box # inside out - prevent this pass else: parent.prepareGeometryChange() parent.setRect(rect)
def reset(self): "QAbstractItemView virtual" debug_print('MetadataView.reset') super(MetadataView, self).reset() # Clear the controls self._populate_controls()
def zoom_to_items(self, items): """Centres view on the centre of the items and, if view is set to 'fit to view', sets the zoom level to encompass items. Emits viewport_changed. """ united = unite_rects(i.sceneBoundingRect() for i in items) if 'whole_scene' == self.zoom_mode: debug_print('Ensuring [{0}] items visible'.format(len(items))) self.ensureVisible(united) self.viewport_changed.emit(self.normalised_scene_rect()) else: debug_print('Showing [{0}] items'.format(len(items))) # Add some padding around the selection padding = 20 if 'follow_selection' == self.zoom_mode: # Update zoom united.adjust(-padding, -padding, 2 * padding, 2 * padding) self.fitInView(united, Qt.KeepAspectRatio) if self.absolute_zoom > self.MAXIMUM_ZOOM: # new_absolute_zoom() emits viewport_changed self.new_absolute_zoom(self.MAXIMUM_ZOOM) else: self.viewport_changed.emit(self.normalised_scene_rect()) else: # zoom_mode == fixed self.ensureVisible(united, xMargin=padding, yMargin=padding)
def setRect(self, rect): """QGraphicsRectItem function """ debug_print('BoxItem.setRect') super(BoxItem, self).setRect(rect) self._set_z_index() self._layout_children()
def add_path(self, path): "Adds path to recent documents" recent = self.read_paths() # Make the path absolute, resolving any symlinks. path = self._resolved_if_possible(Path(path)) # Remove the existing occurrence of path, if present. # A linear scan is acceptable here because the list will always # be very short try: recent.remove(path) except ValueError: # path is not currently in recent pass # Prepend the path recent.insert(0, str(path)) # Limit to MAX_RECENT_DOCS recent = recent[:self.MAX_RECENT_DOCS] debug_print('Writing {0} recent document paths'.format(len(recent))) settings = QSettings() settings.beginWriteArray(self.KEY, len(recent)) try: for index, path in enumerate(recent): settings.setArrayIndex(index) settings.setValue('path', str(path)) finally: settings.endArray()
def load(self, path): "Loads the UserTemplate in path" debug_print('UserTemplateChoice.load [{0}]'.format(path)) self._current = self._load(path) QSettings().setValue(self.PATH_KEY, str(path)) QSettings().setValue(self.DIRECTORY_KEY, str(Path(path).parent)) self.template_changed.emit()
def from_document(self, document): """Load data from document """ # Load the new data # Document promises that either the thumbnail or scanned image will be # available if document.thumbnail.available: debug_print('Model will work on thumbnail') image_array = document.thumbnail.array else: debug_print('Model will work on full-res scan') image_array = document.scanned.array pixmap = QPixmap.fromImage(qimage_of_bgr(image_array)) if pixmap.isNull(): raise ValueError('Unable to create QPixmap') else: data = self._boxes_from_items( document.items, pixmap.width(), pixmap.height() ) # Inform views self.beginResetModel() self._data, self._image_array, self._pixmap = data, image_array, pixmap self.endResetModel()
def keyPressEvent(self, event): """QGraphicsScene virtual """ debug_print('BoxesScene.keyPressEvent') key = event.key() # Mapping from cursor key to adjustment (dx1, dy1, dx2, dy2) cursors = { Qt.Key_Up: ( 0.0,-1.0, 0.0,-1.0), Qt.Key_Right: ( 1.0, 0.0, 1.0, 0.0), Qt.Key_Down: ( 0.0, 1.0, 0.0, 1.0), Qt.Key_Left: (-1.0, 0.0,-1.0, 0.0), } if key in cursors: event.accept() dx1, dy1, dx2, dy2 = cursors[key] modifiers = event.modifiers() if Qt.ShiftModifier & modifiers and not Qt.AltModifier & modifiers: # Adjust the bottom-right corner dx1 = dy1 = 0.0 elif not Qt.ShiftModifier & modifiers and Qt.AltModifier & modifiers: # Adjust the top-left corner dx2 = dy2 = 0.0 if event.isAutoRepeat(): # Larger steps when key is being held down multiplier = 4 dx1, dy1, dx2, dy2 = [v * multiplier for v in (dx1, dy1, dx2, dy2)] self.adjust_selected(dx1, dy1, dx2, dy2) else: super(BoxesScene, self).keyPressEvent(event)
def _set_message(self, message): """Slot for self.new_message """ debug_print("WorkerThread._set_message [{0}]".format(message)) # Must only be called from the main thread assert (self.thread() == QtCore.QThread.currentThread()) self._progress_box.setLabelText(message)
def mouseReleaseEvent(self, event): """QGraphicsItem virtual """ debug_print('ResizeHandle.mouseReleaseEvent') # Redraw parent and corners self.parentItem().update()
def new_absolute_zoom(self, factor): """Sets a new absolute zoom """ f = factor scene_rect = self.scene().sceneRect() # Scene view_rect = self.viewport().rect() # Available space # The size of the scene if the new transform is applied t_scene_rect = QtGui.QTransform.fromScale(f, f).mapRect(scene_rect) if (t_scene_rect.width() < view_rect.width() and t_scene_rect.height() < view_rect.height()): # The user wants to zoom out so that the image is smaller than the # view self.zoom_home() else: f = min(self.MAXIMUM_ZOOM, f) msg = 'Change absolute zoom from [{0}] to [{1}]' debug_print(msg.format(self.absolute_zoom, f)) self.setTransform(QtGui.QTransform.fromScale(f, f)) self.fit_to_view = False selected = self.scene().selectedItems() if selected: # Centre on selected items #self.ensureVisible(unite_rects([i.rect() for i in selected])) self.centerOn( unite_rects([i.rect() for i in selected]).center())
def wheelEvent(self, event): """QGraphicsView virtual """ if Qt.ControlModifier == event.modifiers( ) and not self.scene().is_empty: event.accept() # Wheel event delta is in units of 1/8 of a degree degrees = 8 * event.delta() # Compute a relative scale factor # Multiplier determined by experimenting with a mac trackpad and a # cheap Logitech wheel mouse multiplier = 0.0005 f = 1.0 + degrees * multiplier if 0 < f < 2: debug_print( 'BoxesView.wheelEvent delta degrees [{0}] factor [{1}]'. format(degrees, f)) self.new_relative_zoom(f) else: pass # Extremely large wheel delta else: super(BoxesView, self).wheelEvent(event)
def selectionChanged(self, selected, deselected): """QAbstractItemView virtual """ # Tell the scene about the new selection # TODO LH Use a timer to implement a delayed refresh if not self.handling_selection_update: # TODO Context for this debug_print('GraphicsItemView.selectionChanged') self.handling_selection_update = True try: current = set(self.scene.selectedItems()) new = set(self._rows[i.row()] for i in self.selectionModel().selectedIndexes()) for item in new.difference(current): item.setSelected(True) item.update() for item in current.difference(new): item.setSelected(False) item.update() if 1 == len(new): rect = new.pop().rect() for view in self.scene.views(): view.centerOn(rect.center()) elif 1 < len(new): # Ensure that the selected items are visible rect = unite_rects([i.rect() for i in new]) debug_print('GraphicsItemView will make visible', rect) new.pop().ensureVisible(rect) finally: self.handling_selection_update = False
def _close_group(self, main_layout, group_name, group_layout): """Closes the the existing group, used during controls creation """ debug_print('FormContainer._close_group close group', group_name) # The widget that holds this group's controls controls_widget = QWidget() controls_widget.setLayout(group_layout) if group_name: # Group controls start out hidden controls_widget.setVisible(False) # The group box, which contains the label to toggle the controls # and the controls themselves group_box_layout = QVBoxLayout() group_box_layout.addWidget( ToggleWidgetLabel(group_name, controls_widget, initially_visible=False)) group_box_layout.addWidget(controls_widget) group_box_layout.setContentsMargins( 0, # left 0, # top 0, # right 0 # bottom ) group_box = QGroupBox() group_box.setLayout(group_box_layout) # Add the group box to the main layout main_layout.addRow(group_box) else: # current group has no name and therefore no toggle group main_layout.addRow(controls_widget)
def _close_group(self, main_layout, group_name, group_layout): """Closes the the existing group, used during controls creation """ debug_print('FormContainer._close_group close group', group_name) # The widget that holds this group's controls controls_widget = QWidget() controls_widget.setLayout(group_layout) if group_name: # Group controls start out hidden controls_widget.setVisible(False) # The group box, which contains the label to toggle the controls # and the controls themselves group_box_layout = QVBoxLayout() group_box_layout.addWidget(ToggleWidgetLabel( group_name, controls_widget, initially_visible=False )) group_box_layout.addWidget(controls_widget) group_box_layout.setContentsMargins( 0, # left 0, # top 0, # right 0 # bottom ) group_box = QGroupBox() group_box.setLayout(group_box_layout) # Add the group box to the main layout main_layout.addRow(group_box) else: # current group has no name and therefore no toggle group main_layout.addRow(controls_widget)
def _set_message(self, message): """Slot for self.new_message """ debug_print("WorkerThread._set_message [{0}]".format(message)) # Must only be called from the main thread assert(self.thread() == QtCore.QThread.currentThread()) self._progress_box.setLabelText(message)
def selectionChanged(self, selected, deselected): """QAbstractItemView virtual """ # Tell the scene about the new selection # TODO LH Use a timer to implement a delayed refresh if not self.handling_selection_update: # TODO Context for this debug_print('GraphicsItemView.selectionChanged') self.handling_selection_update = True try: current = set(self.scene.selectedItems()) new = set(self._rows[i.row()] for i in self.selectionModel().selectedIndexes()) for item in new.difference(current): item.setSelected(True) item.update() for item in current.difference(new): item.setSelected(False) item.update() if new: for view in self.scene.views(): view.zoom_to_items(new) finally: self.handling_selection_update = False
def close_document(self): """Closes the document and returns True if not modified or if modified and user does not cancel. Does not close the document and returns False if modified and users cancels. """ debug_print('MainWindow.close_document') if self.model.modified: # Ask the user if they work like to save before closing res = QMessageBox.question( self, 'Save document?', 'Save the document before closing?', (QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel), QMessageBox.Yes) if QMessageBox.Yes == res: self.save_document() # Answering Yes or No means the document will be closed close = QMessageBox.Cancel != res else: # The document is not modified so it is OK to close it close = True if close: self.empty_document() return close
def keyPressEvent(self, event): """QGraphicsScene virtual """ debug_print('BoxesScene.keyPressEvent') key = event.key() # Mapping from cursor key to adjustment (dx1, dy1, dx2, dy2) cursors = { Qt.Key_Up: ( 0.0,-1.0, 0.0,-1.0), Qt.Key_Right: ( 1.0, 0.0, 1.0, 0.0), Qt.Key_Down: ( 0.0, 1.0, 0.0, 1.0), Qt.Key_Left: (-1.0, 0.0,-1.0, 0.0), } if key in cursors.keys(): event.accept() dx1, dy1, dx2, dy2 = cursors[key] mod = event.modifiers() if Qt.ShiftModifier == mod: # Shift - move just the bottom-right corner dx1 = dy1 = 0.0 elif Qt.ControlModifier == mod: # Control - move just the top-left corner dx2 = dy2 = 0.0 self.adjust_selected(dx1, dy1, dx2, dy2) else: super(BoxesScene, self).keyPressEvent(event)
def save_crops(self): """Saves cropped specimen images """ debug_print('MainWindow.save_crops') res = QMessageBox.Yes existing_crops = self.document.crops_dir.is_dir() if existing_crops: msg = 'Overwrite the existing cropped specimen images?' res = QMessageBox.question(self, 'Write cropped specimen images?', msg, QMessageBox.No, QMessageBox.Yes) if QMessageBox.Yes == res: def save_crops(progress): progress('Loading full-resolution scanned image') self.document.scanned.array progress('Saving crops') self.document.save_crops(progress) def completed(operation): QMessageBox.information(self, "Crops saved", msg) self.model.to_document(self.document) msg = "{0} crops saved in {1}" msg = msg.format(self.document.n_items, self.document.crops_dir) self.run_in_worker(save_crops, 'Save crops', completed)
def insertRows(self, row, count, parent=QModelIndex()): """QAbstractItemModel virtual """ debug_print('Model.insertRows row [{0}] count [{1}]'.format(row, count)) if row < 0 or row > len(self._data) or count < 1: raise ValueError('Bad row [{0}] or count [{1}]'.format(row, count)) else: upper = row + count - 1 self.beginInsertRows(QModelIndex(), row, upper) # Create list of new rows. Cannot use [{whatever}] * count because # this will create the same dict instance repeated 'count' times, # not 'count' different dict instances new_rows = [None] * count for i in range(0, count): new_rows[i] = { "fields": {}, "rect": QRect(0, 0, 0, 0), "rotation": 0 } self._data[row:row] = new_rows self.set_modified(True) self.endInsertRows() self.dataChanged.emit(self.index(row, 0), self.index(upper, 0)) return True
def export_csv(dir, overwrite_existing, template): dir = Path(dir) export = DocumentExport(UserTemplate.load(template) if template else DWC) for p in dir.glob('*' + InselectDocument.EXTENSION): try: debug_print('Loading [{0}]'.format(p)) doc = InselectDocument.load(p) validation = export.validation_problems(doc) csv_path = export.csv_path(doc) if validation.any_problems: print( 'Not exporting metadata for [{0}] because there are ' 'validation problems'.format(p) ) for msg in format_validation_problems(validation): print(msg) elif not overwrite_existing and csv_path.is_file(): print('CSV file [{0}] exists - skipping'.format(csv_path)) else: print('Writing CSV for [{0}]'.format(p)) export.export_csv(doc) except KeyboardInterrupt: raise except Exception: print('Error saving CSV from [{0}]'.format(p)) traceback.print_exc()
def create_and_use(self, boxes, path): """Creates a new CookieCutter file that contains boxes, writes in to path and sets it to be the current choice """ debug_print('CookieCutterChoice.create_and_use to [{0}]'.format(path)) cookie_cutter = CookieCutter('', boxes) cookie_cutter.save(path) self.load(path)
def changed(self): "Slot for UserTemplateChoice.template_changed" debug_print('UserTemplateWidget.changed') choice = user_template_choice() self.setText(choice.current.name) self._default_action.setEnabled(not choice.current_is_default) self._refresh_action.setEnabled(not choice.current_is_default) self._reveal_template_action.setEnabled(not choice.current_is_default)
def dragEnterEvent(self, event): """QWidget virtual """ debug_print('MainWindow.dragEnterEvent') if self._accept_drag_drop(event): event.acceptProposedAction() else: super(MainWindow, self).dragEnterEvent(event)
def append_point_of_interest(self, pos): """Appends pos (a QPoint relative to the top-left of this box) to the list of points of interest """ debug_print('New point of interest at [{0}]'.format(pos)) self._pois.append(Reticle(pos - self.boundingRect().topLeft(), self)) self._pois[-1].layout(self.boundingRect()) self._pois[-1].setFlags(QGraphicsItem.ItemIgnoresTransformations)
def selectionChanged(self, selected, deselected): """QAbstractItemView slot """ debug_print('SpecimenView.selectionChanged') # Grid view unless exactly one item selected if self.expanded and 1 != len(self.selectionModel().selectedIndexes()): self.show_grid()
def mouseReleaseEvent(self, event): """QGraphicsRectItem virtual """ debug_print('BoxItem.mouseReleaseEvent') super(BoxItem, self).mouseReleaseEvent(event) self.setCursor(Qt.OpenHandCursor) self._set_z_index() self.update()
def user_template_changed(self): """Informs the model that the user's choice of metadata template has changed. Informs all views. """ debug_print('Model.user_template_changed') if self._data: self.dataChanged.emit(self.index(0, 0), self.index(self.rowCount()-1, 0))
def rowsAboutToBeRemoved(self, parent, start, end): """QAbstractItemView slot """ debug_print('SummaryView.rowsAboutToBeRemoved') self._updated( self.model().rowCount() - (end - start), self.selectionModel().selectedIndexes() )
def hoverLeaveEvent(self, event): """QGraphicsRectItem virtual """ debug_print('BoxItem.hoverLeaveEvent') super(BoxItem, self).hoverLeaveEvent(event) self._set_handles_visible(False) self._set_z_index() self.update()
def reset(self): """QAbstractItemView virtual """ debug_print('MetadataView.reset') super(MetadataView, self).reset() # Clear the controls self.selectionChanged([], [])
def _user_selected_item(self): """The user changed the selected item """ debug_print('FieldComboBox._user_selected_item', self._field) value = self.itemData(self.currentIndex()) new = {self._field: value} for i in self.selected: i.model().setData(i, new, MetadataRole)
def load(self, path): """Loads the CookieCutter in path, updates settings and emits cookie_cutter_changed """ debug_print('CookieCutterChoice.load [{0}]'.format(path)) self._current = self._load(path) QSettings().setValue(self.PATH_KEY, str(path)) QSettings().setValue(self.DIRECTORY_KEY, str(Path(path).parent)) self.cookie_cutter_changed.emit()
def toggle_expanded(self, index): """Selects 'index' and toggles the expanded state """ debug_print('ObjectView.toggle_expanded') self.selectionModel().select(index, QItemSelectionModel.Select) if self.expanded: self.show_grid() else: self.show_expanded()
def __init__(self): super(UserTemplateChoice, self).__init__() self._current = self.DEFAULT previous = QSettings().value(self.PATH_KEY) if previous: try: self._current = self._load(previous) except Exception: debug_print('Error loading user template [{0}]'.format(previous))
def selectionChanged(self, selected, deselected): "QAbstractItemView slot" debug_print('MetadataView.selectionChanged') # If one of our controls has focus, update the model before refreshing # the UI if QtWidgets.qApp.focusWidget() in self._form_container.controls: QtWidgets.qApp.focusWidget().update_model() self._populate_controls()
def toggle_expanded(self, index): """Selects 'index' and toggles the expanded state """ debug_print('ObjectView.toggle_expanded') self.selectionModel().select(index, QItemSelectionModel.Select) if self.expanded_action.isChecked(): self.grid_action.trigger() else: self.expanded_action.trigger()
def selectionChanged(self, selected, deselected): """QAbstractItemView slot """ debug_print('ObjectView.selectionChanged') # Grid view unless exactly one item selected if self.expanded and 1 != len(self.selectionModel().selectedIndexes()): self.show_grid() super(ObjectView, self).selectionChanged(selected, deselected)
def show_alternative_pixmap(self, pixmap): """Show or clear an alternative pixmap in place of the document's usual pixmap. pixmaps should either be a QPixmap of the same dimensions as the documents pixmap (which is shown) or None (which clears any existing alternative pixmap) """ debug_print('show_alternative_pixmap', pixmap) model = self.model() pixmap = pixmap if pixmap else model.data(QModelIndex(), PixmapRole) self.scene.set_pixmap(pixmap)