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]
class CustomQGraphicsScene(PyQt5.QtWidgets.QGraphicsScene):
 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.
def contextMenuEvent(self, event):
 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)
def set_relative_origin_position(self, string):
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.
def set_single_transform_mode_smooth(self, boolean):
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.
def dialog_to_set_px_per_mm(self):
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.

background_color

Current background color.

def background_color_lambda(self, color):
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

Within lambda, set color as list with descriptor and RGB values [str, r, g, b].

background_rgb

Current background color RGB.