butterfly_viewer.aux_scenes
QGraphicsScene with signals and right-click functionality for SplitView.
Not intended as a script.
Creates the base (main) scene of the SplitView for the Butterfly Viewer and Registrator.
1#!/usr/bin/env python3 2 3"""QGraphicsScene with signals and right-click functionality for SplitView. 4 5Not intended as a script. 6 7Creates the base (main) scene of the SplitView for the Butterfly Viewer and Registrator. 8""" 9# SPDX-License-Identifier: GPL-3.0-or-later 10 11 12 13from PyQt5 import QtCore, QtWidgets 14 15from aux_comments import CommentItem 16from aux_rulers import RulerItem 17from aux_dialogs import PixelUnitConversionInputDialog 18 19 20 21class CustomQGraphicsScene(QtWidgets.QGraphicsScene): 22 """QGraphicsScene with signals and right-click functionality for SplitView. 23 24 Recommended to be instantiated without input (for example, my_scene = CustomQGraphicsScene()) 25 26 Signals for right click menu for comments (create comment, save comments, load comments). 27 Signals for right click menu for rulers (create ruler, set origin relative position, set px-per-unit conversion) 28 Signals for right click menu for transform mode (interpolate, non-interpolate) 29 Methods for right click menu. 30 31 Args: 32 Identical to base class QGraphicsScene. 33 """ 34 def __init__(self, *args, **kwargs): 35 super().__init__(*args, **kwargs) 36 37 self.px_conversion = 1.0 38 self.unit_conversion = 1.0 39 self.px_per_unit = 1.0 40 self.px_per_unit_conversion_set = False 41 self.relative_origin_position = "bottomleft" 42 self.single_transform_mode_smooth = False 43 44 self.background_colors = [["Dark gray (default)", 32, 32, 32], 45 ["White", 255, 255, 255], 46 ["Light gray", 223, 223, 223], 47 ["Black", 0, 0, 0]] 48 self._background_color = self.background_colors[0] 49 50 self.disable_right_click = False 51 52 right_click_comment = QtCore.pyqtSignal(QtCore.QPointF) 53 right_click_ruler = QtCore.pyqtSignal(QtCore.QPointF, str, str, float) # Scene position, relative origin position, unit, px-per-unit 54 right_click_save_all_comments = QtCore.pyqtSignal() 55 right_click_load_comments = QtCore.pyqtSignal() 56 right_click_relative_origin_position = QtCore.pyqtSignal(str) 57 changed_px_per_unit = QtCore.pyqtSignal(str, float) # Unit, px-per-unit 58 right_click_single_transform_mode_smooth = QtCore.pyqtSignal(bool) 59 right_click_all_transform_mode_smooth = QtCore.pyqtSignal(bool) 60 right_click_background_color = QtCore.pyqtSignal(list) 61 position_changed_qgraphicsitem = QtCore.pyqtSignal() 62 63 def contextMenuEvent(self, event): 64 """Override the event of the context menu (right-click menu) to display options. 65 66 Triggered when mouse is right-clicked on scene. 67 68 Args: 69 event (PyQt event for contextMenuEvent) 70 """ 71 if self.disable_right_click: 72 return 73 74 what_menu_type = "View" 75 76 scene_pos = event.scenePos() 77 item = self.itemAt(scene_pos, self.views()[0].transform()) 78 79 action_delete = None 80 menu_set_color = None 81 action_set_color_red = None 82 action_set_color_white = None 83 action_set_color_blue = None 84 action_set_color_green = None 85 action_set_color_yellow = None 86 action_set_color_black = None 87 88 item_parent = item 89 if item is not None: 90 while item_parent.parentItem(): # Loop "upwards" to find parent item 91 item_parent = item_parent.parentItem() 92 93 if isinstance(item_parent, CommentItem) or isinstance(item_parent, RulerItem): 94 action_delete = QtWidgets.QAction("Delete") 95 96 if isinstance(item_parent, CommentItem): 97 menu_set_color = QtWidgets.QMenu("Set comment color...") 98 action_set_color_red = menu_set_color.addAction("Red") 99 action_set_color_red.triggered.connect(lambda: item_parent.set_color("red")) 100 action_set_color_white = menu_set_color.addAction("White") 101 action_set_color_white.triggered.connect(lambda: item_parent.set_color("white")) 102 action_set_color_blue = menu_set_color.addAction("Blue") 103 action_set_color_blue.triggered.connect(lambda: item_parent.set_color("blue")) 104 action_set_color_green = menu_set_color.addAction("Green") 105 action_set_color_green.triggered.connect(lambda: item_parent.set_color("green")) 106 action_set_color_yellow = menu_set_color.addAction("Yellow") 107 action_set_color_yellow.triggered.connect(lambda: item_parent.set_color("yellow")) 108 action_set_color_black = menu_set_color.addAction("Black") 109 action_set_color_black.triggered.connect(lambda: item_parent.set_color("black")) 110 111 action_delete.triggered.connect(lambda: self.removeItem(item_parent)) 112 113 what_menu_type = "Edit item(s)" 114 115 menu = QtWidgets.QMenu() 116 117 if what_menu_type == "Edit item(s)": 118 if menu_set_color: 119 menu.addMenu(menu_set_color) 120 if action_delete: 121 menu.addAction(action_delete) # action_delete.triggered.connect(lambda: self.removeItem(item.parentItem())) # = menu.addAction("Delete", self.removeItem(item.parentItem())) 122 else: 123 action_comment = menu.addAction("Comment") 124 action_comment.setToolTip("Add a draggable text comment here") 125 action_comment.triggered.connect(lambda: self.right_click_comment.emit(scene_pos)) # action_comment.triggered.connect(lambda state, x=scene_pos: self.right_click_comment.emit(x)) 126 127 menu_ruler = QtWidgets.QMenu("Measurement ruler...") 128 menu_ruler.setToolTip("Add a ruler to measure distances and angles in this image window...") 129 menu_ruler.setToolTipsVisible(True) 130 menu.addMenu(menu_ruler) 131 132 action_set_px_per_mm = menu_ruler.addAction("Set the ruler conversion factor for real distances (mm, cm)...") 133 action_set_px_per_mm.triggered.connect(lambda: self.dialog_to_set_px_per_mm()) 134 135 menu_ruler.addSeparator() 136 137 action_ruler_px = menu_ruler.addAction("Pixel ruler") 138 action_ruler_px.setToolTip("Add a ruler to measure distances in pixels") 139 action_ruler_px.triggered.connect(lambda: self.right_click_ruler.emit(scene_pos, self.relative_origin_position, "px", 1.0)) 140 141 action_ruler_mm = menu_ruler.addAction("Millimeter ruler") 142 action_ruler_mm.setToolTip("Add a ruler to measure distances in millimeters") 143 action_ruler_mm.triggered.connect(lambda: self.right_click_ruler.emit(scene_pos, self.relative_origin_position, "mm", self.px_per_unit)) 144 145 action_ruler_cm = menu_ruler.addAction("Centimeter ruler") 146 action_ruler_cm.setToolTip("Add a ruler to measure distances in centimeters") 147 action_ruler_cm.triggered.connect(lambda: self.right_click_ruler.emit(scene_pos, self.relative_origin_position, "cm", self.px_per_unit*10)) 148 149 if not self.px_per_unit_conversion_set: 150 text_disclaimer = "(requires conversion to be set before using)" 151 tooltip_disclaimer = "To use this ruler, first set the ruler conversion factor" 152 153 action_ruler_mm.setEnabled(False) 154 action_ruler_mm.setText(action_ruler_mm.text() + " " + text_disclaimer) 155 action_ruler_mm.setToolTip(tooltip_disclaimer) 156 157 action_ruler_cm.setEnabled(False) 158 action_ruler_cm.setText(action_ruler_cm.text() + " " + text_disclaimer) 159 action_ruler_cm.setToolTip(tooltip_disclaimer) 160 161 menu_ruler.addSeparator() 162 163 action_set_relative_origin_position_topleft = menu_ruler.addAction("Switch relative origin to top-left") 164 action_set_relative_origin_position_topleft.triggered.connect(lambda: self.right_click_relative_origin_position.emit("topleft")) 165 action_set_relative_origin_position_topleft.triggered.connect(lambda: self.set_relative_origin_position("topleft")) 166 action_set_relative_origin_position_bottomleft = menu_ruler.addAction("Switch relative origin to bottom-left") 167 action_set_relative_origin_position_bottomleft.triggered.connect(lambda: self.right_click_relative_origin_position.emit("bottomleft")) 168 action_set_relative_origin_position_bottomleft.triggered.connect(lambda: self.set_relative_origin_position("bottomleft")) 169 170 if self.relative_origin_position == "bottomleft": 171 action_set_relative_origin_position_bottomleft.setEnabled(False) 172 elif self.relative_origin_position == "topleft": 173 action_set_relative_origin_position_topleft.setEnabled(False) 174 175 menu.addSeparator() 176 177 action_save_all_comments = menu.addAction("Save all comments of this view (.csv)...") 178 action_save_all_comments.triggered.connect(lambda: self.right_click_save_all_comments.emit()) 179 action_load_comments = menu.addAction("Load comments into this view (.csv)...") 180 action_load_comments.triggered.connect(lambda: self.right_click_load_comments.emit()) 181 182 menu.addSeparator() 183 184 menu_transform = QtWidgets.QMenu("Upsample when zoomed...") 185 menu_transform.setToolTipsVisible(True) 186 menu.addMenu(menu_transform) 187 188 transform_on_tooltip_str = "Pixels are interpolated when zoomed in, thus rendering a smooth appearance" 189 transform_off_tooltip_str = "Pixels are unchanged when zoomed in, thus rendering a true-to-pixel appearance" 190 191 action_set_single_transform_mode_smooth_on = menu_transform.addAction("Switch on") 192 action_set_single_transform_mode_smooth_on.setToolTip(transform_on_tooltip_str) 193 action_set_single_transform_mode_smooth_on.triggered.connect(lambda: self.right_click_single_transform_mode_smooth.emit(True)) 194 action_set_single_transform_mode_smooth_on.triggered.connect(lambda: self.set_single_transform_mode_smooth(True)) 195 196 action_set_single_transform_mode_smooth_off = menu_transform.addAction("Switch off") 197 action_set_single_transform_mode_smooth_off.setToolTip(transform_off_tooltip_str) 198 action_set_single_transform_mode_smooth_off.triggered.connect(lambda: self.right_click_single_transform_mode_smooth.emit(False)) 199 action_set_single_transform_mode_smooth_off.triggered.connect(lambda: self.set_single_transform_mode_smooth(False)) 200 201 if self.single_transform_mode_smooth: 202 action_set_single_transform_mode_smooth_on.setEnabled(False) 203 else: 204 action_set_single_transform_mode_smooth_off.setEnabled(False) 205 206 menu_transform.addSeparator() 207 208 action_set_all_transform_mode_smooth_on = menu_transform.addAction("Switch on (all windows)") 209 action_set_all_transform_mode_smooth_on.setToolTip(transform_on_tooltip_str+" (applies to all current and new image windows)") 210 action_set_all_transform_mode_smooth_on.triggered.connect(lambda: self.right_click_all_transform_mode_smooth.emit(True)) 211 212 action_set_all_transform_mode_smooth_off = menu_transform.addAction("Switch off (all windows)") 213 action_set_all_transform_mode_smooth_off.setToolTip(transform_off_tooltip_str+" (applies to all current and new image windows)") 214 action_set_all_transform_mode_smooth_off.triggered.connect(lambda: self.right_click_all_transform_mode_smooth.emit(False)) 215 216 menu.addSeparator() 217 218 menu_background = QtWidgets.QMenu("Set background color...") 219 menu_background.setToolTipsVisible(True) 220 menu.addMenu(menu_background) 221 222 for color in self.background_colors: 223 descriptor = color[0] 224 rgb = color[1:4] 225 action_set_background = menu_background.addAction(descriptor) 226 action_set_background.setToolTip("RGB " + ", ".join([str(channel) for channel in rgb])) 227 action_set_background.triggered.connect(lambda value, color=color: self.right_click_background_color.emit(color)) 228 action_set_background.triggered.connect(lambda value, color=color: self.background_color_lambda(color)) 229 if color == self.background_color: 230 action_set_background.setEnabled(False) 231 232 menu.exec(event.screenPos()) 233 234 def set_relative_origin_position(self, string): 235 """Set the descriptor of the position of the relative origin for rulers. 236 237 Describes the coordinate orientation: 238 "bottomleft" for Cartesian-style (positive X right, positive Y up) 239 "topleft" for image-style (positive X right, positive Y down) 240 241 Args: 242 string (str): "topleft" or "bottomleft" for position of the origin for coordinate system of rulers. 243 """ 244 self.relative_origin_position = string 245 246 def set_single_transform_mode_smooth(self, boolean): 247 """Set the descriptor of the status of smooth transform mode. 248 249 Describes the transform mode of pixels on zoom: 250 True for smooth (interpolated) 251 False for non-smooth (non-interpolated) 252 253 Args: 254 boolean (bool): True for smooth; False for non-smooth. 255 """ 256 self.single_transform_mode_smooth = boolean 257 258 def dialog_to_set_px_per_mm(self): 259 """Open the dialog for users to set the conversion for pixels to millimeters. 260 261 Emits the value of the px-per-mm conversion if user clicks "Ok" on dialog. 262 """ 263 dialog_window = PixelUnitConversionInputDialog(unit="mm", px_conversion=self.px_conversion, unit_conversion=self.unit_conversion, px_per_unit=self.px_per_unit) 264 dialog_window.setWindowModality(QtCore.Qt.ApplicationModal) 265 if dialog_window.exec_() == QtWidgets.QDialog.Accepted: 266 self.px_per_unit = dialog_window.px_per_unit 267 if self.px_per_unit_conversion_set: 268 self.changed_px_per_unit.emit("mm", self.px_per_unit) 269 self.px_per_unit_conversion_set = True 270 self.px_conversion = dialog_window.px_conversion 271 self.unit_conversion = dialog_window.unit_conversion 272 273 @property 274 def background_color(self): 275 """Current background color.""" 276 return self._background_color 277 278 @background_color.setter 279 def background_color(self, color): 280 """Set color as list with descriptor and RGB values [str, r, g, b].""" 281 self._background_color = color 282 283 def background_color_lambda(self, color): 284 """Within lambda, set color as list with descriptor and RGB values [str, r, g, b].""" 285 self.background_color = color 286 287 @property 288 def background_rgb(self): 289 """Current background color RGB.""" 290 return self._background_color[1:4]
22class CustomQGraphicsScene(QtWidgets.QGraphicsScene): 23 """QGraphicsScene with signals and right-click functionality for SplitView. 24 25 Recommended to be instantiated without input (for example, my_scene = CustomQGraphicsScene()) 26 27 Signals for right click menu for comments (create comment, save comments, load comments). 28 Signals for right click menu for rulers (create ruler, set origin relative position, set px-per-unit conversion) 29 Signals for right click menu for transform mode (interpolate, non-interpolate) 30 Methods for right click menu. 31 32 Args: 33 Identical to base class QGraphicsScene. 34 """ 35 def __init__(self, *args, **kwargs): 36 super().__init__(*args, **kwargs) 37 38 self.px_conversion = 1.0 39 self.unit_conversion = 1.0 40 self.px_per_unit = 1.0 41 self.px_per_unit_conversion_set = False 42 self.relative_origin_position = "bottomleft" 43 self.single_transform_mode_smooth = False 44 45 self.background_colors = [["Dark gray (default)", 32, 32, 32], 46 ["White", 255, 255, 255], 47 ["Light gray", 223, 223, 223], 48 ["Black", 0, 0, 0]] 49 self._background_color = self.background_colors[0] 50 51 self.disable_right_click = False 52 53 right_click_comment = QtCore.pyqtSignal(QtCore.QPointF) 54 right_click_ruler = QtCore.pyqtSignal(QtCore.QPointF, str, str, float) # Scene position, relative origin position, unit, px-per-unit 55 right_click_save_all_comments = QtCore.pyqtSignal() 56 right_click_load_comments = QtCore.pyqtSignal() 57 right_click_relative_origin_position = QtCore.pyqtSignal(str) 58 changed_px_per_unit = QtCore.pyqtSignal(str, float) # Unit, px-per-unit 59 right_click_single_transform_mode_smooth = QtCore.pyqtSignal(bool) 60 right_click_all_transform_mode_smooth = QtCore.pyqtSignal(bool) 61 right_click_background_color = QtCore.pyqtSignal(list) 62 position_changed_qgraphicsitem = QtCore.pyqtSignal() 63 64 def contextMenuEvent(self, event): 65 """Override the event of the context menu (right-click menu) to display options. 66 67 Triggered when mouse is right-clicked on scene. 68 69 Args: 70 event (PyQt event for contextMenuEvent) 71 """ 72 if self.disable_right_click: 73 return 74 75 what_menu_type = "View" 76 77 scene_pos = event.scenePos() 78 item = self.itemAt(scene_pos, self.views()[0].transform()) 79 80 action_delete = None 81 menu_set_color = None 82 action_set_color_red = None 83 action_set_color_white = None 84 action_set_color_blue = None 85 action_set_color_green = None 86 action_set_color_yellow = None 87 action_set_color_black = None 88 89 item_parent = item 90 if item is not None: 91 while item_parent.parentItem(): # Loop "upwards" to find parent item 92 item_parent = item_parent.parentItem() 93 94 if isinstance(item_parent, CommentItem) or isinstance(item_parent, RulerItem): 95 action_delete = QtWidgets.QAction("Delete") 96 97 if isinstance(item_parent, CommentItem): 98 menu_set_color = QtWidgets.QMenu("Set comment color...") 99 action_set_color_red = menu_set_color.addAction("Red") 100 action_set_color_red.triggered.connect(lambda: item_parent.set_color("red")) 101 action_set_color_white = menu_set_color.addAction("White") 102 action_set_color_white.triggered.connect(lambda: item_parent.set_color("white")) 103 action_set_color_blue = menu_set_color.addAction("Blue") 104 action_set_color_blue.triggered.connect(lambda: item_parent.set_color("blue")) 105 action_set_color_green = menu_set_color.addAction("Green") 106 action_set_color_green.triggered.connect(lambda: item_parent.set_color("green")) 107 action_set_color_yellow = menu_set_color.addAction("Yellow") 108 action_set_color_yellow.triggered.connect(lambda: item_parent.set_color("yellow")) 109 action_set_color_black = menu_set_color.addAction("Black") 110 action_set_color_black.triggered.connect(lambda: item_parent.set_color("black")) 111 112 action_delete.triggered.connect(lambda: self.removeItem(item_parent)) 113 114 what_menu_type = "Edit item(s)" 115 116 menu = QtWidgets.QMenu() 117 118 if what_menu_type == "Edit item(s)": 119 if menu_set_color: 120 menu.addMenu(menu_set_color) 121 if action_delete: 122 menu.addAction(action_delete) # action_delete.triggered.connect(lambda: self.removeItem(item.parentItem())) # = menu.addAction("Delete", self.removeItem(item.parentItem())) 123 else: 124 action_comment = menu.addAction("Comment") 125 action_comment.setToolTip("Add a draggable text comment here") 126 action_comment.triggered.connect(lambda: self.right_click_comment.emit(scene_pos)) # action_comment.triggered.connect(lambda state, x=scene_pos: self.right_click_comment.emit(x)) 127 128 menu_ruler = QtWidgets.QMenu("Measurement ruler...") 129 menu_ruler.setToolTip("Add a ruler to measure distances and angles in this image window...") 130 menu_ruler.setToolTipsVisible(True) 131 menu.addMenu(menu_ruler) 132 133 action_set_px_per_mm = menu_ruler.addAction("Set the ruler conversion factor for real distances (mm, cm)...") 134 action_set_px_per_mm.triggered.connect(lambda: self.dialog_to_set_px_per_mm()) 135 136 menu_ruler.addSeparator() 137 138 action_ruler_px = menu_ruler.addAction("Pixel ruler") 139 action_ruler_px.setToolTip("Add a ruler to measure distances in pixels") 140 action_ruler_px.triggered.connect(lambda: self.right_click_ruler.emit(scene_pos, self.relative_origin_position, "px", 1.0)) 141 142 action_ruler_mm = menu_ruler.addAction("Millimeter ruler") 143 action_ruler_mm.setToolTip("Add a ruler to measure distances in millimeters") 144 action_ruler_mm.triggered.connect(lambda: self.right_click_ruler.emit(scene_pos, self.relative_origin_position, "mm", self.px_per_unit)) 145 146 action_ruler_cm = menu_ruler.addAction("Centimeter ruler") 147 action_ruler_cm.setToolTip("Add a ruler to measure distances in centimeters") 148 action_ruler_cm.triggered.connect(lambda: self.right_click_ruler.emit(scene_pos, self.relative_origin_position, "cm", self.px_per_unit*10)) 149 150 if not self.px_per_unit_conversion_set: 151 text_disclaimer = "(requires conversion to be set before using)" 152 tooltip_disclaimer = "To use this ruler, first set the ruler conversion factor" 153 154 action_ruler_mm.setEnabled(False) 155 action_ruler_mm.setText(action_ruler_mm.text() + " " + text_disclaimer) 156 action_ruler_mm.setToolTip(tooltip_disclaimer) 157 158 action_ruler_cm.setEnabled(False) 159 action_ruler_cm.setText(action_ruler_cm.text() + " " + text_disclaimer) 160 action_ruler_cm.setToolTip(tooltip_disclaimer) 161 162 menu_ruler.addSeparator() 163 164 action_set_relative_origin_position_topleft = menu_ruler.addAction("Switch relative origin to top-left") 165 action_set_relative_origin_position_topleft.triggered.connect(lambda: self.right_click_relative_origin_position.emit("topleft")) 166 action_set_relative_origin_position_topleft.triggered.connect(lambda: self.set_relative_origin_position("topleft")) 167 action_set_relative_origin_position_bottomleft = menu_ruler.addAction("Switch relative origin to bottom-left") 168 action_set_relative_origin_position_bottomleft.triggered.connect(lambda: self.right_click_relative_origin_position.emit("bottomleft")) 169 action_set_relative_origin_position_bottomleft.triggered.connect(lambda: self.set_relative_origin_position("bottomleft")) 170 171 if self.relative_origin_position == "bottomleft": 172 action_set_relative_origin_position_bottomleft.setEnabled(False) 173 elif self.relative_origin_position == "topleft": 174 action_set_relative_origin_position_topleft.setEnabled(False) 175 176 menu.addSeparator() 177 178 action_save_all_comments = menu.addAction("Save all comments of this view (.csv)...") 179 action_save_all_comments.triggered.connect(lambda: self.right_click_save_all_comments.emit()) 180 action_load_comments = menu.addAction("Load comments into this view (.csv)...") 181 action_load_comments.triggered.connect(lambda: self.right_click_load_comments.emit()) 182 183 menu.addSeparator() 184 185 menu_transform = QtWidgets.QMenu("Upsample when zoomed...") 186 menu_transform.setToolTipsVisible(True) 187 menu.addMenu(menu_transform) 188 189 transform_on_tooltip_str = "Pixels are interpolated when zoomed in, thus rendering a smooth appearance" 190 transform_off_tooltip_str = "Pixels are unchanged when zoomed in, thus rendering a true-to-pixel appearance" 191 192 action_set_single_transform_mode_smooth_on = menu_transform.addAction("Switch on") 193 action_set_single_transform_mode_smooth_on.setToolTip(transform_on_tooltip_str) 194 action_set_single_transform_mode_smooth_on.triggered.connect(lambda: self.right_click_single_transform_mode_smooth.emit(True)) 195 action_set_single_transform_mode_smooth_on.triggered.connect(lambda: self.set_single_transform_mode_smooth(True)) 196 197 action_set_single_transform_mode_smooth_off = menu_transform.addAction("Switch off") 198 action_set_single_transform_mode_smooth_off.setToolTip(transform_off_tooltip_str) 199 action_set_single_transform_mode_smooth_off.triggered.connect(lambda: self.right_click_single_transform_mode_smooth.emit(False)) 200 action_set_single_transform_mode_smooth_off.triggered.connect(lambda: self.set_single_transform_mode_smooth(False)) 201 202 if self.single_transform_mode_smooth: 203 action_set_single_transform_mode_smooth_on.setEnabled(False) 204 else: 205 action_set_single_transform_mode_smooth_off.setEnabled(False) 206 207 menu_transform.addSeparator() 208 209 action_set_all_transform_mode_smooth_on = menu_transform.addAction("Switch on (all windows)") 210 action_set_all_transform_mode_smooth_on.setToolTip(transform_on_tooltip_str+" (applies to all current and new image windows)") 211 action_set_all_transform_mode_smooth_on.triggered.connect(lambda: self.right_click_all_transform_mode_smooth.emit(True)) 212 213 action_set_all_transform_mode_smooth_off = menu_transform.addAction("Switch off (all windows)") 214 action_set_all_transform_mode_smooth_off.setToolTip(transform_off_tooltip_str+" (applies to all current and new image windows)") 215 action_set_all_transform_mode_smooth_off.triggered.connect(lambda: self.right_click_all_transform_mode_smooth.emit(False)) 216 217 menu.addSeparator() 218 219 menu_background = QtWidgets.QMenu("Set background color...") 220 menu_background.setToolTipsVisible(True) 221 menu.addMenu(menu_background) 222 223 for color in self.background_colors: 224 descriptor = color[0] 225 rgb = color[1:4] 226 action_set_background = menu_background.addAction(descriptor) 227 action_set_background.setToolTip("RGB " + ", ".join([str(channel) for channel in rgb])) 228 action_set_background.triggered.connect(lambda value, color=color: self.right_click_background_color.emit(color)) 229 action_set_background.triggered.connect(lambda value, color=color: self.background_color_lambda(color)) 230 if color == self.background_color: 231 action_set_background.setEnabled(False) 232 233 menu.exec(event.screenPos()) 234 235 def set_relative_origin_position(self, string): 236 """Set the descriptor of the position of the relative origin for rulers. 237 238 Describes the coordinate orientation: 239 "bottomleft" for Cartesian-style (positive X right, positive Y up) 240 "topleft" for image-style (positive X right, positive Y down) 241 242 Args: 243 string (str): "topleft" or "bottomleft" for position of the origin for coordinate system of rulers. 244 """ 245 self.relative_origin_position = string 246 247 def set_single_transform_mode_smooth(self, boolean): 248 """Set the descriptor of the status of smooth transform mode. 249 250 Describes the transform mode of pixels on zoom: 251 True for smooth (interpolated) 252 False for non-smooth (non-interpolated) 253 254 Args: 255 boolean (bool): True for smooth; False for non-smooth. 256 """ 257 self.single_transform_mode_smooth = boolean 258 259 def dialog_to_set_px_per_mm(self): 260 """Open the dialog for users to set the conversion for pixels to millimeters. 261 262 Emits the value of the px-per-mm conversion if user clicks "Ok" on dialog. 263 """ 264 dialog_window = PixelUnitConversionInputDialog(unit="mm", px_conversion=self.px_conversion, unit_conversion=self.unit_conversion, px_per_unit=self.px_per_unit) 265 dialog_window.setWindowModality(QtCore.Qt.ApplicationModal) 266 if dialog_window.exec_() == QtWidgets.QDialog.Accepted: 267 self.px_per_unit = dialog_window.px_per_unit 268 if self.px_per_unit_conversion_set: 269 self.changed_px_per_unit.emit("mm", self.px_per_unit) 270 self.px_per_unit_conversion_set = True 271 self.px_conversion = dialog_window.px_conversion 272 self.unit_conversion = dialog_window.unit_conversion 273 274 @property 275 def background_color(self): 276 """Current background color.""" 277 return self._background_color 278 279 @background_color.setter 280 def background_color(self, color): 281 """Set color as list with descriptor and RGB values [str, r, g, b].""" 282 self._background_color = color 283 284 def background_color_lambda(self, color): 285 """Within lambda, set color as list with descriptor and RGB values [str, r, g, b].""" 286 self.background_color = color 287 288 @property 289 def background_rgb(self): 290 """Current background color RGB.""" 291 return self._background_color[1:4]
QGraphicsScene with signals and right-click functionality for SplitView.
Recommended to be instantiated without input (for example, my_scene = CustomQGraphicsScene())
Signals for right click menu for comments (create comment, save comments, load comments). Signals for right click menu for rulers (create ruler, set origin relative position, set px-per-unit conversion) Signals for right click menu for transform mode (interpolate, non-interpolate) Methods for right click menu.
Arguments:
- Identical to base class QGraphicsScene.
64 def contextMenuEvent(self, event): 65 """Override the event of the context menu (right-click menu) to display options. 66 67 Triggered when mouse is right-clicked on scene. 68 69 Args: 70 event (PyQt event for contextMenuEvent) 71 """ 72 if self.disable_right_click: 73 return 74 75 what_menu_type = "View" 76 77 scene_pos = event.scenePos() 78 item = self.itemAt(scene_pos, self.views()[0].transform()) 79 80 action_delete = None 81 menu_set_color = None 82 action_set_color_red = None 83 action_set_color_white = None 84 action_set_color_blue = None 85 action_set_color_green = None 86 action_set_color_yellow = None 87 action_set_color_black = None 88 89 item_parent = item 90 if item is not None: 91 while item_parent.parentItem(): # Loop "upwards" to find parent item 92 item_parent = item_parent.parentItem() 93 94 if isinstance(item_parent, CommentItem) or isinstance(item_parent, RulerItem): 95 action_delete = QtWidgets.QAction("Delete") 96 97 if isinstance(item_parent, CommentItem): 98 menu_set_color = QtWidgets.QMenu("Set comment color...") 99 action_set_color_red = menu_set_color.addAction("Red") 100 action_set_color_red.triggered.connect(lambda: item_parent.set_color("red")) 101 action_set_color_white = menu_set_color.addAction("White") 102 action_set_color_white.triggered.connect(lambda: item_parent.set_color("white")) 103 action_set_color_blue = menu_set_color.addAction("Blue") 104 action_set_color_blue.triggered.connect(lambda: item_parent.set_color("blue")) 105 action_set_color_green = menu_set_color.addAction("Green") 106 action_set_color_green.triggered.connect(lambda: item_parent.set_color("green")) 107 action_set_color_yellow = menu_set_color.addAction("Yellow") 108 action_set_color_yellow.triggered.connect(lambda: item_parent.set_color("yellow")) 109 action_set_color_black = menu_set_color.addAction("Black") 110 action_set_color_black.triggered.connect(lambda: item_parent.set_color("black")) 111 112 action_delete.triggered.connect(lambda: self.removeItem(item_parent)) 113 114 what_menu_type = "Edit item(s)" 115 116 menu = QtWidgets.QMenu() 117 118 if what_menu_type == "Edit item(s)": 119 if menu_set_color: 120 menu.addMenu(menu_set_color) 121 if action_delete: 122 menu.addAction(action_delete) # action_delete.triggered.connect(lambda: self.removeItem(item.parentItem())) # = menu.addAction("Delete", self.removeItem(item.parentItem())) 123 else: 124 action_comment = menu.addAction("Comment") 125 action_comment.setToolTip("Add a draggable text comment here") 126 action_comment.triggered.connect(lambda: self.right_click_comment.emit(scene_pos)) # action_comment.triggered.connect(lambda state, x=scene_pos: self.right_click_comment.emit(x)) 127 128 menu_ruler = QtWidgets.QMenu("Measurement ruler...") 129 menu_ruler.setToolTip("Add a ruler to measure distances and angles in this image window...") 130 menu_ruler.setToolTipsVisible(True) 131 menu.addMenu(menu_ruler) 132 133 action_set_px_per_mm = menu_ruler.addAction("Set the ruler conversion factor for real distances (mm, cm)...") 134 action_set_px_per_mm.triggered.connect(lambda: self.dialog_to_set_px_per_mm()) 135 136 menu_ruler.addSeparator() 137 138 action_ruler_px = menu_ruler.addAction("Pixel ruler") 139 action_ruler_px.setToolTip("Add a ruler to measure distances in pixels") 140 action_ruler_px.triggered.connect(lambda: self.right_click_ruler.emit(scene_pos, self.relative_origin_position, "px", 1.0)) 141 142 action_ruler_mm = menu_ruler.addAction("Millimeter ruler") 143 action_ruler_mm.setToolTip("Add a ruler to measure distances in millimeters") 144 action_ruler_mm.triggered.connect(lambda: self.right_click_ruler.emit(scene_pos, self.relative_origin_position, "mm", self.px_per_unit)) 145 146 action_ruler_cm = menu_ruler.addAction("Centimeter ruler") 147 action_ruler_cm.setToolTip("Add a ruler to measure distances in centimeters") 148 action_ruler_cm.triggered.connect(lambda: self.right_click_ruler.emit(scene_pos, self.relative_origin_position, "cm", self.px_per_unit*10)) 149 150 if not self.px_per_unit_conversion_set: 151 text_disclaimer = "(requires conversion to be set before using)" 152 tooltip_disclaimer = "To use this ruler, first set the ruler conversion factor" 153 154 action_ruler_mm.setEnabled(False) 155 action_ruler_mm.setText(action_ruler_mm.text() + " " + text_disclaimer) 156 action_ruler_mm.setToolTip(tooltip_disclaimer) 157 158 action_ruler_cm.setEnabled(False) 159 action_ruler_cm.setText(action_ruler_cm.text() + " " + text_disclaimer) 160 action_ruler_cm.setToolTip(tooltip_disclaimer) 161 162 menu_ruler.addSeparator() 163 164 action_set_relative_origin_position_topleft = menu_ruler.addAction("Switch relative origin to top-left") 165 action_set_relative_origin_position_topleft.triggered.connect(lambda: self.right_click_relative_origin_position.emit("topleft")) 166 action_set_relative_origin_position_topleft.triggered.connect(lambda: self.set_relative_origin_position("topleft")) 167 action_set_relative_origin_position_bottomleft = menu_ruler.addAction("Switch relative origin to bottom-left") 168 action_set_relative_origin_position_bottomleft.triggered.connect(lambda: self.right_click_relative_origin_position.emit("bottomleft")) 169 action_set_relative_origin_position_bottomleft.triggered.connect(lambda: self.set_relative_origin_position("bottomleft")) 170 171 if self.relative_origin_position == "bottomleft": 172 action_set_relative_origin_position_bottomleft.setEnabled(False) 173 elif self.relative_origin_position == "topleft": 174 action_set_relative_origin_position_topleft.setEnabled(False) 175 176 menu.addSeparator() 177 178 action_save_all_comments = menu.addAction("Save all comments of this view (.csv)...") 179 action_save_all_comments.triggered.connect(lambda: self.right_click_save_all_comments.emit()) 180 action_load_comments = menu.addAction("Load comments into this view (.csv)...") 181 action_load_comments.triggered.connect(lambda: self.right_click_load_comments.emit()) 182 183 menu.addSeparator() 184 185 menu_transform = QtWidgets.QMenu("Upsample when zoomed...") 186 menu_transform.setToolTipsVisible(True) 187 menu.addMenu(menu_transform) 188 189 transform_on_tooltip_str = "Pixels are interpolated when zoomed in, thus rendering a smooth appearance" 190 transform_off_tooltip_str = "Pixels are unchanged when zoomed in, thus rendering a true-to-pixel appearance" 191 192 action_set_single_transform_mode_smooth_on = menu_transform.addAction("Switch on") 193 action_set_single_transform_mode_smooth_on.setToolTip(transform_on_tooltip_str) 194 action_set_single_transform_mode_smooth_on.triggered.connect(lambda: self.right_click_single_transform_mode_smooth.emit(True)) 195 action_set_single_transform_mode_smooth_on.triggered.connect(lambda: self.set_single_transform_mode_smooth(True)) 196 197 action_set_single_transform_mode_smooth_off = menu_transform.addAction("Switch off") 198 action_set_single_transform_mode_smooth_off.setToolTip(transform_off_tooltip_str) 199 action_set_single_transform_mode_smooth_off.triggered.connect(lambda: self.right_click_single_transform_mode_smooth.emit(False)) 200 action_set_single_transform_mode_smooth_off.triggered.connect(lambda: self.set_single_transform_mode_smooth(False)) 201 202 if self.single_transform_mode_smooth: 203 action_set_single_transform_mode_smooth_on.setEnabled(False) 204 else: 205 action_set_single_transform_mode_smooth_off.setEnabled(False) 206 207 menu_transform.addSeparator() 208 209 action_set_all_transform_mode_smooth_on = menu_transform.addAction("Switch on (all windows)") 210 action_set_all_transform_mode_smooth_on.setToolTip(transform_on_tooltip_str+" (applies to all current and new image windows)") 211 action_set_all_transform_mode_smooth_on.triggered.connect(lambda: self.right_click_all_transform_mode_smooth.emit(True)) 212 213 action_set_all_transform_mode_smooth_off = menu_transform.addAction("Switch off (all windows)") 214 action_set_all_transform_mode_smooth_off.setToolTip(transform_off_tooltip_str+" (applies to all current and new image windows)") 215 action_set_all_transform_mode_smooth_off.triggered.connect(lambda: self.right_click_all_transform_mode_smooth.emit(False)) 216 217 menu.addSeparator() 218 219 menu_background = QtWidgets.QMenu("Set background color...") 220 menu_background.setToolTipsVisible(True) 221 menu.addMenu(menu_background) 222 223 for color in self.background_colors: 224 descriptor = color[0] 225 rgb = color[1:4] 226 action_set_background = menu_background.addAction(descriptor) 227 action_set_background.setToolTip("RGB " + ", ".join([str(channel) for channel in rgb])) 228 action_set_background.triggered.connect(lambda value, color=color: self.right_click_background_color.emit(color)) 229 action_set_background.triggered.connect(lambda value, color=color: self.background_color_lambda(color)) 230 if color == self.background_color: 231 action_set_background.setEnabled(False) 232 233 menu.exec(event.screenPos())
Override the event of the context menu (right-click menu) to display options.
Triggered when mouse is right-clicked on scene.
Arguments:
- event (PyQt event for contextMenuEvent)
235 def set_relative_origin_position(self, string): 236 """Set the descriptor of the position of the relative origin for rulers. 237 238 Describes the coordinate orientation: 239 "bottomleft" for Cartesian-style (positive X right, positive Y up) 240 "topleft" for image-style (positive X right, positive Y down) 241 242 Args: 243 string (str): "topleft" or "bottomleft" for position of the origin for coordinate system of rulers. 244 """ 245 self.relative_origin_position = string
Set the descriptor of the position of the relative origin for rulers.
Describes the coordinate orientation:
"bottomleft" for Cartesian-style (positive X right, positive Y up) "topleft" for image-style (positive X right, positive Y down)
Arguments:
- string (str): "topleft" or "bottomleft" for position of the origin for coordinate system of rulers.
247 def set_single_transform_mode_smooth(self, boolean): 248 """Set the descriptor of the status of smooth transform mode. 249 250 Describes the transform mode of pixels on zoom: 251 True for smooth (interpolated) 252 False for non-smooth (non-interpolated) 253 254 Args: 255 boolean (bool): True for smooth; False for non-smooth. 256 """ 257 self.single_transform_mode_smooth = boolean
Set the descriptor of the status of smooth transform mode.
Describes the transform mode of pixels on zoom:
True for smooth (interpolated) False for non-smooth (non-interpolated)
Arguments:
- boolean (bool): True for smooth; False for non-smooth.
259 def dialog_to_set_px_per_mm(self): 260 """Open the dialog for users to set the conversion for pixels to millimeters. 261 262 Emits the value of the px-per-mm conversion if user clicks "Ok" on dialog. 263 """ 264 dialog_window = PixelUnitConversionInputDialog(unit="mm", px_conversion=self.px_conversion, unit_conversion=self.unit_conversion, px_per_unit=self.px_per_unit) 265 dialog_window.setWindowModality(QtCore.Qt.ApplicationModal) 266 if dialog_window.exec_() == QtWidgets.QDialog.Accepted: 267 self.px_per_unit = dialog_window.px_per_unit 268 if self.px_per_unit_conversion_set: 269 self.changed_px_per_unit.emit("mm", self.px_per_unit) 270 self.px_per_unit_conversion_set = True 271 self.px_conversion = dialog_window.px_conversion 272 self.unit_conversion = dialog_window.unit_conversion
Open the dialog for users to set the conversion for pixels to millimeters.
Emits the value of the px-per-mm conversion if user clicks "Ok" on dialog.