butterfly_viewer.aux_splitview
Image viewing widget for individual images and sliding overlays with sync zoom and pan.
Not intended as a script. Used in Butterfly Viewer and Registrator.
Credits:
PyQt MDI Image Viewer by tpgit (http://tpgit.github.io/MDIImageViewer/) for sync pan and zoom.
1#!/usr/bin/env python3 2 3"""Image viewing widget for individual images and sliding overlays with sync zoom and pan. 4 5Not intended as a script. Used in Butterfly Viewer and Registrator. 6 7Credits: 8 PyQt MDI Image Viewer by tpgit (http://tpgit.github.io/MDIImageViewer/) for sync pan and zoom. 9""" 10# SPDX-License-Identifier: GPL-3.0-or-later 11 12 13 14import sip 15import gc 16import os 17import math 18import csv 19from datetime import datetime 20 21from PyQt5 import QtCore, QtGui, QtWidgets 22 23from aux_viewing import SynchableGraphicsView 24from aux_trackers import EventTracker, EventTrackerSplitBypassDeadzone 25from aux_functions import strippedName 26from aux_labels import FilenameLabel 27from aux_scenes import CustomQGraphicsScene 28from aux_comments import CommentItem 29from aux_rulers import RulerItem 30 31 32 33sip.setapi('QDate', 2) 34sip.setapi('QTime', 2) 35sip.setapi('QDateTime', 2) 36sip.setapi('QUrl', 2) 37sip.setapi('QTextStream', 2) 38sip.setapi('QVariant', 2) 39sip.setapi('QString', 2) 40 41 42 43class SplitView(QtWidgets.QFrame): 44 """Image viewing widget for individual images and sliding overlays. 45 46 Creates an interface with a base image as a main image located at the top left 47 and optionally 3 other images (top-left, bottom-left, bottom-right) as a sliding overlay. 48 Supports zoom and pan. 49 Enables synchronized zoom and pan via signals. 50 Input images for a given sliding overlay must have identical resolutions to 51 function properly. 52 53 Args: 54 pixmap (QPixmap): The main image to be viewed; the basis of the sliding overlay (main; topleft) 55 filename_main_topleft (str): The image filepath of the main image. 56 name (str): The name of the viewing widget. 57 pixmap_topright (QPixmap): The top-right image of the sliding overlay (set None to exclude). 58 pixmap_bottomleft (QPixmap): The bottom-left image of the sliding overlay (set None to exclude). 59 pixmap_bottomright (QPixmap): The bottom-right image of the sliding overlay (set None to exclude). 60 """ 61 62 def __init__(self, pixmap_main_topleft=None, filename_main_topleft=None, name=None, 63 pixmap_topright=None, pixmap_bottomleft=None, pixmap_bottomright=None, transform_mode_smooth=False): 64 super().__init__() 65 66 self.currentFile = filename_main_topleft 67 68 self.setWindowFlags(QtCore.Qt.FramelessWindowHint) # Clean appearance 69 self.setFrameStyle(QtWidgets.QFrame.NoFrame) 70 71 self.viewName = name 72 self.comment_position_object = None 73 74 pixmap_main_topleft = self.pixmap_none_ify(pixmap_main_topleft) # Return None if given QPixmap has zero width/height or is not a QPixmap 75 pixmap_topright = self.pixmap_none_ify(pixmap_topright) 76 pixmap_bottomleft = self.pixmap_none_ify(pixmap_bottomleft) 77 pixmap_bottomright = self.pixmap_none_ify(pixmap_bottomright) 78 79 self.pixmap_topright_exists = (pixmap_topright is not None) # Boolean for existence of pixmap is easier to check 80 self.pixmap_bottomright_exists = (pixmap_bottomright is not None) 81 self.pixmap_bottomleft_exists = (pixmap_bottomleft is not None) 82 83 # Pixmaps which do no exist should be transparent, so they are made into an empty pixmap 84 if not self.pixmap_bottomright_exists: 85 pixmap_bottomright = QtGui.QPixmap() 86 87 if not self.pixmap_topright_exists: 88 pixmap_topright = QtGui.QPixmap() 89 90 if not self.pixmap_bottomleft_exists: 91 pixmap_bottomleft = QtGui.QPixmap() 92 93 self._zoomFactorDelta = 1.25 # How much zoom for each zoom call 94 95 self.transform_mode_smooth = transform_mode_smooth 96 97 # Sliding overlay is based on the main pixmap view of the top-left pixmap 98 # self._scene_main_topleft = QtWidgets.QGraphicsScene() 99 self._scene_main_topleft = CustomQGraphicsScene() 100 self._view_main_topleft = SynchableGraphicsView(self._scene_main_topleft) 101 102 self._view_main_topleft.setInteractive(True) # Functional settings 103 self._view_main_topleft.setViewportUpdateMode(QtWidgets.QGraphicsView.MinimalViewportUpdate) 104 self._view_main_topleft.setResizeAnchor(QtWidgets.QGraphicsView.AnchorViewCenter) 105 self._view_main_topleft.setRenderHints(QtGui.QPainter.SmoothPixmapTransform | QtGui.QPainter.Antialiasing) 106 107 self._scene_main_topleft.changed.connect(self.sceneChanged) # Pass along underlying signals 108 self._view_main_topleft.transformChanged.connect(self.transformChanged) 109 self._view_main_topleft.transformChanged.connect(self.on_transformChanged) 110 self._view_main_topleft.scrollChanged.connect(self.scrollChanged) 111 self._view_main_topleft.wheelNotches.connect(self.handleWheelNotches) 112 self._scene_main_topleft.right_click_comment.connect(self.on_right_click_comment) 113 self._scene_main_topleft.right_click_ruler.connect(self.on_right_click_ruler) 114 self._scene_main_topleft.right_click_save_all_comments.connect(self.on_right_click_save_all_comments) 115 self._scene_main_topleft.right_click_load_comments.connect(self.on_right_click_load_comments) 116 self._scene_main_topleft.right_click_relative_origin_position.connect(self.on_right_click_set_relative_origin_position) 117 self._scene_main_topleft.changed_px_per_unit.connect(self.on_changed_px_per_unit) 118 self._scene_main_topleft.right_click_single_transform_mode_smooth.connect(self.set_transform_mode_smooth) 119 self._scene_main_topleft.right_click_all_transform_mode_smooth.connect(self.was_set_global_transform_mode) 120 self._scene_main_topleft.right_click_background_color.connect(self.set_scene_background_color) 121 self._scene_main_topleft.right_click_background_color.connect(self.was_set_scene_background_color) 122 123 self._pixmapItem_main_topleft = QtWidgets.QGraphicsPixmapItem() 124 self._scene_main_topleft.addItem(self._pixmapItem_main_topleft) 125 126 # A pseudo view directly atop the main view is needed to drive the position of the split and layout of the four pixmaps 127 self._view_layoutdriving_topleft = QtWidgets.QGraphicsView() 128 self._view_layoutdriving_topleft.setStyleSheet("border: 0px; border-style: solid; background-color: rgba(0,0,0,0)") 129 self._view_layoutdriving_topleft.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents) 130 self._view_layoutdriving_topleft.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) 131 self._view_layoutdriving_topleft.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) 132 self._view_layoutdriving_topleft.setResizeAnchor(QtWidgets.QGraphicsView.AnchorUnderMouse) 133 134 # Add top right pixmap view 135 self._pixmapItem_topright = QtWidgets.QGraphicsPixmapItem() 136 self.pixmap_topright = pixmap_topright 137 138 self._scene_topright = QtWidgets.QGraphicsScene() 139 self._scene_topright.addItem(self._pixmapItem_topright) 140 141 self._view_topright = QtWidgets.QGraphicsView(self._scene_topright) 142 self._view_topright.setStyleSheet("border: 0px; border-style: solid; border-color: rgba(0,0,0,0); background-color: rgba(0,0,0,0);") 143 self._view_topright.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents) 144 self._view_topright.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) 145 self._view_topright.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) 146 self._view_topright.setResizeAnchor(QtWidgets.QGraphicsView.AnchorViewCenter) 147 148 # Add bottom left pixmap view 149 self._pixmapItem_bottomleft = QtWidgets.QGraphicsPixmapItem() 150 self.pixmap_bottomleft = pixmap_bottomleft 151 152 self._scene_bottomleft = QtWidgets.QGraphicsScene() 153 self._scene_bottomleft.addItem(self._pixmapItem_bottomleft) 154 155 self._view_bottomleft = QtWidgets.QGraphicsView(self._scene_bottomleft) 156 self._view_bottomleft.setStyleSheet("border: 0px; border-style: solid; border-color: rgba(0,0,0,0); background-color: rgba(0,0,0,0);") 157 self._view_bottomleft.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents) 158 self._view_bottomleft.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) 159 self._view_bottomleft.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) 160 self._view_bottomleft.setResizeAnchor(QtWidgets.QGraphicsView.AnchorViewCenter) 161 162 # Add bottom right pixmap view 163 self._pixmapItem_bottomright = QtWidgets.QGraphicsPixmapItem() 164 self.pixmap_bottomright = pixmap_bottomright 165 166 self._scene_bottomright = QtWidgets.QGraphicsScene() 167 self._scene_bottomright.addItem(self._pixmapItem_bottomright) 168 169 self._view_bottomright = QtWidgets.QGraphicsView(self._scene_bottomright) 170 self._view_bottomright.setStyleSheet("border: 0px; border-style: solid; border-color: rgba(0,0,0,0); background-color: rgba(0,0,0,0);") 171 self._view_bottomright.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents) 172 self._view_bottomright.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) 173 self._view_bottomright.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) 174 self._view_bottomright.setResizeAnchor(QtWidgets.QGraphicsView.AnchorViewCenter) 175 176 # Make the sizes of the four views entirely dictated by the "layout driving" view 177 size_policy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Ignored) 178 self._view_main_topleft.setSizePolicy(size_policy) 179 self._view_topright.setSizePolicy(size_policy) 180 self._view_bottomright.setSizePolicy(size_policy) 181 self._view_bottomleft.setSizePolicy(size_policy) 182 183 # By default the split is set to half the widget's size so all pixmap views are equally sized at the start 184 self._view_layoutdriving_topleft.setMaximumWidth(self.width()/2.0) 185 self._view_layoutdriving_topleft.setMaximumHeight(self.height()/2.0) 186 187 if pixmap_main_topleft: # Instantiate transform and resizing 188 self.pixmap_main_topleft = pixmap_main_topleft 189 190 # SplitView layout 191 self._layout = QtWidgets.QGridLayout() 192 self._layout.setContentsMargins(0, 0, 0, 0) 193 self._layout.setSpacing(0) 194 195 self.setContentsMargins(0, 0, 0, 0) 196 197 # Labels for the four pixmap views 198 self.label_main_topleft = FilenameLabel(visibility_based_on_text=True, belongs_to_split=True) 199 self.label_topright = FilenameLabel(visibility_based_on_text=True, belongs_to_split=True) 200 self.label_bottomright = FilenameLabel(visibility_based_on_text=True, belongs_to_split=True) 201 self.label_bottomleft = FilenameLabel(visibility_based_on_text=True, belongs_to_split=True) 202 203 self.label_main_topleft.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents) 204 self.label_topright.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents) 205 self.label_bottomright.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents) 206 self.label_bottomleft.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents) 207 208 # Pushbutton to close the image window 209 self.close_pushbutton = QtWidgets.QPushButton("×") 210 self.close_pushbutton.setToolTip("Close image window") 211 self.close_pushbutton.clicked.connect(self.was_clicked_close_pushbutton) 212 self.close_pushbutton_always_visible = True 213 214 # Create deadzones along the bounds of SplitView to fix the issue of resize handles showing in QMdiArea despite windowless setting. 215 # An event tracker "bypass" is needed for each deadzone because they hide the mouse from the sliding overlay, so the mouse must be separately tracked to ensure the split is updated. 216 px_deadzone = 8 217 218 self.resize_deadzone_top = QtWidgets.QPushButton("") 219 self.resize_deadzone_top.setFixedHeight(px_deadzone) 220 self.resize_deadzone_top.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed) 221 self.resize_deadzone_top.setEnabled(False) 222 self.resize_deadzone_top.setStyleSheet(""" 223 QPushButton { 224 color: transparent; 225 background-color: transparent; 226 border: 0px black; 227 } 228 """) 229 tracker_deadzone_top = EventTrackerSplitBypassDeadzone(self.resize_deadzone_top) 230 tracker_deadzone_top.mouse_position_changed_global.connect(self.update_split_given_global) 231 232 self.resize_deadzone_bottom = QtWidgets.QPushButton("") 233 self.resize_deadzone_bottom.setFixedHeight(px_deadzone) 234 self.resize_deadzone_bottom.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed) 235 self.resize_deadzone_bottom.setEnabled(False) 236 self.resize_deadzone_bottom.setStyleSheet(""" 237 QPushButton { 238 color: transparent; 239 background-color: transparent; 240 border: 0px black; 241 } 242 """) 243 tracker_deadzone_bottom = EventTrackerSplitBypassDeadzone(self.resize_deadzone_bottom) 244 tracker_deadzone_bottom.mouse_position_changed_global.connect(self.update_split_given_global) 245 246 self.resize_deadzone_left = QtWidgets.QPushButton("") 247 self.resize_deadzone_left.setFixedWidth(px_deadzone) 248 self.resize_deadzone_left.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Expanding) 249 self.resize_deadzone_left.setEnabled(False) 250 self.resize_deadzone_left.setStyleSheet(""" 251 QPushButton { 252 color: transparent; 253 background-color: transparent; 254 border: 0px black; 255 } 256 """) 257 tracker_deadzone_left = EventTrackerSplitBypassDeadzone(self.resize_deadzone_left) 258 tracker_deadzone_left.mouse_position_changed_global.connect(self.update_split_given_global) 259 260 self.resize_deadzone_right = QtWidgets.QPushButton("") 261 self.resize_deadzone_right.setFixedWidth(px_deadzone) 262 self.resize_deadzone_right.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Expanding) 263 self.resize_deadzone_right.setEnabled(False) 264 self.resize_deadzone_right.setStyleSheet(""" 265 QPushButton { 266 color: transparent; 267 background-color: transparent; 268 border: 0px black; 269 } 270 """) 271 tracker_deadzone_right = EventTrackerSplitBypassDeadzone(self.resize_deadzone_right) 272 tracker_deadzone_right.mouse_position_changed_global.connect(self.update_split_given_global) 273 274 # A frame is placed over the border of the widget to highlight it as the active subwindow in Butterfly Viewer. 275 self.frame_hud = QtWidgets.QFrame() 276 self.frame_hud.setStyleSheet("border: 0px solid transparent") 277 self.frame_hud.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents) 278 279 # Set layout 280 self._layout.addWidget(self._view_main_topleft, 0, 0, 2, 2) 281 self._layout.addWidget(self._view_layoutdriving_topleft, 0, 0) 282 self._layout.addWidget(self._view_topright, 0, 1) 283 self._layout.addWidget(self._view_bottomleft, 1, 0) 284 self._layout.addWidget(self._view_bottomright, 1, 1) 285 286 self._layout.addWidget(self.resize_deadzone_top, 0, 0, 2, 2, QtCore.Qt.AlignTop) 287 self._layout.addWidget(self.resize_deadzone_bottom, 0, 0, 2, 2, QtCore.Qt.AlignBottom) 288 self._layout.addWidget(self.resize_deadzone_left, 0, 0, 2, 2, QtCore.Qt.AlignLeft) 289 self._layout.addWidget(self.resize_deadzone_right, 0, 0, 2, 2, QtCore.Qt.AlignRight) 290 291 self._layout.addWidget(self.frame_hud, 0, 0, 2, 2) 292 293 self._layout.addWidget(self.label_main_topleft, 0, 0, 2, 2, QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft) 294 self._layout.addWidget(self.label_topright, 0, 0, 2, 2, QtCore.Qt.AlignTop | QtCore.Qt.AlignRight) 295 self._layout.addWidget(self.label_bottomright, 0, 0, 2, 2, QtCore.Qt.AlignBottom | QtCore.Qt.AlignRight) 296 self._layout.addWidget(self.label_bottomleft, 0, 0, 2, 2, QtCore.Qt.AlignBottom | QtCore.Qt.AlignLeft) 297 298 self._layout.addWidget(self.close_pushbutton, 0, 0, 2, 2, QtCore.Qt.AlignTop | QtCore.Qt.AlignRight) 299 300 self.setLayout(self._layout) 301 302 self.set_scene_background_color(self._scene_main_topleft.background_color) 303 self._view_main_topleft.setStyleSheet("border: 0px; border-style: solid; border-color: red; background-color: rgba(0,0,0,0)") 304 305 # Track the mouse position to know where to set the split 306 self.tracker = EventTracker(self) 307 self.tracker.mouse_position_changed.connect(self.positionChanged) 308 self.tracker.mouse_position_changed.connect(self.on_positionChanged) 309 310 # Create a rectangular box the size of one pixel in the main scene to show the user the size and position of the pixel over which their mouse is hovering 311 self.mouse_rect_scene_main_topleft = None 312 self.create_mouse_rect() 313 314 # Allow users to lock the split and remember where the split was last set 315 self.split_locked = False 316 self.last_updated_point_of_split_on_scene_main = QtCore.QPoint() 317 318 self.enableScrollBars(False) # Clean look with no scrollbars 319 320 @property 321 def currentFile(self): 322 """str: Filepath of base image (filename_main_topleft).""" 323 return self._currentFile 324 325 @currentFile.setter 326 def currentFile(self, filename_main_topleft): 327 self._currentFile = QtCore.QFileInfo(filename_main_topleft).canonicalFilePath() 328 self._isUntitled = False 329 self.setWindowTitle(self.userFriendlyCurrentFile) 330 331 @property 332 def userFriendlyCurrentFile(self): 333 """str: Filename of base image.""" 334 if self.currentFile: 335 return strippedName(self.currentFile) 336 else: 337 return "" 338 339 def set_close_pushbutton_always_visible(self, boolean): 340 """Enable/disable the always-on visiblilty of the close X of the view. 341 342 Arg: 343 boolean (bool): True to show the close X always; False to hide unless mouse hovers over. 344 """ 345 self.close_pushbutton_always_visible = boolean 346 self.refresh_close_pushbutton_stylesheet() 347 348 def refresh_close_pushbutton_stylesheet(self): 349 """Refresh stylesheet of close pushbutton based on background color and visibility.""" 350 if not self.close_pushbutton: 351 return 352 always_visible = self.close_pushbutton_always_visible 353 background_rgb = self._scene_main_topleft.background_rgb 354 avg_background_rgb = sum(background_rgb)/len(background_rgb) 355 if not always_visible: # Hide unless hovered 356 self.close_pushbutton.setStyleSheet(""" 357 QPushButton { 358 width: 1.8em; 359 height: 1.8em; 360 color: transparent; 361 background-color: rgba(223, 0, 0, 0); 362 font-weight: bold; 363 border-width: 0px; 364 border-style: solid; 365 border-color: transparent; 366 font-size: 10pt; 367 } 368 QPushButton:hover { 369 color: white; 370 background-color: rgba(223, 0, 0, 223); 371 } 372 QPushButton:pressed { 373 color: white; 374 background-color: rgba(255, 0, 0, 255); 375 } 376 """) 377 else: # Always visible 378 if avg_background_rgb >= 223: # Unhovered is black X on light background 379 self.close_pushbutton.setStyleSheet(""" 380 QPushButton { 381 width: 1.8em; 382 height: 1.8em; 383 color: black; 384 background-color: rgba(223, 0, 0, 0); 385 font-weight: bold; 386 border-width: 0px; 387 border-style: solid; 388 border-color: transparent; 389 font-size: 10pt; 390 } 391 QPushButton:hover { 392 color: white; 393 background-color: rgba(223, 0, 0, 223); 394 } 395 QPushButton:pressed { 396 color: white; 397 background-color: rgba(255, 0, 0, 255); 398 } 399 """) 400 else: # Unhovered is white X on dark background 401 self.close_pushbutton.setStyleSheet(""" 402 QPushButton { 403 width: 1.8em; 404 height: 1.8em; 405 color: white; 406 background-color: rgba(223, 0, 0, 0); 407 font-weight: bold; 408 border-width: 0px; 409 border-style: solid; 410 border-color: transparent; 411 font-size: 10pt; 412 } 413 QPushButton:hover { 414 color: white; 415 background-color: rgba(223, 0, 0, 223); 416 } 417 QPushButton:pressed { 418 color: white; 419 background-color: rgba(255, 0, 0, 255); 420 } 421 """) 422 423 424 def set_scene_background(self, brush): 425 """Set scene background color with QBrush. 426 427 Args: 428 brush (QBrush) 429 """ 430 if not self._scene_main_topleft: 431 return 432 self._scene_main_topleft.setBackgroundBrush(brush) 433 434 def set_scene_background_color(self, color: list): 435 """Set scene background color with color list. 436 437 The init for CustomQGraphicsScene contains the ground truth for selectable background colors. 438 439 Args: 440 color (list): Descriptor string and RGB int values. Example: ["White", 255, 255, 255]. 441 """ 442 if not self._scene_main_topleft: 443 return 444 rgb = color[1:4] 445 rgb_clamp = [max(min(channel, 255), 0) for channel in rgb] 446 brush = QtGui.QColor(rgb_clamp[0], rgb_clamp[1], rgb_clamp[2]) 447 self.set_scene_background(brush) 448 self._scene_main_topleft.background_color = color 449 self.refresh_close_pushbutton_stylesheet() 450 451 452 def pixmap_none_ify(self, pixmap): 453 """Return None if pixmap has no pixels. 454 455 Args: 456 pixmap (QPixmap) 457 458 Returns: 459 None if pixmap has no pixels; same pixmap if it has pixels 460 """ 461 if pixmap: 462 if pixmap.width()==0 or pixmap.height==0: 463 return None 464 else: 465 return pixmap 466 else: 467 return None 468 469 @QtCore.pyqtSlot(QtCore.QPoint) 470 def on_positionChanged(self, pos): 471 """Update the position of the split and the 1x1 pixel rectangle. 472 473 Triggered when mouse is moved. 474 475 Args: 476 pos (QPoint): The position of the mouse relative to widget. 477 """ 478 point_of_mouse_on_widget = pos 479 480 self.update_split(point_of_mouse_on_widget) 481 self.update_mouse_rect(point_of_mouse_on_widget) 482 483 def set_split(self, x_percent=0.5, y_percent=0.5, ignore_lock=False, percent_of_visible=False): 484 """Set the position of the split with x and y as proportion of base image's resolution. 485 486 Sets split position using a proportion of x and y (by default of entire main pixmap; can be set to proportion of visible pixmap). 487 Top left is x=0, y=0; bottom right is x=1, y=1. 488 This is needed to position the split without mouse movement from user (for example, to preview the effect of the transparency sliders in Butterfly Viewer) 489 490 Args: 491 x_percent (float): The position of the split as a proportion (0-1) of the base image's horizontal resolution. 492 y_percent (float): The position of the split as a proportion (0-1) of the base image's vertical resolution. 493 ignore_lock (bool): True to ignore the lock status of the split; False to adhere. 494 percent_of_visible (bool): True to set split as proportion of visible area; False as proportion of the full image resolution. 495 """ 496 if percent_of_visible: 497 x = x_percent*self.width() 498 y = y_percent*self.height() 499 point_of_split_on_widget = QtCore.QPoint(x, y) 500 else: 501 width_pixmap_main_topleft = self._pixmapItem_main_topleft.pixmap().width() 502 height_pixmap_main_topleft = self._pixmapItem_main_topleft.pixmap().height() 503 504 x = x_percent*width_pixmap_main_topleft 505 y = y_percent*height_pixmap_main_topleft 506 507 point_of_split_on_scene = QtCore.QPointF(x,y) 508 509 point_of_split_on_widget = self._view_main_topleft.mapFromScene(point_of_split_on_scene) 510 511 self.update_split(point_of_split_on_widget, ignore_lock=ignore_lock) 512 513 def update_split(self, pos = None, pos_is_global=False, ignore_lock=False): 514 """Update the position of the split with mouse position. 515 516 Args: 517 pos (QPoint): Position of the mouse. 518 pos_is_global (bool): True if given mouse position is relative to MdiChild; False if global position. 519 ignore_lock (bool): True to ignore (bypass) the status of the split lock. 520 """ 521 if pos is None: # Get position of the split from the mouse's global coordinates (can be slow!) 522 point_of_cursor_global = QtGui.QCursor.pos() 523 point_of_mouse_on_widget = self._view_main_topleft.mapFromGlobal(point_of_cursor_global) 524 else: 525 if pos_is_global: 526 point_of_mouse_on_widget = self._view_main_topleft.mapFromGlobal(pos) 527 else: 528 point_of_mouse_on_widget = pos 529 530 point_of_mouse_on_widget.setX(point_of_mouse_on_widget.x()+1) # Offset +1 needed to have mouse cursor be hovering over the main scene (e.g., to allow manipulation of graphics item) 531 point_of_mouse_on_widget.setY(point_of_mouse_on_widget.y()+1) 532 533 self.last_updated_point_of_split_on_scene_main = self._view_main_topleft.mapToScene(point_of_mouse_on_widget.x(), point_of_mouse_on_widget.y()) 534 535 point_of_bottom_right_on_widget = QtCore.QPointF(self.width(), self.height()) 536 537 point_of_widget_origin_on_scene_main = self._view_main_topleft.mapToScene(0,0) 538 539 point_of_split_on_scene_main = self._view_main_topleft.mapToScene(max(point_of_mouse_on_widget.x(),0),max(point_of_mouse_on_widget.y(),0)) 540 point_of_bottom_right_on_scene_main = self._view_main_topleft.mapToScene(point_of_bottom_right_on_widget.x(), point_of_bottom_right_on_widget.y()) 541 542 self._view_layoutdriving_topleft.setMaximumWidth(max(point_of_mouse_on_widget.x(),0)) 543 self._view_layoutdriving_topleft.setMaximumHeight(max(point_of_mouse_on_widget.y(),0)) 544 545 render_buffer = 100 # Needed to prevent slight pixel offset of the sliding overlays when zoomed-out below ~0.5x 546 547 top_left_of_scene_topright = QtCore.QPointF(point_of_split_on_scene_main.x(), point_of_widget_origin_on_scene_main.y()) 548 bottom_right_of_scene_topright = QtCore.QPointF(point_of_bottom_right_on_scene_main.x() + render_buffer, point_of_split_on_scene_main.y() + render_buffer) 549 550 top_left_of_scene_bottomright = QtCore.QPointF(point_of_split_on_scene_main.x(), point_of_split_on_scene_main.y()) 551 bottom_right_of_scene_bottomright = QtCore.QPointF(point_of_bottom_right_on_scene_main.x() + render_buffer, point_of_bottom_right_on_scene_main.y() + render_buffer) 552 553 top_left_of_scene_bottomleft = QtCore.QPointF(point_of_widget_origin_on_scene_main.x(), point_of_split_on_scene_main.y()) 554 bottom_right_of_scene_bottomleft = QtCore.QPointF(point_of_split_on_scene_main.x() + render_buffer, point_of_bottom_right_on_scene_main.y() + render_buffer) 555 556 rect_of_scene_topright = QtCore.QRectF(top_left_of_scene_topright, bottom_right_of_scene_topright) 557 rect_of_scene_bottomright = QtCore.QRectF(top_left_of_scene_bottomright, bottom_right_of_scene_bottomright) 558 rect_of_scene_bottomleft = QtCore.QRectF(top_left_of_scene_bottomleft, bottom_right_of_scene_bottomleft) 559 560 self._view_topright.setSceneRect(rect_of_scene_topright) 561 self._view_bottomright.setSceneRect(rect_of_scene_bottomright) 562 self._view_bottomleft.setSceneRect(rect_of_scene_bottomleft) 563 564 self._view_topright.centerOn(top_left_of_scene_topright) 565 self._view_bottomright.centerOn(top_left_of_scene_bottomright) 566 self._view_bottomleft.centerOn(top_left_of_scene_bottomleft) 567 568 def refresh_split_based_on_last_updated_point_of_split_on_scene_main(self): 569 """Refresh the position of the split using the previously recorded split location. 570 571 This is needed to maintain the position of the split during synchronized zooming and panning. 572 """ 573 point_of_mouse_on_widget = self._view_main_topleft.mapFromScene(self.last_updated_point_of_split_on_scene_main) 574 575 point_of_bottom_right_on_widget = QtCore.QPointF(self.width(), self.height()) 576 577 point_of_widget_origin_on_scene_main = self._view_main_topleft.mapToScene(0,0) 578 point_of_split_on_scene_main = self._view_main_topleft.mapToScene(max(point_of_mouse_on_widget.x(),0),max(point_of_mouse_on_widget.y(),0)) 579 point_of_bottom_right_on_scene_main = self._view_main_topleft.mapToScene(point_of_bottom_right_on_widget.x(), point_of_bottom_right_on_widget.y()) 580 581 self._view_layoutdriving_topleft.setMaximumWidth(max(point_of_mouse_on_widget.x(),0)) 582 self._view_layoutdriving_topleft.setMaximumHeight(max(point_of_mouse_on_widget.y(),0)) 583 584 render_buffer = 100 # Needed to prevent slight pixel offset of the sliding overlays when zoomed-out below ~0.5x 585 586 top_left_of_scene_topright = QtCore.QPointF(point_of_split_on_scene_main.x(), point_of_widget_origin_on_scene_main.y()) 587 bottom_right_of_scene_topright = QtCore.QPointF(point_of_bottom_right_on_scene_main.x() + render_buffer, point_of_split_on_scene_main.y() + render_buffer) 588 589 top_left_of_scene_bottomright = QtCore.QPointF(point_of_split_on_scene_main.x(), point_of_split_on_scene_main.y()) 590 bottom_right_of_scene_bottomright = QtCore.QPointF(point_of_bottom_right_on_scene_main.x() + render_buffer, point_of_bottom_right_on_scene_main.y() + render_buffer) 591 592 top_left_of_scene_bottomleft = QtCore.QPointF(point_of_widget_origin_on_scene_main.x(), point_of_split_on_scene_main.y()) 593 bottom_right_of_scene_bottomleft = QtCore.QPointF(point_of_split_on_scene_main.x() + render_buffer, point_of_bottom_right_on_scene_main.y() + render_buffer) 594 595 rect_of_scene_topright = QtCore.QRectF(top_left_of_scene_topright, bottom_right_of_scene_topright) 596 rect_of_scene_bottomright = QtCore.QRectF(top_left_of_scene_bottomright, bottom_right_of_scene_bottomright) 597 rect_of_scene_bottomleft = QtCore.QRectF(top_left_of_scene_bottomleft, bottom_right_of_scene_bottomleft) 598 599 self._view_topright.setSceneRect(rect_of_scene_topright) 600 self._view_bottomright.setSceneRect(rect_of_scene_bottomright) 601 self._view_bottomleft.setSceneRect(rect_of_scene_bottomleft) 602 603 self._view_topright.centerOn(top_left_of_scene_topright) 604 self._view_bottomright.centerOn(top_left_of_scene_bottomright) 605 self._view_bottomleft.centerOn(top_left_of_scene_bottomleft) 606 607 def update_split_given_global(self, pos_global): 608 """Update the position of the split based on given global mouse position. 609 610 Convenience function. 611 612 Args: 613 pos_global (QPoint): The position of the mouse in global coordinates. 614 """ 615 self.update_split(pos = pos_global, pos_is_global=True) 616 617 618 def on_right_click_comment(self, scene_pos): 619 """Create an editable and movable comment on the scene of the main view at the given scene position. 620 621 Args: 622 pos (QPointF): position of the comment datum on the scene. 623 """ 624 pos_on_scene = scene_pos 625 self._scene_main_topleft.addItem(CommentItem(initial_scene_pos=pos_on_scene, set_cursor_on_creation=True)) 626 627 def on_right_click_ruler(self, scene_pos, relative_origin_position, unit="px", px_per_unit=1.0, update_px_per_unit_on_existing=False): 628 """Create a movable ruler on the scene of the main view at the given scene position. 629 630 Args: 631 scene_pos (QPointF): The position of the ruler center on the scene. 632 relative_origin_position (str): The position of the origin for coordinate system ("topleft" or "bottomleft"). 633 unit (str): The text for labeling units of ruler values. 634 px_per_unit (float): The conversion for pixels to units. For example, 10 means 10 pixels-per-unit, meaning the ruler value will show 1 when measuring 10 pixels. 635 update_px_per_unit_on_existing (bool): False always. (Legacy from past versions; future work should remove. 636 """ 637 placement_factor = 1/3 638 px_per_unit = px_per_unit 639 relative_origin_position = relative_origin_position 640 641 widget_width = self.width() 642 widget_height = self.height() 643 644 pos_p1 = self._view_main_topleft.mapToScene(widget_width*placement_factor, widget_height*placement_factor) 645 pos_p2 = self._view_main_topleft.mapToScene(widget_width*(1-placement_factor), widget_height*(1-placement_factor)) 646 self._scene_main_topleft.addItem(RulerItem(unit=unit, px_per_unit=px_per_unit, initial_pos_p1=pos_p1, initial_pos_p2=pos_p2, relative_origin_position=relative_origin_position)) 647 648 def on_changed_px_per_unit(self, unit, px_per_unit): 649 """Update the units and pixel-per-unit conversions of all rulers in main scene. 650 651 Args: 652 unit (str): The text for labeling units of ruler values. 653 px_per_unit (float): The conversion for pixels to units. For example, 10 means 10 pixels-per-unit, meaning the ruler value will show 1 when measuring 10 pixels. 654 """ 655 for item in self._scene_main_topleft.items(): 656 if isinstance(item, RulerItem): 657 if item.unit == unit: 658 item.set_and_refresh_px_per_unit(px_per_unit) 659 660 def on_right_click_save_all_comments(self): 661 """Open a dialog window for user to save all existing comments on the main scene to .csv. 662 663 Triggered from right-click menu on view. 664 """ 665 self.display_loading_grayout(True, "Selecting folder and name for saving all comments in current view...", pseudo_load_time=0) 666 667 style = [] 668 x = [] 669 y = [] 670 color = [] 671 string = [] 672 i = -1 673 for item in self._scene_main_topleft.items(): 674 675 if isinstance(item, CommentItem): 676 i += 1 677 678 comment_pos = item.get_scene_pos() 679 comment_color = item.get_color() 680 comment_string = item.get_comment_text_str() 681 682 style.append("plain text") 683 x.append(comment_pos.x()) 684 y.append(comment_pos.y()) 685 color.append(comment_color) 686 string.append(comment_string) 687 688 689 folderpath = None 690 filepath_mainview = self.currentFile 691 692 if filepath_mainview: 693 folderpath = filepath_mainview 694 folderpath = os.path.dirname(folderpath) 695 folderpath = folderpath + "\\" 696 else: 697 self.display_loading_grayout(False, pseudo_load_time=0) 698 return 699 700 header = [["Butterfly Viewer"], 701 ["1.0"], 702 ["comments"], 703 ["no details"], 704 ["origin"], 705 [os.path.basename(filepath_mainview)], 706 ["Style", "Image x", "Image y", "Appearance", "String"]] 707 708 date_and_time = datetime.now().strftime('%Y-%m-%d %H%M%S') # Sets the default filename with date and time 709 filename = "Untitled comments" + " - " + os.path.basename(filepath_mainview).split('.')[0] + " - " + date_and_time + ".csv" 710 name_filters = "CSV (*.csv)" # Allows users to select filetype of screenshot 711 712 filepath, _ = QtWidgets.QFileDialog.getSaveFileName(self, "Save all comments of current view to .csv", folderpath+filename, name_filters) 713 714 if filepath: 715 self.display_loading_grayout(True, "Saving all comments of current view to .csv...") 716 717 with open(filepath, "w", newline='') as csv_file: 718 csv_writer = csv.writer(csv_file, delimiter="|") 719 for row in header: 720 csv_writer.writerow(row) 721 for row in zip(style, x, y, color, string): 722 csv_writer.writerow(row) 723 724 self.display_loading_grayout(False) 725 726 727 def on_right_click_load_comments(self): 728 """Open a dialog window for user to load comments to the main scene via .csv as saved previously. 729 730 Triggered from right-click menu on view. 731 """ 732 self.display_loading_grayout(True, "Selecting comment file (.csv) to load into current view...", pseudo_load_time=0) 733 734 folderpath = None 735 filepath_mainview = self.currentFile 736 737 if filepath_mainview: 738 folderpath = filepath_mainview 739 folderpath = os.path.dirname(folderpath) 740 # folderpath = folderpath + "\\" 741 else: 742 self.display_loading_grayout(False, pseudo_load_time=0) 743 return 744 745 filename, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Select comment file (.csv) to load into current view", folderpath, "Comma-Separated Value File (*.csv)") 746 747 if filename: 748 self.display_loading_grayout(True, "Loading comments from selected .csv into current view...") 749 750 with open(filename, "r", newline='') as csv_file: 751 csv_reader = csv.reader(csv_file, delimiter="|") 752 csv_list = list(csv_reader) 753 754 i = None 755 756 try: 757 i = csv_list.index(["Style", "Image x", "Image y", "Appearance", "String"]) 758 except ValueError: 759 box_type = QtWidgets.QMessageBox.Warning 760 title = "Invalid .csv comment file" 761 text = "The selected .csv comment file does not have a format accepted by this app." 762 box_buttons = QtWidgets.QMessageBox.Close 763 box = QtWidgets.QMessageBox(box_type, title, text, box_buttons) 764 box.exec_() 765 else: 766 i += 1 # Move to first comment item 767 no_comments = True 768 for row in csv_list[i:]: 769 if row[0] == "plain text": 770 no_comments = False 771 comment_x = float(row[1]) 772 comment_y = float(row[2]) 773 comment_color = row[3] 774 comment_string = row[4] 775 comment_pos = QtCore.QPointF(comment_x, comment_y) 776 self._scene_main_topleft.addItem(CommentItem(initial_scene_pos=comment_pos, color=comment_color, comment_text=comment_string, set_cursor_on_creation=False)) 777 if no_comments: 778 box_type = QtWidgets.QMessageBox.Warning 779 title = "No comments in .csv" 780 text = "No comments found in the selected .csv comment file." 781 box_buttons = QtWidgets.QMessageBox.Close 782 box = QtWidgets.QMessageBox(box_type, title, text, box_buttons) 783 box.exec_() 784 785 self.display_loading_grayout(False) 786 787 def on_right_click_set_relative_origin_position(self, string): 788 """Set orientation of the coordinate system for rulers by positioning the relative origin. 789 790 Allows users to switch the coordinate orientation: 791 "bottomleft" for Cartesian-style (positive X right, positive Y up) 792 topleft" for image-style (positive X right, positive Y down) 793 794 Args: 795 relative_origin_position (str): The position of the origin for coordinate system ("topleft" or "bottomleft"). 796 """ 797 for item in self._scene_main_topleft.items(): 798 if isinstance(item, RulerItem): 799 item.set_and_refresh_relative_origin_position(string) 800 801 def display_loading_grayout(self, boolean, text="Loading...", pseudo_load_time=0.2): 802 """Emit signal for showing/hiding a grayout screen to indicate loading sequences. 803 804 Args: 805 boolean (bool): True to show grayout; False to hide. 806 text (str): The text to show on the grayout. 807 pseudo_load_time (float): The delay (in seconds) to hide the grayout to give user feeling of action. 808 """ 809 self.signal_display_loading_grayout.emit(boolean, text, pseudo_load_time) 810 811 def update_mouse_rect(self, pos = None): 812 """Update the position of red 1x1 outline at the pointer in the main scene. 813 814 Args: 815 pos (QPoint): The position of the mouse on the widget. Set to None to make the function determine the position using the mouse global coordinates. 816 """ 817 if not self.mouse_rect_scene_main_topleft: 818 return 819 820 if pos is None: # Get position of split on main scene by directly pulling mouse global coordinates 821 point_of_cursor_global = QtGui.QCursor.pos() 822 point_of_mouse_on_widget = self._view_main_topleft.mapFromGlobal(QtGui.QCursor.pos()) 823 else: 824 point_of_mouse_on_widget = pos 825 826 mouse_rect_pos_origin = self._view_main_topleft.mapToScene(point_of_mouse_on_widget.x(),point_of_mouse_on_widget.y()) 827 mouse_rect_pos_origin.setX(math.floor(mouse_rect_pos_origin.x() - self.mouse_rect_width + 1)) 828 mouse_rect_pos_origin.setY(math.floor(mouse_rect_pos_origin.y() - self.mouse_rect_height + 1)) 829 830 self.mouse_rect_scene_main_topleft.setPos(mouse_rect_pos_origin.x(), mouse_rect_pos_origin.y()) 831 832 # Signals 833 signal_display_loading_grayout = QtCore.pyqtSignal(bool, str, float) 834 """Emitted when comments are being saved or loaded.""" 835 836 became_closed = QtCore.pyqtSignal() 837 """Emitted when SplitView is closed.""" 838 839 was_clicked_close_pushbutton = QtCore.pyqtSignal() 840 """Emitted when close pushbutton is clicked (pressed+released).""" 841 842 was_set_global_transform_mode = QtCore.pyqtSignal(bool) 843 """Emitted when transform mode is set for all views in right-click menu (passes it along).""" 844 845 was_set_scene_background_color = QtCore.pyqtSignal(list) 846 """Emitted when background color is set in right-click menu (passes it along).""" 847 848 positionChanged = QtCore.pyqtSignal(QtCore.QPoint) 849 """Emitted when mouse changes position.""" 850 851 sceneChanged = QtCore.pyqtSignal('QList<QRectF>') 852 """Scene Changed **Signal**. 853 854 Emitted whenever the |QGraphicsScene| content changes.""" 855 856 transformChanged = QtCore.pyqtSignal() 857 """Transformed Changed **Signal**. 858 859 Emitted whenever the |QGraphicsView| Transform matrix has been changed.""" 860 861 scrollChanged = QtCore.pyqtSignal() 862 """Scroll Changed **Signal**. 863 864 Emitted whenever the scrollbar position or range has changed.""" 865 866 def connectSbarSignals(self, slot): 867 """Connect to scrollbar changed signals. 868 869 :param slot: slot to connect scrollbar signals to.""" 870 self._view_main_topleft.connectSbarSignals(slot) 871 872 def disconnectSbarSignals(self): 873 self._view_main_topleft.disconnectSbarSignals() 874 875 @property 876 def pixmap_main_topleft(self): 877 """The currently viewed |QPixmap| (*QPixmap*).""" 878 return self._pixmapItem_main_topleft.pixmap() 879 880 @pixmap_main_topleft.setter 881 def pixmap_main_topleft(self, pixmap_main_topleft): 882 if pixmap_main_topleft is not None: 883 self._pixmapItem_main_topleft.setPixmap(pixmap_main_topleft) 884 self._pixmapItem_main_topleft.setTransformationMode(QtCore.Qt.SmoothTransformation) 885 self._pixmap_base_original = pixmap_main_topleft 886 self.set_opacity_base(100) 887 888 @QtCore.pyqtSlot() 889 def set_opacity_base(self, percent): 890 """Set transparency of base image. 891 892 Args: 893 percent (float,int): The transparency as percent opacity, where 100 is opaque (not transparent) and 0 is transparent (0-100). 894 """ 895 896 self._opacity_base = percent 897 898 pixmap_to_be_transparent = QtGui.QPixmap(self._pixmap_base_original.size()) 899 pixmap_to_be_transparent.fill(QtCore.Qt.transparent) 900 painter = QtGui.QPainter(pixmap_to_be_transparent) 901 painter.setOpacity(percent/100) 902 painter.drawPixmap(QtCore.QPoint(), self._pixmap_base_original) 903 painter.end() 904 905 self._pixmapItem_main_topleft.setPixmap(pixmap_to_be_transparent) 906 907 @property 908 def pixmap_topright(self): 909 """The currently viewed QPixmap of the top-right of the split.""" 910 return self._pixmapItem_topright.pixmap() 911 912 @pixmap_topright.setter 913 def pixmap_topright(self, pixmap): 914 self._pixmap_topright_original = pixmap 915 self.set_opacity_topright(100) 916 917 @QtCore.pyqtSlot() 918 def set_opacity_topright(self, percent): 919 """Set transparency of top-right of sliding overlay. 920 921 Allows users to see base image underneath. 922 Provide enhanced integration and comparison of images (for example, blending raking light with color). 923 924 Args: 925 percent (float,int): The transparency as percent opacity, where 100 is opaque (not transparent) and 0 is transparent (0-100). 926 """ 927 if not self.pixmap_topright_exists: 928 self._opacity_topright = 100 929 return 930 931 self._opacity_topright = percent 932 933 pixmap_to_be_transparent = QtGui.QPixmap(self._pixmap_topright_original.size()) 934 pixmap_to_be_transparent.fill(QtCore.Qt.transparent) 935 painter = QtGui.QPainter(pixmap_to_be_transparent) 936 painter.setOpacity(percent/100) 937 painter.drawPixmap(QtCore.QPoint(), self._pixmap_topright_original) 938 painter.end() 939 940 self._pixmapItem_topright.setPixmap(pixmap_to_be_transparent) 941 942 943 @property 944 def pixmap_bottomright(self): 945 """The currently viewed QPixmap of the bottom-right of the split.""" 946 return self._pixmapItem_bottomright.pixmap() 947 948 @pixmap_bottomright.setter 949 def pixmap_bottomright(self, pixmap): 950 self._pixmap_bottomright_original = pixmap 951 self.set_opacity_bottomright(100) 952 953 @QtCore.pyqtSlot() 954 def set_opacity_bottomright(self, percent): 955 """Set transparency of bottom-right of sliding overlay. 956 957 Args: 958 percent (float,int): The transparency as percent opacity, where 100 is opaque (not transparent) and 0 is transparent (0-100). 959 """ 960 if not self.pixmap_bottomright_exists: 961 self._opacity_bottomright = 100 962 return 963 964 self._opacity_bottomright = percent 965 966 pixmap_to_be_transparent = QtGui.QPixmap(self._pixmap_bottomright_original.size()) 967 pixmap_to_be_transparent.fill(QtCore.Qt.transparent) 968 painter = QtGui.QPainter(pixmap_to_be_transparent) 969 painter.setOpacity(percent/100) 970 painter.drawPixmap(QtCore.QPoint(), self._pixmap_bottomright_original) 971 painter.end() 972 973 self._pixmapItem_bottomright.setPixmap(pixmap_to_be_transparent) 974 975 976 @property 977 def pixmap_bottomleft(self): 978 """The currently viewed QPixmap of the bottom-left of the split.""" 979 return self._pixmapItem_bottomleft.pixmap() 980 981 @pixmap_bottomleft.setter 982 def pixmap_bottomleft(self, pixmap): 983 self._pixmap_bottomleft_original = pixmap 984 self.set_opacity_bottomleft(100) 985 986 @QtCore.pyqtSlot() 987 def set_opacity_bottomleft(self, percent): 988 """Set transparency of bottom-left of sliding overlay. 989 990 Args: 991 percent (float,int): The transparency as percent opacity, where 100 is opaque (not transparent) and 0 is transparent (0-100). 992 """ 993 if not self.pixmap_bottomleft_exists: 994 self._opacity_bottomleft = 100 995 return 996 997 self._opacity_bottomleft = percent 998 999 pixmap_to_be_transparent = QtGui.QPixmap(self._pixmap_bottomleft_original.size()) 1000 pixmap_to_be_transparent.fill(QtCore.Qt.transparent) 1001 painter = QtGui.QPainter(pixmap_to_be_transparent) 1002 painter.setOpacity(percent/100) 1003 painter.drawPixmap(QtCore.QPoint(), self._pixmap_bottomleft_original) 1004 painter.end() 1005 1006 self._pixmapItem_bottomleft.setPixmap(pixmap_to_be_transparent) 1007 1008 def moveEvent(self, event): 1009 """Override move event of frame.""" 1010 super().moveEvent(event) 1011 1012 def resizeEvent(self, event): 1013 """Override resize event of frame to ensure scene is also resized.""" 1014 self.resize_scene() 1015 super().resizeEvent(event) # Equivalent to QtWidgets.QFrame.resizeEvent(self, event) 1016 1017 def resize_scene(self): 1018 """Resize the scene to allow image to be panned just before the main pixmap leaves the viewport. 1019 1020 This is needed to expand the scene so that users can pan the pixmap such that its edges are at the center of the view. 1021 This changes the default behavior, which limits the scene to the bounds of the pixmap, thereby blocking users 1022 from panning outside the bounds of the pixmap, which can feel abrupt and restrictive. 1023 This takes care of preventing users from panning too far away from the pixmap. 1024 """ 1025 scene_to_viewport_factor = self._view_main_topleft.zoomFactor 1026 1027 width_viewport_window = self.width() 1028 height_viewport_window = self.height() 1029 1030 peek_precent = 0.1 # Percent of pixmap to be left "peeking" at bounds of pan 1031 peek_margin_x = width_viewport_window*peek_precent # [px] 1032 peek_margin_y = height_viewport_window*peek_precent 1033 1034 width_viewport = (width_viewport_window - peek_margin_x)/scene_to_viewport_factor # This is the size of the viewport on the screen 1035 height_viewport = (height_viewport_window - peek_margin_y)/scene_to_viewport_factor 1036 1037 width_pixmap = self._pixmapItem_main_topleft.pixmap().width() 1038 height_pixmap = self._pixmapItem_main_topleft.pixmap().height() 1039 1040 width_scene = 2.0*(width_viewport + width_pixmap/2.0) # The scene spans twice the viewport plus the pixmap 1041 height_scene = 2.0*(height_viewport + height_pixmap/2.0) 1042 1043 scene_rect = QtCore.QRectF(-width_scene/2.0 + width_pixmap/2.0,-height_scene/2.0 + height_pixmap/2.0,width_scene,height_scene) 1044 self._scene_main_topleft.setSceneRect(scene_rect) 1045 1046 def set_transform_mode_smooth_on(self): 1047 """Set transform mode to smooth (interpolate) when zoomfactor is >= 1.0.""" 1048 self.transform_mode_smooth = True 1049 self._scene_main_topleft.set_single_transform_mode_smooth(True) 1050 self.refresh_transform_mode() 1051 1052 def set_transform_mode_smooth_off(self): 1053 """Set transform mode to non-smooth (non-interpolated) when zoomfactor is >= 1.0.""" 1054 self.transform_mode_smooth = False 1055 self._scene_main_topleft.set_single_transform_mode_smooth(False) 1056 self.refresh_transform_mode() 1057 1058 def set_transform_mode_smooth(self, boolean): 1059 """Set transform mode when zoomfactor is >= 1.0. 1060 1061 Convenience function. 1062 1063 Args: 1064 boolean (bool): True to smooth (interpolate); False to fast (not interpolate). 1065 """ 1066 if boolean: 1067 self.set_transform_mode_smooth_on() 1068 elif not boolean: 1069 self.set_transform_mode_smooth_off() 1070 1071 @QtCore.pyqtSlot() 1072 def on_transformChanged(self): 1073 """Resize scene if image transform is changed (for example, when zoomed).""" 1074 self.resize_scene() 1075 self.update_split() 1076 1077 @property 1078 def viewName(self): 1079 """str: The name of the SplitView.""" 1080 return self._name 1081 1082 @viewName.setter 1083 def viewName(self, name): 1084 self._name = name 1085 1086 @property 1087 def handDragging(self): 1088 """bool: The hand dragging state.""" 1089 return self._view_main_topleft.handDragging 1090 1091 @property 1092 def scrollState(self): 1093 """tuple: The percentage of scene extents 1094 *(sceneWidthPercent, sceneHeightPercent)*""" 1095 return self._view_main_topleft.scrollState 1096 1097 @scrollState.setter 1098 def scrollState(self, state): 1099 self._view_main_topleft.scrollState = state 1100 1101 @property 1102 def zoomFactor(self): 1103 """float: The zoom scale factor.""" 1104 return self._view_main_topleft.zoomFactor 1105 1106 @zoomFactor.setter 1107 def zoomFactor(self, newZoomFactor): 1108 """Apply zoom to all views, taking into account the transform mode. 1109 1110 Args: 1111 newZoomFactor (float) 1112 """ 1113 if newZoomFactor < 1.0: 1114 self._pixmapItem_main_topleft.setTransformationMode(QtCore.Qt.SmoothTransformation) 1115 self._pixmapItem_topright.setTransformationMode(QtCore.Qt.SmoothTransformation) 1116 self._pixmapItem_bottomright.setTransformationMode(QtCore.Qt.SmoothTransformation) 1117 self._pixmapItem_bottomleft.setTransformationMode(QtCore.Qt.SmoothTransformation) 1118 elif self.transform_mode_smooth: 1119 self._pixmapItem_main_topleft.setTransformationMode(QtCore.Qt.SmoothTransformation) 1120 self._pixmapItem_topright.setTransformationMode(QtCore.Qt.SmoothTransformation) 1121 self._pixmapItem_bottomright.setTransformationMode(QtCore.Qt.SmoothTransformation) 1122 self._pixmapItem_bottomleft.setTransformationMode(QtCore.Qt.SmoothTransformation) 1123 else: 1124 self._pixmapItem_main_topleft.setTransformationMode(QtCore.Qt.FastTransformation) 1125 self._pixmapItem_topright.setTransformationMode(QtCore.Qt.FastTransformation) 1126 self._pixmapItem_bottomright.setTransformationMode(QtCore.Qt.FastTransformation) 1127 self._pixmapItem_bottomleft.setTransformationMode(QtCore.Qt.FastTransformation) 1128 1129 self._view_main_topleft.zoomFactor = newZoomFactor 1130 1131 newZoomFactor = newZoomFactor / self._view_topright.transform().m11() 1132 self._view_topright.scale(newZoomFactor, newZoomFactor) 1133 self._view_bottomright.scale(newZoomFactor, newZoomFactor) 1134 self._view_bottomleft.scale(newZoomFactor, newZoomFactor) 1135 1136 def refresh_transform_mode(self): 1137 """Refresh zoom of all views, taking into account the transform mode.""" 1138 self._view_main_topleft.zoomFactor 1139 if self.zoomFactor < 1.0: 1140 self._pixmapItem_main_topleft.setTransformationMode(QtCore.Qt.SmoothTransformation) 1141 self._pixmapItem_topright.setTransformationMode(QtCore.Qt.SmoothTransformation) 1142 self._pixmapItem_bottomright.setTransformationMode(QtCore.Qt.SmoothTransformation) 1143 self._pixmapItem_bottomleft.setTransformationMode(QtCore.Qt.SmoothTransformation) 1144 elif self.transform_mode_smooth: 1145 self._pixmapItem_main_topleft.setTransformationMode(QtCore.Qt.SmoothTransformation) 1146 self._pixmapItem_topright.setTransformationMode(QtCore.Qt.SmoothTransformation) 1147 self._pixmapItem_bottomright.setTransformationMode(QtCore.Qt.SmoothTransformation) 1148 self._pixmapItem_bottomleft.setTransformationMode(QtCore.Qt.SmoothTransformation) 1149 else: 1150 self._pixmapItem_main_topleft.setTransformationMode(QtCore.Qt.FastTransformation) 1151 self._pixmapItem_topright.setTransformationMode(QtCore.Qt.FastTransformation) 1152 self._pixmapItem_bottomright.setTransformationMode(QtCore.Qt.FastTransformation) 1153 self._pixmapItem_bottomleft.setTransformationMode(QtCore.Qt.FastTransformation) 1154 1155 @property 1156 def _horizontalScrollBar(self): 1157 """Get the SplitView horizontal scrollbar widget (*QScrollBar*). 1158 1159 (Only used for debugging purposes)""" 1160 return self._view_main_topleft.horizontalScrollBar() 1161 1162 @property 1163 def _verticalScrollBar(self): 1164 """Get the SplitView vertical scrollbar widget (*QScrollBar*). 1165 1166 (Only used for debugging purposes)""" 1167 return self._view_main_topleft.verticalScrollBar() 1168 1169 @property 1170 def _sceneRect(self): 1171 """Get the SplitView sceneRect (*QRectF*). 1172 1173 (Only used for debugging purposes)""" 1174 return self._view_main_topleft.sceneRect() 1175 1176 @QtCore.pyqtSlot() 1177 def scrollToTop(self): 1178 """Scroll to top of image.""" 1179 self._view_main_topleft.scrollToTop() 1180 1181 @QtCore.pyqtSlot() 1182 def scrollToBottom(self): 1183 """Scroll to bottom of image.""" 1184 self._view_main_topleft.scrollToBottom() 1185 1186 @QtCore.pyqtSlot() 1187 def scrollToBegin(self): 1188 """Scroll to left side of image.""" 1189 self._view_main_topleft.scrollToBegin() 1190 1191 @QtCore.pyqtSlot() 1192 def scrollToEnd(self): 1193 """Scroll to right side of image.""" 1194 self._view_main_topleft.scrollToEnd() 1195 1196 @QtCore.pyqtSlot() 1197 def centerView(self): 1198 """Center image in view.""" 1199 self._view_main_topleft.centerView() 1200 1201 @QtCore.pyqtSlot(bool) 1202 def enableScrollBars(self, enable): 1203 """Set visiblility of the view's scrollbars. 1204 1205 :param bool enable: True to enable the scrollbars """ 1206 self._view_main_topleft.enableScrollBars(enable) 1207 1208 @QtCore.pyqtSlot(bool) 1209 def enableHandDrag(self, enable): 1210 """Set whether dragging the view with the hand cursor is allowed. 1211 1212 :param bool enable: True to enable hand dragging """ 1213 self._view_main_topleft.enableHandDrag(enable) 1214 1215 @QtCore.pyqtSlot() 1216 def zoomIn(self): 1217 """Zoom in on image.""" 1218 self.scaleImage(self._zoomFactorDelta) 1219 1220 @QtCore.pyqtSlot() 1221 def zoomOut(self): 1222 """Zoom out on image.""" 1223 self.scaleImage(1 / self._zoomFactorDelta) 1224 1225 @QtCore.pyqtSlot() 1226 def actualSize(self): 1227 """Change zoom to show image at actual size. 1228 1229 (image pixel is equal to screen pixel)""" 1230 self.scaleImage(1.0, combine=False) 1231 1232 @QtCore.pyqtSlot() 1233 def fitToWindow(self): 1234 """Fit image within view. 1235 1236 If the viewport is wider than the main pixmap, then fit the pixmap to height; if the viewport is narrower, then fit the pixmap to width 1237 """ 1238 if not self._pixmapItem_main_topleft.pixmap(): 1239 return 1240 1241 padding_margin = 2 # Leaves visual gap between pixmap and border of viewport 1242 viewport_rect = self._view_main_topleft.viewport().rect().adjusted(padding_margin, padding_margin, 1243 -padding_margin, -padding_margin) 1244 aspect_ratio_viewport = viewport_rect.width()/viewport_rect.height() 1245 aspect_ratio_pixmap = self._pixmapItem_main_topleft.pixmap().width()/self._pixmapItem_main_topleft.pixmap().height() 1246 if aspect_ratio_viewport > aspect_ratio_pixmap: 1247 self.fitHeight() 1248 else: 1249 self.fitWidth() 1250 1251 self.transformChanged.emit() 1252 1253 @QtCore.pyqtSlot() 1254 def fitWidth(self): 1255 """Fit image width to view width.""" 1256 if not self._pixmapItem_main_topleft.pixmap(): 1257 return 1258 padding_margin = 2 1259 viewRect = self._view_main_topleft.viewport().rect().adjusted(padding_margin, padding_margin, 1260 -padding_margin, -padding_margin) 1261 factor = viewRect.width() / self._pixmapItem_main_topleft.pixmap().width() 1262 self.scaleImage(factor, combine=False) 1263 self._view_main_topleft.centerView() 1264 1265 @QtCore.pyqtSlot() 1266 def fitHeight(self): 1267 """Fit image height to view height.""" 1268 if not self._pixmapItem_main_topleft.pixmap(): 1269 return 1270 padding_margin = 2 1271 viewRect = self._view_main_topleft.viewport().rect().adjusted(padding_margin, padding_margin, 1272 -padding_margin, -padding_margin) 1273 factor = viewRect.height() / self._pixmapItem_main_topleft.pixmap().height() 1274 self.scaleImage(factor, combine=False) 1275 self._view_main_topleft.centerView() 1276 1277 def handleWheelNotches(self, notches): 1278 """Handle wheel notch event from underlying |QGraphicsView|. 1279 1280 :param float notches: Mouse wheel notches""" 1281 self.scaleImage(self._zoomFactorDelta ** notches) 1282 1283 def closeEvent(self, event): 1284 """Overriden in order to disconnect scrollbar signals before 1285 closing. 1286 1287 :param QEvent event: instance of a |QEvent| 1288 1289 If this isn't done Python crashes!""" 1290 #self.scrollChanged.disconnect() #doesn't prevent crash 1291 self.disconnectSbarSignals() 1292 1293 self._scene_main_topleft.deleteLater() 1294 self._view_main_topleft.deleteLater() 1295 del self._pixmap_base_original 1296 1297 self._scene_topright.deleteLater() 1298 self._view_topright.deleteLater() 1299 del self._pixmap_topright_original 1300 1301 self._scene_bottomright.deleteLater() 1302 self._view_bottomright.deleteLater() 1303 del self._pixmap_bottomright_original 1304 1305 self._scene_bottomleft.deleteLater() 1306 self._view_bottomleft.deleteLater() 1307 del self._pixmap_bottomleft_original 1308 1309 super().closeEvent(event) 1310 gc.collect() 1311 self.became_closed.emit() 1312 1313 def scaleImage(self, factor, combine=True): 1314 """Scale image by factor. 1315 1316 :param float factor: either new :attr:`zoomFactor` or amount to scale 1317 current :attr:`zoomFactor` 1318 1319 :param bool combine: if ``True`` scales the current 1320 :attr:`zoomFactor` by factor. Otherwise 1321 just sets :attr:`zoomFactor` to factor""" 1322 if not self._pixmapItem_main_topleft.pixmap(): 1323 return 1324 1325 if combine: 1326 self.zoomFactor = self.zoomFactor * factor 1327 else: 1328 self.zoomFactor = factor 1329 1330 self._view_main_topleft.checkTransformChanged() 1331 1332 def dumpTransform(self): 1333 """Dump view transform to stdout.""" 1334 self._view_main_topleft.dumpTransform(self._view_main_topleft.transform(), " "*4) 1335 1336 1337 def create_mouse_rect(self): 1338 """Create a red 1x1 outline at the pointer in the main scene. 1339 1340 Indicates to the user the size and position of the pixel over which the mouse is hovering. 1341 Helps to understand the position of individual pixels and their scale at the current zoom. 1342 """ 1343 1344 pen = QtGui.QPen() 1345 pen.setWidth(0.1) 1346 pen.setColor(QtCore.Qt.red) 1347 pen.setCapStyle(QtCore.Qt.SquareCap) 1348 pen.setJoinStyle(QtCore.Qt.MiterJoin) 1349 1350 brush = QtGui.QBrush() 1351 brush.setColor(QtCore.Qt.transparent) 1352 1353 self.mouse_rect_width = 1 1354 self.mouse_rect_height = 1 1355 1356 self.mouse_rect_topleft = QtCore.QPointF(0,0) 1357 self.mouse_rect_bottomright = QtCore.QPointF(self.mouse_rect_width-0.01, self.mouse_rect_height-0.01) 1358 self.mouse_rect = QtCore.QRectF(self.mouse_rect_topleft, self.mouse_rect_bottomright) 1359 1360 self.mouse_rect_scene_main_topleft = QtWidgets.QGraphicsRectItem(self.mouse_rect) # To add the same item in two scenes, you need to create two unique items 1361 1362 self.mouse_rect_scene_main_topleft.setPos(0,0) 1363 1364 self.mouse_rect_scene_main_topleft.setBrush(brush) 1365 self.mouse_rect_scene_main_topleft.setPen(pen) 1366 1367 self._scene_main_topleft.addItem(self.mouse_rect_scene_main_topleft) 1368 1369 def set_mouse_rect_visible(self, boolean): 1370 """Set the visibilty of the red 1x1 outline at the pointer in the main scene. 1371 1372 Args: 1373 boolean (bool): True to show 1x1 outline; False to hide. 1374 """ 1375 self.mouse_rect_scene_main_topleft.setVisible(boolean)
44class SplitView(QtWidgets.QFrame): 45 """Image viewing widget for individual images and sliding overlays. 46 47 Creates an interface with a base image as a main image located at the top left 48 and optionally 3 other images (top-left, bottom-left, bottom-right) as a sliding overlay. 49 Supports zoom and pan. 50 Enables synchronized zoom and pan via signals. 51 Input images for a given sliding overlay must have identical resolutions to 52 function properly. 53 54 Args: 55 pixmap (QPixmap): The main image to be viewed; the basis of the sliding overlay (main; topleft) 56 filename_main_topleft (str): The image filepath of the main image. 57 name (str): The name of the viewing widget. 58 pixmap_topright (QPixmap): The top-right image of the sliding overlay (set None to exclude). 59 pixmap_bottomleft (QPixmap): The bottom-left image of the sliding overlay (set None to exclude). 60 pixmap_bottomright (QPixmap): The bottom-right image of the sliding overlay (set None to exclude). 61 """ 62 63 def __init__(self, pixmap_main_topleft=None, filename_main_topleft=None, name=None, 64 pixmap_topright=None, pixmap_bottomleft=None, pixmap_bottomright=None, transform_mode_smooth=False): 65 super().__init__() 66 67 self.currentFile = filename_main_topleft 68 69 self.setWindowFlags(QtCore.Qt.FramelessWindowHint) # Clean appearance 70 self.setFrameStyle(QtWidgets.QFrame.NoFrame) 71 72 self.viewName = name 73 self.comment_position_object = None 74 75 pixmap_main_topleft = self.pixmap_none_ify(pixmap_main_topleft) # Return None if given QPixmap has zero width/height or is not a QPixmap 76 pixmap_topright = self.pixmap_none_ify(pixmap_topright) 77 pixmap_bottomleft = self.pixmap_none_ify(pixmap_bottomleft) 78 pixmap_bottomright = self.pixmap_none_ify(pixmap_bottomright) 79 80 self.pixmap_topright_exists = (pixmap_topright is not None) # Boolean for existence of pixmap is easier to check 81 self.pixmap_bottomright_exists = (pixmap_bottomright is not None) 82 self.pixmap_bottomleft_exists = (pixmap_bottomleft is not None) 83 84 # Pixmaps which do no exist should be transparent, so they are made into an empty pixmap 85 if not self.pixmap_bottomright_exists: 86 pixmap_bottomright = QtGui.QPixmap() 87 88 if not self.pixmap_topright_exists: 89 pixmap_topright = QtGui.QPixmap() 90 91 if not self.pixmap_bottomleft_exists: 92 pixmap_bottomleft = QtGui.QPixmap() 93 94 self._zoomFactorDelta = 1.25 # How much zoom for each zoom call 95 96 self.transform_mode_smooth = transform_mode_smooth 97 98 # Sliding overlay is based on the main pixmap view of the top-left pixmap 99 # self._scene_main_topleft = QtWidgets.QGraphicsScene() 100 self._scene_main_topleft = CustomQGraphicsScene() 101 self._view_main_topleft = SynchableGraphicsView(self._scene_main_topleft) 102 103 self._view_main_topleft.setInteractive(True) # Functional settings 104 self._view_main_topleft.setViewportUpdateMode(QtWidgets.QGraphicsView.MinimalViewportUpdate) 105 self._view_main_topleft.setResizeAnchor(QtWidgets.QGraphicsView.AnchorViewCenter) 106 self._view_main_topleft.setRenderHints(QtGui.QPainter.SmoothPixmapTransform | QtGui.QPainter.Antialiasing) 107 108 self._scene_main_topleft.changed.connect(self.sceneChanged) # Pass along underlying signals 109 self._view_main_topleft.transformChanged.connect(self.transformChanged) 110 self._view_main_topleft.transformChanged.connect(self.on_transformChanged) 111 self._view_main_topleft.scrollChanged.connect(self.scrollChanged) 112 self._view_main_topleft.wheelNotches.connect(self.handleWheelNotches) 113 self._scene_main_topleft.right_click_comment.connect(self.on_right_click_comment) 114 self._scene_main_topleft.right_click_ruler.connect(self.on_right_click_ruler) 115 self._scene_main_topleft.right_click_save_all_comments.connect(self.on_right_click_save_all_comments) 116 self._scene_main_topleft.right_click_load_comments.connect(self.on_right_click_load_comments) 117 self._scene_main_topleft.right_click_relative_origin_position.connect(self.on_right_click_set_relative_origin_position) 118 self._scene_main_topleft.changed_px_per_unit.connect(self.on_changed_px_per_unit) 119 self._scene_main_topleft.right_click_single_transform_mode_smooth.connect(self.set_transform_mode_smooth) 120 self._scene_main_topleft.right_click_all_transform_mode_smooth.connect(self.was_set_global_transform_mode) 121 self._scene_main_topleft.right_click_background_color.connect(self.set_scene_background_color) 122 self._scene_main_topleft.right_click_background_color.connect(self.was_set_scene_background_color) 123 124 self._pixmapItem_main_topleft = QtWidgets.QGraphicsPixmapItem() 125 self._scene_main_topleft.addItem(self._pixmapItem_main_topleft) 126 127 # A pseudo view directly atop the main view is needed to drive the position of the split and layout of the four pixmaps 128 self._view_layoutdriving_topleft = QtWidgets.QGraphicsView() 129 self._view_layoutdriving_topleft.setStyleSheet("border: 0px; border-style: solid; background-color: rgba(0,0,0,0)") 130 self._view_layoutdriving_topleft.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents) 131 self._view_layoutdriving_topleft.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) 132 self._view_layoutdriving_topleft.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) 133 self._view_layoutdriving_topleft.setResizeAnchor(QtWidgets.QGraphicsView.AnchorUnderMouse) 134 135 # Add top right pixmap view 136 self._pixmapItem_topright = QtWidgets.QGraphicsPixmapItem() 137 self.pixmap_topright = pixmap_topright 138 139 self._scene_topright = QtWidgets.QGraphicsScene() 140 self._scene_topright.addItem(self._pixmapItem_topright) 141 142 self._view_topright = QtWidgets.QGraphicsView(self._scene_topright) 143 self._view_topright.setStyleSheet("border: 0px; border-style: solid; border-color: rgba(0,0,0,0); background-color: rgba(0,0,0,0);") 144 self._view_topright.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents) 145 self._view_topright.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) 146 self._view_topright.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) 147 self._view_topright.setResizeAnchor(QtWidgets.QGraphicsView.AnchorViewCenter) 148 149 # Add bottom left pixmap view 150 self._pixmapItem_bottomleft = QtWidgets.QGraphicsPixmapItem() 151 self.pixmap_bottomleft = pixmap_bottomleft 152 153 self._scene_bottomleft = QtWidgets.QGraphicsScene() 154 self._scene_bottomleft.addItem(self._pixmapItem_bottomleft) 155 156 self._view_bottomleft = QtWidgets.QGraphicsView(self._scene_bottomleft) 157 self._view_bottomleft.setStyleSheet("border: 0px; border-style: solid; border-color: rgba(0,0,0,0); background-color: rgba(0,0,0,0);") 158 self._view_bottomleft.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents) 159 self._view_bottomleft.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) 160 self._view_bottomleft.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) 161 self._view_bottomleft.setResizeAnchor(QtWidgets.QGraphicsView.AnchorViewCenter) 162 163 # Add bottom right pixmap view 164 self._pixmapItem_bottomright = QtWidgets.QGraphicsPixmapItem() 165 self.pixmap_bottomright = pixmap_bottomright 166 167 self._scene_bottomright = QtWidgets.QGraphicsScene() 168 self._scene_bottomright.addItem(self._pixmapItem_bottomright) 169 170 self._view_bottomright = QtWidgets.QGraphicsView(self._scene_bottomright) 171 self._view_bottomright.setStyleSheet("border: 0px; border-style: solid; border-color: rgba(0,0,0,0); background-color: rgba(0,0,0,0);") 172 self._view_bottomright.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents) 173 self._view_bottomright.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) 174 self._view_bottomright.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) 175 self._view_bottomright.setResizeAnchor(QtWidgets.QGraphicsView.AnchorViewCenter) 176 177 # Make the sizes of the four views entirely dictated by the "layout driving" view 178 size_policy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Ignored) 179 self._view_main_topleft.setSizePolicy(size_policy) 180 self._view_topright.setSizePolicy(size_policy) 181 self._view_bottomright.setSizePolicy(size_policy) 182 self._view_bottomleft.setSizePolicy(size_policy) 183 184 # By default the split is set to half the widget's size so all pixmap views are equally sized at the start 185 self._view_layoutdriving_topleft.setMaximumWidth(self.width()/2.0) 186 self._view_layoutdriving_topleft.setMaximumHeight(self.height()/2.0) 187 188 if pixmap_main_topleft: # Instantiate transform and resizing 189 self.pixmap_main_topleft = pixmap_main_topleft 190 191 # SplitView layout 192 self._layout = QtWidgets.QGridLayout() 193 self._layout.setContentsMargins(0, 0, 0, 0) 194 self._layout.setSpacing(0) 195 196 self.setContentsMargins(0, 0, 0, 0) 197 198 # Labels for the four pixmap views 199 self.label_main_topleft = FilenameLabel(visibility_based_on_text=True, belongs_to_split=True) 200 self.label_topright = FilenameLabel(visibility_based_on_text=True, belongs_to_split=True) 201 self.label_bottomright = FilenameLabel(visibility_based_on_text=True, belongs_to_split=True) 202 self.label_bottomleft = FilenameLabel(visibility_based_on_text=True, belongs_to_split=True) 203 204 self.label_main_topleft.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents) 205 self.label_topright.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents) 206 self.label_bottomright.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents) 207 self.label_bottomleft.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents) 208 209 # Pushbutton to close the image window 210 self.close_pushbutton = QtWidgets.QPushButton("×") 211 self.close_pushbutton.setToolTip("Close image window") 212 self.close_pushbutton.clicked.connect(self.was_clicked_close_pushbutton) 213 self.close_pushbutton_always_visible = True 214 215 # Create deadzones along the bounds of SplitView to fix the issue of resize handles showing in QMdiArea despite windowless setting. 216 # An event tracker "bypass" is needed for each deadzone because they hide the mouse from the sliding overlay, so the mouse must be separately tracked to ensure the split is updated. 217 px_deadzone = 8 218 219 self.resize_deadzone_top = QtWidgets.QPushButton("") 220 self.resize_deadzone_top.setFixedHeight(px_deadzone) 221 self.resize_deadzone_top.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed) 222 self.resize_deadzone_top.setEnabled(False) 223 self.resize_deadzone_top.setStyleSheet(""" 224 QPushButton { 225 color: transparent; 226 background-color: transparent; 227 border: 0px black; 228 } 229 """) 230 tracker_deadzone_top = EventTrackerSplitBypassDeadzone(self.resize_deadzone_top) 231 tracker_deadzone_top.mouse_position_changed_global.connect(self.update_split_given_global) 232 233 self.resize_deadzone_bottom = QtWidgets.QPushButton("") 234 self.resize_deadzone_bottom.setFixedHeight(px_deadzone) 235 self.resize_deadzone_bottom.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed) 236 self.resize_deadzone_bottom.setEnabled(False) 237 self.resize_deadzone_bottom.setStyleSheet(""" 238 QPushButton { 239 color: transparent; 240 background-color: transparent; 241 border: 0px black; 242 } 243 """) 244 tracker_deadzone_bottom = EventTrackerSplitBypassDeadzone(self.resize_deadzone_bottom) 245 tracker_deadzone_bottom.mouse_position_changed_global.connect(self.update_split_given_global) 246 247 self.resize_deadzone_left = QtWidgets.QPushButton("") 248 self.resize_deadzone_left.setFixedWidth(px_deadzone) 249 self.resize_deadzone_left.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Expanding) 250 self.resize_deadzone_left.setEnabled(False) 251 self.resize_deadzone_left.setStyleSheet(""" 252 QPushButton { 253 color: transparent; 254 background-color: transparent; 255 border: 0px black; 256 } 257 """) 258 tracker_deadzone_left = EventTrackerSplitBypassDeadzone(self.resize_deadzone_left) 259 tracker_deadzone_left.mouse_position_changed_global.connect(self.update_split_given_global) 260 261 self.resize_deadzone_right = QtWidgets.QPushButton("") 262 self.resize_deadzone_right.setFixedWidth(px_deadzone) 263 self.resize_deadzone_right.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Expanding) 264 self.resize_deadzone_right.setEnabled(False) 265 self.resize_deadzone_right.setStyleSheet(""" 266 QPushButton { 267 color: transparent; 268 background-color: transparent; 269 border: 0px black; 270 } 271 """) 272 tracker_deadzone_right = EventTrackerSplitBypassDeadzone(self.resize_deadzone_right) 273 tracker_deadzone_right.mouse_position_changed_global.connect(self.update_split_given_global) 274 275 # A frame is placed over the border of the widget to highlight it as the active subwindow in Butterfly Viewer. 276 self.frame_hud = QtWidgets.QFrame() 277 self.frame_hud.setStyleSheet("border: 0px solid transparent") 278 self.frame_hud.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents) 279 280 # Set layout 281 self._layout.addWidget(self._view_main_topleft, 0, 0, 2, 2) 282 self._layout.addWidget(self._view_layoutdriving_topleft, 0, 0) 283 self._layout.addWidget(self._view_topright, 0, 1) 284 self._layout.addWidget(self._view_bottomleft, 1, 0) 285 self._layout.addWidget(self._view_bottomright, 1, 1) 286 287 self._layout.addWidget(self.resize_deadzone_top, 0, 0, 2, 2, QtCore.Qt.AlignTop) 288 self._layout.addWidget(self.resize_deadzone_bottom, 0, 0, 2, 2, QtCore.Qt.AlignBottom) 289 self._layout.addWidget(self.resize_deadzone_left, 0, 0, 2, 2, QtCore.Qt.AlignLeft) 290 self._layout.addWidget(self.resize_deadzone_right, 0, 0, 2, 2, QtCore.Qt.AlignRight) 291 292 self._layout.addWidget(self.frame_hud, 0, 0, 2, 2) 293 294 self._layout.addWidget(self.label_main_topleft, 0, 0, 2, 2, QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft) 295 self._layout.addWidget(self.label_topright, 0, 0, 2, 2, QtCore.Qt.AlignTop | QtCore.Qt.AlignRight) 296 self._layout.addWidget(self.label_bottomright, 0, 0, 2, 2, QtCore.Qt.AlignBottom | QtCore.Qt.AlignRight) 297 self._layout.addWidget(self.label_bottomleft, 0, 0, 2, 2, QtCore.Qt.AlignBottom | QtCore.Qt.AlignLeft) 298 299 self._layout.addWidget(self.close_pushbutton, 0, 0, 2, 2, QtCore.Qt.AlignTop | QtCore.Qt.AlignRight) 300 301 self.setLayout(self._layout) 302 303 self.set_scene_background_color(self._scene_main_topleft.background_color) 304 self._view_main_topleft.setStyleSheet("border: 0px; border-style: solid; border-color: red; background-color: rgba(0,0,0,0)") 305 306 # Track the mouse position to know where to set the split 307 self.tracker = EventTracker(self) 308 self.tracker.mouse_position_changed.connect(self.positionChanged) 309 self.tracker.mouse_position_changed.connect(self.on_positionChanged) 310 311 # Create a rectangular box the size of one pixel in the main scene to show the user the size and position of the pixel over which their mouse is hovering 312 self.mouse_rect_scene_main_topleft = None 313 self.create_mouse_rect() 314 315 # Allow users to lock the split and remember where the split was last set 316 self.split_locked = False 317 self.last_updated_point_of_split_on_scene_main = QtCore.QPoint() 318 319 self.enableScrollBars(False) # Clean look with no scrollbars 320 321 @property 322 def currentFile(self): 323 """str: Filepath of base image (filename_main_topleft).""" 324 return self._currentFile 325 326 @currentFile.setter 327 def currentFile(self, filename_main_topleft): 328 self._currentFile = QtCore.QFileInfo(filename_main_topleft).canonicalFilePath() 329 self._isUntitled = False 330 self.setWindowTitle(self.userFriendlyCurrentFile) 331 332 @property 333 def userFriendlyCurrentFile(self): 334 """str: Filename of base image.""" 335 if self.currentFile: 336 return strippedName(self.currentFile) 337 else: 338 return "" 339 340 def set_close_pushbutton_always_visible(self, boolean): 341 """Enable/disable the always-on visiblilty of the close X of the view. 342 343 Arg: 344 boolean (bool): True to show the close X always; False to hide unless mouse hovers over. 345 """ 346 self.close_pushbutton_always_visible = boolean 347 self.refresh_close_pushbutton_stylesheet() 348 349 def refresh_close_pushbutton_stylesheet(self): 350 """Refresh stylesheet of close pushbutton based on background color and visibility.""" 351 if not self.close_pushbutton: 352 return 353 always_visible = self.close_pushbutton_always_visible 354 background_rgb = self._scene_main_topleft.background_rgb 355 avg_background_rgb = sum(background_rgb)/len(background_rgb) 356 if not always_visible: # Hide unless hovered 357 self.close_pushbutton.setStyleSheet(""" 358 QPushButton { 359 width: 1.8em; 360 height: 1.8em; 361 color: transparent; 362 background-color: rgba(223, 0, 0, 0); 363 font-weight: bold; 364 border-width: 0px; 365 border-style: solid; 366 border-color: transparent; 367 font-size: 10pt; 368 } 369 QPushButton:hover { 370 color: white; 371 background-color: rgba(223, 0, 0, 223); 372 } 373 QPushButton:pressed { 374 color: white; 375 background-color: rgba(255, 0, 0, 255); 376 } 377 """) 378 else: # Always visible 379 if avg_background_rgb >= 223: # Unhovered is black X on light background 380 self.close_pushbutton.setStyleSheet(""" 381 QPushButton { 382 width: 1.8em; 383 height: 1.8em; 384 color: black; 385 background-color: rgba(223, 0, 0, 0); 386 font-weight: bold; 387 border-width: 0px; 388 border-style: solid; 389 border-color: transparent; 390 font-size: 10pt; 391 } 392 QPushButton:hover { 393 color: white; 394 background-color: rgba(223, 0, 0, 223); 395 } 396 QPushButton:pressed { 397 color: white; 398 background-color: rgba(255, 0, 0, 255); 399 } 400 """) 401 else: # Unhovered is white X on dark background 402 self.close_pushbutton.setStyleSheet(""" 403 QPushButton { 404 width: 1.8em; 405 height: 1.8em; 406 color: white; 407 background-color: rgba(223, 0, 0, 0); 408 font-weight: bold; 409 border-width: 0px; 410 border-style: solid; 411 border-color: transparent; 412 font-size: 10pt; 413 } 414 QPushButton:hover { 415 color: white; 416 background-color: rgba(223, 0, 0, 223); 417 } 418 QPushButton:pressed { 419 color: white; 420 background-color: rgba(255, 0, 0, 255); 421 } 422 """) 423 424 425 def set_scene_background(self, brush): 426 """Set scene background color with QBrush. 427 428 Args: 429 brush (QBrush) 430 """ 431 if not self._scene_main_topleft: 432 return 433 self._scene_main_topleft.setBackgroundBrush(brush) 434 435 def set_scene_background_color(self, color: list): 436 """Set scene background color with color list. 437 438 The init for CustomQGraphicsScene contains the ground truth for selectable background colors. 439 440 Args: 441 color (list): Descriptor string and RGB int values. Example: ["White", 255, 255, 255]. 442 """ 443 if not self._scene_main_topleft: 444 return 445 rgb = color[1:4] 446 rgb_clamp = [max(min(channel, 255), 0) for channel in rgb] 447 brush = QtGui.QColor(rgb_clamp[0], rgb_clamp[1], rgb_clamp[2]) 448 self.set_scene_background(brush) 449 self._scene_main_topleft.background_color = color 450 self.refresh_close_pushbutton_stylesheet() 451 452 453 def pixmap_none_ify(self, pixmap): 454 """Return None if pixmap has no pixels. 455 456 Args: 457 pixmap (QPixmap) 458 459 Returns: 460 None if pixmap has no pixels; same pixmap if it has pixels 461 """ 462 if pixmap: 463 if pixmap.width()==0 or pixmap.height==0: 464 return None 465 else: 466 return pixmap 467 else: 468 return None 469 470 @QtCore.pyqtSlot(QtCore.QPoint) 471 def on_positionChanged(self, pos): 472 """Update the position of the split and the 1x1 pixel rectangle. 473 474 Triggered when mouse is moved. 475 476 Args: 477 pos (QPoint): The position of the mouse relative to widget. 478 """ 479 point_of_mouse_on_widget = pos 480 481 self.update_split(point_of_mouse_on_widget) 482 self.update_mouse_rect(point_of_mouse_on_widget) 483 484 def set_split(self, x_percent=0.5, y_percent=0.5, ignore_lock=False, percent_of_visible=False): 485 """Set the position of the split with x and y as proportion of base image's resolution. 486 487 Sets split position using a proportion of x and y (by default of entire main pixmap; can be set to proportion of visible pixmap). 488 Top left is x=0, y=0; bottom right is x=1, y=1. 489 This is needed to position the split without mouse movement from user (for example, to preview the effect of the transparency sliders in Butterfly Viewer) 490 491 Args: 492 x_percent (float): The position of the split as a proportion (0-1) of the base image's horizontal resolution. 493 y_percent (float): The position of the split as a proportion (0-1) of the base image's vertical resolution. 494 ignore_lock (bool): True to ignore the lock status of the split; False to adhere. 495 percent_of_visible (bool): True to set split as proportion of visible area; False as proportion of the full image resolution. 496 """ 497 if percent_of_visible: 498 x = x_percent*self.width() 499 y = y_percent*self.height() 500 point_of_split_on_widget = QtCore.QPoint(x, y) 501 else: 502 width_pixmap_main_topleft = self._pixmapItem_main_topleft.pixmap().width() 503 height_pixmap_main_topleft = self._pixmapItem_main_topleft.pixmap().height() 504 505 x = x_percent*width_pixmap_main_topleft 506 y = y_percent*height_pixmap_main_topleft 507 508 point_of_split_on_scene = QtCore.QPointF(x,y) 509 510 point_of_split_on_widget = self._view_main_topleft.mapFromScene(point_of_split_on_scene) 511 512 self.update_split(point_of_split_on_widget, ignore_lock=ignore_lock) 513 514 def update_split(self, pos = None, pos_is_global=False, ignore_lock=False): 515 """Update the position of the split with mouse position. 516 517 Args: 518 pos (QPoint): Position of the mouse. 519 pos_is_global (bool): True if given mouse position is relative to MdiChild; False if global position. 520 ignore_lock (bool): True to ignore (bypass) the status of the split lock. 521 """ 522 if pos is None: # Get position of the split from the mouse's global coordinates (can be slow!) 523 point_of_cursor_global = QtGui.QCursor.pos() 524 point_of_mouse_on_widget = self._view_main_topleft.mapFromGlobal(point_of_cursor_global) 525 else: 526 if pos_is_global: 527 point_of_mouse_on_widget = self._view_main_topleft.mapFromGlobal(pos) 528 else: 529 point_of_mouse_on_widget = pos 530 531 point_of_mouse_on_widget.setX(point_of_mouse_on_widget.x()+1) # Offset +1 needed to have mouse cursor be hovering over the main scene (e.g., to allow manipulation of graphics item) 532 point_of_mouse_on_widget.setY(point_of_mouse_on_widget.y()+1) 533 534 self.last_updated_point_of_split_on_scene_main = self._view_main_topleft.mapToScene(point_of_mouse_on_widget.x(), point_of_mouse_on_widget.y()) 535 536 point_of_bottom_right_on_widget = QtCore.QPointF(self.width(), self.height()) 537 538 point_of_widget_origin_on_scene_main = self._view_main_topleft.mapToScene(0,0) 539 540 point_of_split_on_scene_main = self._view_main_topleft.mapToScene(max(point_of_mouse_on_widget.x(),0),max(point_of_mouse_on_widget.y(),0)) 541 point_of_bottom_right_on_scene_main = self._view_main_topleft.mapToScene(point_of_bottom_right_on_widget.x(), point_of_bottom_right_on_widget.y()) 542 543 self._view_layoutdriving_topleft.setMaximumWidth(max(point_of_mouse_on_widget.x(),0)) 544 self._view_layoutdriving_topleft.setMaximumHeight(max(point_of_mouse_on_widget.y(),0)) 545 546 render_buffer = 100 # Needed to prevent slight pixel offset of the sliding overlays when zoomed-out below ~0.5x 547 548 top_left_of_scene_topright = QtCore.QPointF(point_of_split_on_scene_main.x(), point_of_widget_origin_on_scene_main.y()) 549 bottom_right_of_scene_topright = QtCore.QPointF(point_of_bottom_right_on_scene_main.x() + render_buffer, point_of_split_on_scene_main.y() + render_buffer) 550 551 top_left_of_scene_bottomright = QtCore.QPointF(point_of_split_on_scene_main.x(), point_of_split_on_scene_main.y()) 552 bottom_right_of_scene_bottomright = QtCore.QPointF(point_of_bottom_right_on_scene_main.x() + render_buffer, point_of_bottom_right_on_scene_main.y() + render_buffer) 553 554 top_left_of_scene_bottomleft = QtCore.QPointF(point_of_widget_origin_on_scene_main.x(), point_of_split_on_scene_main.y()) 555 bottom_right_of_scene_bottomleft = QtCore.QPointF(point_of_split_on_scene_main.x() + render_buffer, point_of_bottom_right_on_scene_main.y() + render_buffer) 556 557 rect_of_scene_topright = QtCore.QRectF(top_left_of_scene_topright, bottom_right_of_scene_topright) 558 rect_of_scene_bottomright = QtCore.QRectF(top_left_of_scene_bottomright, bottom_right_of_scene_bottomright) 559 rect_of_scene_bottomleft = QtCore.QRectF(top_left_of_scene_bottomleft, bottom_right_of_scene_bottomleft) 560 561 self._view_topright.setSceneRect(rect_of_scene_topright) 562 self._view_bottomright.setSceneRect(rect_of_scene_bottomright) 563 self._view_bottomleft.setSceneRect(rect_of_scene_bottomleft) 564 565 self._view_topright.centerOn(top_left_of_scene_topright) 566 self._view_bottomright.centerOn(top_left_of_scene_bottomright) 567 self._view_bottomleft.centerOn(top_left_of_scene_bottomleft) 568 569 def refresh_split_based_on_last_updated_point_of_split_on_scene_main(self): 570 """Refresh the position of the split using the previously recorded split location. 571 572 This is needed to maintain the position of the split during synchronized zooming and panning. 573 """ 574 point_of_mouse_on_widget = self._view_main_topleft.mapFromScene(self.last_updated_point_of_split_on_scene_main) 575 576 point_of_bottom_right_on_widget = QtCore.QPointF(self.width(), self.height()) 577 578 point_of_widget_origin_on_scene_main = self._view_main_topleft.mapToScene(0,0) 579 point_of_split_on_scene_main = self._view_main_topleft.mapToScene(max(point_of_mouse_on_widget.x(),0),max(point_of_mouse_on_widget.y(),0)) 580 point_of_bottom_right_on_scene_main = self._view_main_topleft.mapToScene(point_of_bottom_right_on_widget.x(), point_of_bottom_right_on_widget.y()) 581 582 self._view_layoutdriving_topleft.setMaximumWidth(max(point_of_mouse_on_widget.x(),0)) 583 self._view_layoutdriving_topleft.setMaximumHeight(max(point_of_mouse_on_widget.y(),0)) 584 585 render_buffer = 100 # Needed to prevent slight pixel offset of the sliding overlays when zoomed-out below ~0.5x 586 587 top_left_of_scene_topright = QtCore.QPointF(point_of_split_on_scene_main.x(), point_of_widget_origin_on_scene_main.y()) 588 bottom_right_of_scene_topright = QtCore.QPointF(point_of_bottom_right_on_scene_main.x() + render_buffer, point_of_split_on_scene_main.y() + render_buffer) 589 590 top_left_of_scene_bottomright = QtCore.QPointF(point_of_split_on_scene_main.x(), point_of_split_on_scene_main.y()) 591 bottom_right_of_scene_bottomright = QtCore.QPointF(point_of_bottom_right_on_scene_main.x() + render_buffer, point_of_bottom_right_on_scene_main.y() + render_buffer) 592 593 top_left_of_scene_bottomleft = QtCore.QPointF(point_of_widget_origin_on_scene_main.x(), point_of_split_on_scene_main.y()) 594 bottom_right_of_scene_bottomleft = QtCore.QPointF(point_of_split_on_scene_main.x() + render_buffer, point_of_bottom_right_on_scene_main.y() + render_buffer) 595 596 rect_of_scene_topright = QtCore.QRectF(top_left_of_scene_topright, bottom_right_of_scene_topright) 597 rect_of_scene_bottomright = QtCore.QRectF(top_left_of_scene_bottomright, bottom_right_of_scene_bottomright) 598 rect_of_scene_bottomleft = QtCore.QRectF(top_left_of_scene_bottomleft, bottom_right_of_scene_bottomleft) 599 600 self._view_topright.setSceneRect(rect_of_scene_topright) 601 self._view_bottomright.setSceneRect(rect_of_scene_bottomright) 602 self._view_bottomleft.setSceneRect(rect_of_scene_bottomleft) 603 604 self._view_topright.centerOn(top_left_of_scene_topright) 605 self._view_bottomright.centerOn(top_left_of_scene_bottomright) 606 self._view_bottomleft.centerOn(top_left_of_scene_bottomleft) 607 608 def update_split_given_global(self, pos_global): 609 """Update the position of the split based on given global mouse position. 610 611 Convenience function. 612 613 Args: 614 pos_global (QPoint): The position of the mouse in global coordinates. 615 """ 616 self.update_split(pos = pos_global, pos_is_global=True) 617 618 619 def on_right_click_comment(self, scene_pos): 620 """Create an editable and movable comment on the scene of the main view at the given scene position. 621 622 Args: 623 pos (QPointF): position of the comment datum on the scene. 624 """ 625 pos_on_scene = scene_pos 626 self._scene_main_topleft.addItem(CommentItem(initial_scene_pos=pos_on_scene, set_cursor_on_creation=True)) 627 628 def on_right_click_ruler(self, scene_pos, relative_origin_position, unit="px", px_per_unit=1.0, update_px_per_unit_on_existing=False): 629 """Create a movable ruler on the scene of the main view at the given scene position. 630 631 Args: 632 scene_pos (QPointF): The position of the ruler center on the scene. 633 relative_origin_position (str): The position of the origin for coordinate system ("topleft" or "bottomleft"). 634 unit (str): The text for labeling units of ruler values. 635 px_per_unit (float): The conversion for pixels to units. For example, 10 means 10 pixels-per-unit, meaning the ruler value will show 1 when measuring 10 pixels. 636 update_px_per_unit_on_existing (bool): False always. (Legacy from past versions; future work should remove. 637 """ 638 placement_factor = 1/3 639 px_per_unit = px_per_unit 640 relative_origin_position = relative_origin_position 641 642 widget_width = self.width() 643 widget_height = self.height() 644 645 pos_p1 = self._view_main_topleft.mapToScene(widget_width*placement_factor, widget_height*placement_factor) 646 pos_p2 = self._view_main_topleft.mapToScene(widget_width*(1-placement_factor), widget_height*(1-placement_factor)) 647 self._scene_main_topleft.addItem(RulerItem(unit=unit, px_per_unit=px_per_unit, initial_pos_p1=pos_p1, initial_pos_p2=pos_p2, relative_origin_position=relative_origin_position)) 648 649 def on_changed_px_per_unit(self, unit, px_per_unit): 650 """Update the units and pixel-per-unit conversions of all rulers in main scene. 651 652 Args: 653 unit (str): The text for labeling units of ruler values. 654 px_per_unit (float): The conversion for pixels to units. For example, 10 means 10 pixels-per-unit, meaning the ruler value will show 1 when measuring 10 pixels. 655 """ 656 for item in self._scene_main_topleft.items(): 657 if isinstance(item, RulerItem): 658 if item.unit == unit: 659 item.set_and_refresh_px_per_unit(px_per_unit) 660 661 def on_right_click_save_all_comments(self): 662 """Open a dialog window for user to save all existing comments on the main scene to .csv. 663 664 Triggered from right-click menu on view. 665 """ 666 self.display_loading_grayout(True, "Selecting folder and name for saving all comments in current view...", pseudo_load_time=0) 667 668 style = [] 669 x = [] 670 y = [] 671 color = [] 672 string = [] 673 i = -1 674 for item in self._scene_main_topleft.items(): 675 676 if isinstance(item, CommentItem): 677 i += 1 678 679 comment_pos = item.get_scene_pos() 680 comment_color = item.get_color() 681 comment_string = item.get_comment_text_str() 682 683 style.append("plain text") 684 x.append(comment_pos.x()) 685 y.append(comment_pos.y()) 686 color.append(comment_color) 687 string.append(comment_string) 688 689 690 folderpath = None 691 filepath_mainview = self.currentFile 692 693 if filepath_mainview: 694 folderpath = filepath_mainview 695 folderpath = os.path.dirname(folderpath) 696 folderpath = folderpath + "\\" 697 else: 698 self.display_loading_grayout(False, pseudo_load_time=0) 699 return 700 701 header = [["Butterfly Viewer"], 702 ["1.0"], 703 ["comments"], 704 ["no details"], 705 ["origin"], 706 [os.path.basename(filepath_mainview)], 707 ["Style", "Image x", "Image y", "Appearance", "String"]] 708 709 date_and_time = datetime.now().strftime('%Y-%m-%d %H%M%S') # Sets the default filename with date and time 710 filename = "Untitled comments" + " - " + os.path.basename(filepath_mainview).split('.')[0] + " - " + date_and_time + ".csv" 711 name_filters = "CSV (*.csv)" # Allows users to select filetype of screenshot 712 713 filepath, _ = QtWidgets.QFileDialog.getSaveFileName(self, "Save all comments of current view to .csv", folderpath+filename, name_filters) 714 715 if filepath: 716 self.display_loading_grayout(True, "Saving all comments of current view to .csv...") 717 718 with open(filepath, "w", newline='') as csv_file: 719 csv_writer = csv.writer(csv_file, delimiter="|") 720 for row in header: 721 csv_writer.writerow(row) 722 for row in zip(style, x, y, color, string): 723 csv_writer.writerow(row) 724 725 self.display_loading_grayout(False) 726 727 728 def on_right_click_load_comments(self): 729 """Open a dialog window for user to load comments to the main scene via .csv as saved previously. 730 731 Triggered from right-click menu on view. 732 """ 733 self.display_loading_grayout(True, "Selecting comment file (.csv) to load into current view...", pseudo_load_time=0) 734 735 folderpath = None 736 filepath_mainview = self.currentFile 737 738 if filepath_mainview: 739 folderpath = filepath_mainview 740 folderpath = os.path.dirname(folderpath) 741 # folderpath = folderpath + "\\" 742 else: 743 self.display_loading_grayout(False, pseudo_load_time=0) 744 return 745 746 filename, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Select comment file (.csv) to load into current view", folderpath, "Comma-Separated Value File (*.csv)") 747 748 if filename: 749 self.display_loading_grayout(True, "Loading comments from selected .csv into current view...") 750 751 with open(filename, "r", newline='') as csv_file: 752 csv_reader = csv.reader(csv_file, delimiter="|") 753 csv_list = list(csv_reader) 754 755 i = None 756 757 try: 758 i = csv_list.index(["Style", "Image x", "Image y", "Appearance", "String"]) 759 except ValueError: 760 box_type = QtWidgets.QMessageBox.Warning 761 title = "Invalid .csv comment file" 762 text = "The selected .csv comment file does not have a format accepted by this app." 763 box_buttons = QtWidgets.QMessageBox.Close 764 box = QtWidgets.QMessageBox(box_type, title, text, box_buttons) 765 box.exec_() 766 else: 767 i += 1 # Move to first comment item 768 no_comments = True 769 for row in csv_list[i:]: 770 if row[0] == "plain text": 771 no_comments = False 772 comment_x = float(row[1]) 773 comment_y = float(row[2]) 774 comment_color = row[3] 775 comment_string = row[4] 776 comment_pos = QtCore.QPointF(comment_x, comment_y) 777 self._scene_main_topleft.addItem(CommentItem(initial_scene_pos=comment_pos, color=comment_color, comment_text=comment_string, set_cursor_on_creation=False)) 778 if no_comments: 779 box_type = QtWidgets.QMessageBox.Warning 780 title = "No comments in .csv" 781 text = "No comments found in the selected .csv comment file." 782 box_buttons = QtWidgets.QMessageBox.Close 783 box = QtWidgets.QMessageBox(box_type, title, text, box_buttons) 784 box.exec_() 785 786 self.display_loading_grayout(False) 787 788 def on_right_click_set_relative_origin_position(self, string): 789 """Set orientation of the coordinate system for rulers by positioning the relative origin. 790 791 Allows users to switch the coordinate orientation: 792 "bottomleft" for Cartesian-style (positive X right, positive Y up) 793 topleft" for image-style (positive X right, positive Y down) 794 795 Args: 796 relative_origin_position (str): The position of the origin for coordinate system ("topleft" or "bottomleft"). 797 """ 798 for item in self._scene_main_topleft.items(): 799 if isinstance(item, RulerItem): 800 item.set_and_refresh_relative_origin_position(string) 801 802 def display_loading_grayout(self, boolean, text="Loading...", pseudo_load_time=0.2): 803 """Emit signal for showing/hiding a grayout screen to indicate loading sequences. 804 805 Args: 806 boolean (bool): True to show grayout; False to hide. 807 text (str): The text to show on the grayout. 808 pseudo_load_time (float): The delay (in seconds) to hide the grayout to give user feeling of action. 809 """ 810 self.signal_display_loading_grayout.emit(boolean, text, pseudo_load_time) 811 812 def update_mouse_rect(self, pos = None): 813 """Update the position of red 1x1 outline at the pointer in the main scene. 814 815 Args: 816 pos (QPoint): The position of the mouse on the widget. Set to None to make the function determine the position using the mouse global coordinates. 817 """ 818 if not self.mouse_rect_scene_main_topleft: 819 return 820 821 if pos is None: # Get position of split on main scene by directly pulling mouse global coordinates 822 point_of_cursor_global = QtGui.QCursor.pos() 823 point_of_mouse_on_widget = self._view_main_topleft.mapFromGlobal(QtGui.QCursor.pos()) 824 else: 825 point_of_mouse_on_widget = pos 826 827 mouse_rect_pos_origin = self._view_main_topleft.mapToScene(point_of_mouse_on_widget.x(),point_of_mouse_on_widget.y()) 828 mouse_rect_pos_origin.setX(math.floor(mouse_rect_pos_origin.x() - self.mouse_rect_width + 1)) 829 mouse_rect_pos_origin.setY(math.floor(mouse_rect_pos_origin.y() - self.mouse_rect_height + 1)) 830 831 self.mouse_rect_scene_main_topleft.setPos(mouse_rect_pos_origin.x(), mouse_rect_pos_origin.y()) 832 833 # Signals 834 signal_display_loading_grayout = QtCore.pyqtSignal(bool, str, float) 835 """Emitted when comments are being saved or loaded.""" 836 837 became_closed = QtCore.pyqtSignal() 838 """Emitted when SplitView is closed.""" 839 840 was_clicked_close_pushbutton = QtCore.pyqtSignal() 841 """Emitted when close pushbutton is clicked (pressed+released).""" 842 843 was_set_global_transform_mode = QtCore.pyqtSignal(bool) 844 """Emitted when transform mode is set for all views in right-click menu (passes it along).""" 845 846 was_set_scene_background_color = QtCore.pyqtSignal(list) 847 """Emitted when background color is set in right-click menu (passes it along).""" 848 849 positionChanged = QtCore.pyqtSignal(QtCore.QPoint) 850 """Emitted when mouse changes position.""" 851 852 sceneChanged = QtCore.pyqtSignal('QList<QRectF>') 853 """Scene Changed **Signal**. 854 855 Emitted whenever the |QGraphicsScene| content changes.""" 856 857 transformChanged = QtCore.pyqtSignal() 858 """Transformed Changed **Signal**. 859 860 Emitted whenever the |QGraphicsView| Transform matrix has been changed.""" 861 862 scrollChanged = QtCore.pyqtSignal() 863 """Scroll Changed **Signal**. 864 865 Emitted whenever the scrollbar position or range has changed.""" 866 867 def connectSbarSignals(self, slot): 868 """Connect to scrollbar changed signals. 869 870 :param slot: slot to connect scrollbar signals to.""" 871 self._view_main_topleft.connectSbarSignals(slot) 872 873 def disconnectSbarSignals(self): 874 self._view_main_topleft.disconnectSbarSignals() 875 876 @property 877 def pixmap_main_topleft(self): 878 """The currently viewed |QPixmap| (*QPixmap*).""" 879 return self._pixmapItem_main_topleft.pixmap() 880 881 @pixmap_main_topleft.setter 882 def pixmap_main_topleft(self, pixmap_main_topleft): 883 if pixmap_main_topleft is not None: 884 self._pixmapItem_main_topleft.setPixmap(pixmap_main_topleft) 885 self._pixmapItem_main_topleft.setTransformationMode(QtCore.Qt.SmoothTransformation) 886 self._pixmap_base_original = pixmap_main_topleft 887 self.set_opacity_base(100) 888 889 @QtCore.pyqtSlot() 890 def set_opacity_base(self, percent): 891 """Set transparency of base image. 892 893 Args: 894 percent (float,int): The transparency as percent opacity, where 100 is opaque (not transparent) and 0 is transparent (0-100). 895 """ 896 897 self._opacity_base = percent 898 899 pixmap_to_be_transparent = QtGui.QPixmap(self._pixmap_base_original.size()) 900 pixmap_to_be_transparent.fill(QtCore.Qt.transparent) 901 painter = QtGui.QPainter(pixmap_to_be_transparent) 902 painter.setOpacity(percent/100) 903 painter.drawPixmap(QtCore.QPoint(), self._pixmap_base_original) 904 painter.end() 905 906 self._pixmapItem_main_topleft.setPixmap(pixmap_to_be_transparent) 907 908 @property 909 def pixmap_topright(self): 910 """The currently viewed QPixmap of the top-right of the split.""" 911 return self._pixmapItem_topright.pixmap() 912 913 @pixmap_topright.setter 914 def pixmap_topright(self, pixmap): 915 self._pixmap_topright_original = pixmap 916 self.set_opacity_topright(100) 917 918 @QtCore.pyqtSlot() 919 def set_opacity_topright(self, percent): 920 """Set transparency of top-right of sliding overlay. 921 922 Allows users to see base image underneath. 923 Provide enhanced integration and comparison of images (for example, blending raking light with color). 924 925 Args: 926 percent (float,int): The transparency as percent opacity, where 100 is opaque (not transparent) and 0 is transparent (0-100). 927 """ 928 if not self.pixmap_topright_exists: 929 self._opacity_topright = 100 930 return 931 932 self._opacity_topright = percent 933 934 pixmap_to_be_transparent = QtGui.QPixmap(self._pixmap_topright_original.size()) 935 pixmap_to_be_transparent.fill(QtCore.Qt.transparent) 936 painter = QtGui.QPainter(pixmap_to_be_transparent) 937 painter.setOpacity(percent/100) 938 painter.drawPixmap(QtCore.QPoint(), self._pixmap_topright_original) 939 painter.end() 940 941 self._pixmapItem_topright.setPixmap(pixmap_to_be_transparent) 942 943 944 @property 945 def pixmap_bottomright(self): 946 """The currently viewed QPixmap of the bottom-right of the split.""" 947 return self._pixmapItem_bottomright.pixmap() 948 949 @pixmap_bottomright.setter 950 def pixmap_bottomright(self, pixmap): 951 self._pixmap_bottomright_original = pixmap 952 self.set_opacity_bottomright(100) 953 954 @QtCore.pyqtSlot() 955 def set_opacity_bottomright(self, percent): 956 """Set transparency of bottom-right of sliding overlay. 957 958 Args: 959 percent (float,int): The transparency as percent opacity, where 100 is opaque (not transparent) and 0 is transparent (0-100). 960 """ 961 if not self.pixmap_bottomright_exists: 962 self._opacity_bottomright = 100 963 return 964 965 self._opacity_bottomright = percent 966 967 pixmap_to_be_transparent = QtGui.QPixmap(self._pixmap_bottomright_original.size()) 968 pixmap_to_be_transparent.fill(QtCore.Qt.transparent) 969 painter = QtGui.QPainter(pixmap_to_be_transparent) 970 painter.setOpacity(percent/100) 971 painter.drawPixmap(QtCore.QPoint(), self._pixmap_bottomright_original) 972 painter.end() 973 974 self._pixmapItem_bottomright.setPixmap(pixmap_to_be_transparent) 975 976 977 @property 978 def pixmap_bottomleft(self): 979 """The currently viewed QPixmap of the bottom-left of the split.""" 980 return self._pixmapItem_bottomleft.pixmap() 981 982 @pixmap_bottomleft.setter 983 def pixmap_bottomleft(self, pixmap): 984 self._pixmap_bottomleft_original = pixmap 985 self.set_opacity_bottomleft(100) 986 987 @QtCore.pyqtSlot() 988 def set_opacity_bottomleft(self, percent): 989 """Set transparency of bottom-left of sliding overlay. 990 991 Args: 992 percent (float,int): The transparency as percent opacity, where 100 is opaque (not transparent) and 0 is transparent (0-100). 993 """ 994 if not self.pixmap_bottomleft_exists: 995 self._opacity_bottomleft = 100 996 return 997 998 self._opacity_bottomleft = percent 999 1000 pixmap_to_be_transparent = QtGui.QPixmap(self._pixmap_bottomleft_original.size()) 1001 pixmap_to_be_transparent.fill(QtCore.Qt.transparent) 1002 painter = QtGui.QPainter(pixmap_to_be_transparent) 1003 painter.setOpacity(percent/100) 1004 painter.drawPixmap(QtCore.QPoint(), self._pixmap_bottomleft_original) 1005 painter.end() 1006 1007 self._pixmapItem_bottomleft.setPixmap(pixmap_to_be_transparent) 1008 1009 def moveEvent(self, event): 1010 """Override move event of frame.""" 1011 super().moveEvent(event) 1012 1013 def resizeEvent(self, event): 1014 """Override resize event of frame to ensure scene is also resized.""" 1015 self.resize_scene() 1016 super().resizeEvent(event) # Equivalent to QtWidgets.QFrame.resizeEvent(self, event) 1017 1018 def resize_scene(self): 1019 """Resize the scene to allow image to be panned just before the main pixmap leaves the viewport. 1020 1021 This is needed to expand the scene so that users can pan the pixmap such that its edges are at the center of the view. 1022 This changes the default behavior, which limits the scene to the bounds of the pixmap, thereby blocking users 1023 from panning outside the bounds of the pixmap, which can feel abrupt and restrictive. 1024 This takes care of preventing users from panning too far away from the pixmap. 1025 """ 1026 scene_to_viewport_factor = self._view_main_topleft.zoomFactor 1027 1028 width_viewport_window = self.width() 1029 height_viewport_window = self.height() 1030 1031 peek_precent = 0.1 # Percent of pixmap to be left "peeking" at bounds of pan 1032 peek_margin_x = width_viewport_window*peek_precent # [px] 1033 peek_margin_y = height_viewport_window*peek_precent 1034 1035 width_viewport = (width_viewport_window - peek_margin_x)/scene_to_viewport_factor # This is the size of the viewport on the screen 1036 height_viewport = (height_viewport_window - peek_margin_y)/scene_to_viewport_factor 1037 1038 width_pixmap = self._pixmapItem_main_topleft.pixmap().width() 1039 height_pixmap = self._pixmapItem_main_topleft.pixmap().height() 1040 1041 width_scene = 2.0*(width_viewport + width_pixmap/2.0) # The scene spans twice the viewport plus the pixmap 1042 height_scene = 2.0*(height_viewport + height_pixmap/2.0) 1043 1044 scene_rect = QtCore.QRectF(-width_scene/2.0 + width_pixmap/2.0,-height_scene/2.0 + height_pixmap/2.0,width_scene,height_scene) 1045 self._scene_main_topleft.setSceneRect(scene_rect) 1046 1047 def set_transform_mode_smooth_on(self): 1048 """Set transform mode to smooth (interpolate) when zoomfactor is >= 1.0.""" 1049 self.transform_mode_smooth = True 1050 self._scene_main_topleft.set_single_transform_mode_smooth(True) 1051 self.refresh_transform_mode() 1052 1053 def set_transform_mode_smooth_off(self): 1054 """Set transform mode to non-smooth (non-interpolated) when zoomfactor is >= 1.0.""" 1055 self.transform_mode_smooth = False 1056 self._scene_main_topleft.set_single_transform_mode_smooth(False) 1057 self.refresh_transform_mode() 1058 1059 def set_transform_mode_smooth(self, boolean): 1060 """Set transform mode when zoomfactor is >= 1.0. 1061 1062 Convenience function. 1063 1064 Args: 1065 boolean (bool): True to smooth (interpolate); False to fast (not interpolate). 1066 """ 1067 if boolean: 1068 self.set_transform_mode_smooth_on() 1069 elif not boolean: 1070 self.set_transform_mode_smooth_off() 1071 1072 @QtCore.pyqtSlot() 1073 def on_transformChanged(self): 1074 """Resize scene if image transform is changed (for example, when zoomed).""" 1075 self.resize_scene() 1076 self.update_split() 1077 1078 @property 1079 def viewName(self): 1080 """str: The name of the SplitView.""" 1081 return self._name 1082 1083 @viewName.setter 1084 def viewName(self, name): 1085 self._name = name 1086 1087 @property 1088 def handDragging(self): 1089 """bool: The hand dragging state.""" 1090 return self._view_main_topleft.handDragging 1091 1092 @property 1093 def scrollState(self): 1094 """tuple: The percentage of scene extents 1095 *(sceneWidthPercent, sceneHeightPercent)*""" 1096 return self._view_main_topleft.scrollState 1097 1098 @scrollState.setter 1099 def scrollState(self, state): 1100 self._view_main_topleft.scrollState = state 1101 1102 @property 1103 def zoomFactor(self): 1104 """float: The zoom scale factor.""" 1105 return self._view_main_topleft.zoomFactor 1106 1107 @zoomFactor.setter 1108 def zoomFactor(self, newZoomFactor): 1109 """Apply zoom to all views, taking into account the transform mode. 1110 1111 Args: 1112 newZoomFactor (float) 1113 """ 1114 if newZoomFactor < 1.0: 1115 self._pixmapItem_main_topleft.setTransformationMode(QtCore.Qt.SmoothTransformation) 1116 self._pixmapItem_topright.setTransformationMode(QtCore.Qt.SmoothTransformation) 1117 self._pixmapItem_bottomright.setTransformationMode(QtCore.Qt.SmoothTransformation) 1118 self._pixmapItem_bottomleft.setTransformationMode(QtCore.Qt.SmoothTransformation) 1119 elif self.transform_mode_smooth: 1120 self._pixmapItem_main_topleft.setTransformationMode(QtCore.Qt.SmoothTransformation) 1121 self._pixmapItem_topright.setTransformationMode(QtCore.Qt.SmoothTransformation) 1122 self._pixmapItem_bottomright.setTransformationMode(QtCore.Qt.SmoothTransformation) 1123 self._pixmapItem_bottomleft.setTransformationMode(QtCore.Qt.SmoothTransformation) 1124 else: 1125 self._pixmapItem_main_topleft.setTransformationMode(QtCore.Qt.FastTransformation) 1126 self._pixmapItem_topright.setTransformationMode(QtCore.Qt.FastTransformation) 1127 self._pixmapItem_bottomright.setTransformationMode(QtCore.Qt.FastTransformation) 1128 self._pixmapItem_bottomleft.setTransformationMode(QtCore.Qt.FastTransformation) 1129 1130 self._view_main_topleft.zoomFactor = newZoomFactor 1131 1132 newZoomFactor = newZoomFactor / self._view_topright.transform().m11() 1133 self._view_topright.scale(newZoomFactor, newZoomFactor) 1134 self._view_bottomright.scale(newZoomFactor, newZoomFactor) 1135 self._view_bottomleft.scale(newZoomFactor, newZoomFactor) 1136 1137 def refresh_transform_mode(self): 1138 """Refresh zoom of all views, taking into account the transform mode.""" 1139 self._view_main_topleft.zoomFactor 1140 if self.zoomFactor < 1.0: 1141 self._pixmapItem_main_topleft.setTransformationMode(QtCore.Qt.SmoothTransformation) 1142 self._pixmapItem_topright.setTransformationMode(QtCore.Qt.SmoothTransformation) 1143 self._pixmapItem_bottomright.setTransformationMode(QtCore.Qt.SmoothTransformation) 1144 self._pixmapItem_bottomleft.setTransformationMode(QtCore.Qt.SmoothTransformation) 1145 elif self.transform_mode_smooth: 1146 self._pixmapItem_main_topleft.setTransformationMode(QtCore.Qt.SmoothTransformation) 1147 self._pixmapItem_topright.setTransformationMode(QtCore.Qt.SmoothTransformation) 1148 self._pixmapItem_bottomright.setTransformationMode(QtCore.Qt.SmoothTransformation) 1149 self._pixmapItem_bottomleft.setTransformationMode(QtCore.Qt.SmoothTransformation) 1150 else: 1151 self._pixmapItem_main_topleft.setTransformationMode(QtCore.Qt.FastTransformation) 1152 self._pixmapItem_topright.setTransformationMode(QtCore.Qt.FastTransformation) 1153 self._pixmapItem_bottomright.setTransformationMode(QtCore.Qt.FastTransformation) 1154 self._pixmapItem_bottomleft.setTransformationMode(QtCore.Qt.FastTransformation) 1155 1156 @property 1157 def _horizontalScrollBar(self): 1158 """Get the SplitView horizontal scrollbar widget (*QScrollBar*). 1159 1160 (Only used for debugging purposes)""" 1161 return self._view_main_topleft.horizontalScrollBar() 1162 1163 @property 1164 def _verticalScrollBar(self): 1165 """Get the SplitView vertical scrollbar widget (*QScrollBar*). 1166 1167 (Only used for debugging purposes)""" 1168 return self._view_main_topleft.verticalScrollBar() 1169 1170 @property 1171 def _sceneRect(self): 1172 """Get the SplitView sceneRect (*QRectF*). 1173 1174 (Only used for debugging purposes)""" 1175 return self._view_main_topleft.sceneRect() 1176 1177 @QtCore.pyqtSlot() 1178 def scrollToTop(self): 1179 """Scroll to top of image.""" 1180 self._view_main_topleft.scrollToTop() 1181 1182 @QtCore.pyqtSlot() 1183 def scrollToBottom(self): 1184 """Scroll to bottom of image.""" 1185 self._view_main_topleft.scrollToBottom() 1186 1187 @QtCore.pyqtSlot() 1188 def scrollToBegin(self): 1189 """Scroll to left side of image.""" 1190 self._view_main_topleft.scrollToBegin() 1191 1192 @QtCore.pyqtSlot() 1193 def scrollToEnd(self): 1194 """Scroll to right side of image.""" 1195 self._view_main_topleft.scrollToEnd() 1196 1197 @QtCore.pyqtSlot() 1198 def centerView(self): 1199 """Center image in view.""" 1200 self._view_main_topleft.centerView() 1201 1202 @QtCore.pyqtSlot(bool) 1203 def enableScrollBars(self, enable): 1204 """Set visiblility of the view's scrollbars. 1205 1206 :param bool enable: True to enable the scrollbars """ 1207 self._view_main_topleft.enableScrollBars(enable) 1208 1209 @QtCore.pyqtSlot(bool) 1210 def enableHandDrag(self, enable): 1211 """Set whether dragging the view with the hand cursor is allowed. 1212 1213 :param bool enable: True to enable hand dragging """ 1214 self._view_main_topleft.enableHandDrag(enable) 1215 1216 @QtCore.pyqtSlot() 1217 def zoomIn(self): 1218 """Zoom in on image.""" 1219 self.scaleImage(self._zoomFactorDelta) 1220 1221 @QtCore.pyqtSlot() 1222 def zoomOut(self): 1223 """Zoom out on image.""" 1224 self.scaleImage(1 / self._zoomFactorDelta) 1225 1226 @QtCore.pyqtSlot() 1227 def actualSize(self): 1228 """Change zoom to show image at actual size. 1229 1230 (image pixel is equal to screen pixel)""" 1231 self.scaleImage(1.0, combine=False) 1232 1233 @QtCore.pyqtSlot() 1234 def fitToWindow(self): 1235 """Fit image within view. 1236 1237 If the viewport is wider than the main pixmap, then fit the pixmap to height; if the viewport is narrower, then fit the pixmap to width 1238 """ 1239 if not self._pixmapItem_main_topleft.pixmap(): 1240 return 1241 1242 padding_margin = 2 # Leaves visual gap between pixmap and border of viewport 1243 viewport_rect = self._view_main_topleft.viewport().rect().adjusted(padding_margin, padding_margin, 1244 -padding_margin, -padding_margin) 1245 aspect_ratio_viewport = viewport_rect.width()/viewport_rect.height() 1246 aspect_ratio_pixmap = self._pixmapItem_main_topleft.pixmap().width()/self._pixmapItem_main_topleft.pixmap().height() 1247 if aspect_ratio_viewport > aspect_ratio_pixmap: 1248 self.fitHeight() 1249 else: 1250 self.fitWidth() 1251 1252 self.transformChanged.emit() 1253 1254 @QtCore.pyqtSlot() 1255 def fitWidth(self): 1256 """Fit image width to view width.""" 1257 if not self._pixmapItem_main_topleft.pixmap(): 1258 return 1259 padding_margin = 2 1260 viewRect = self._view_main_topleft.viewport().rect().adjusted(padding_margin, padding_margin, 1261 -padding_margin, -padding_margin) 1262 factor = viewRect.width() / self._pixmapItem_main_topleft.pixmap().width() 1263 self.scaleImage(factor, combine=False) 1264 self._view_main_topleft.centerView() 1265 1266 @QtCore.pyqtSlot() 1267 def fitHeight(self): 1268 """Fit image height to view height.""" 1269 if not self._pixmapItem_main_topleft.pixmap(): 1270 return 1271 padding_margin = 2 1272 viewRect = self._view_main_topleft.viewport().rect().adjusted(padding_margin, padding_margin, 1273 -padding_margin, -padding_margin) 1274 factor = viewRect.height() / self._pixmapItem_main_topleft.pixmap().height() 1275 self.scaleImage(factor, combine=False) 1276 self._view_main_topleft.centerView() 1277 1278 def handleWheelNotches(self, notches): 1279 """Handle wheel notch event from underlying |QGraphicsView|. 1280 1281 :param float notches: Mouse wheel notches""" 1282 self.scaleImage(self._zoomFactorDelta ** notches) 1283 1284 def closeEvent(self, event): 1285 """Overriden in order to disconnect scrollbar signals before 1286 closing. 1287 1288 :param QEvent event: instance of a |QEvent| 1289 1290 If this isn't done Python crashes!""" 1291 #self.scrollChanged.disconnect() #doesn't prevent crash 1292 self.disconnectSbarSignals() 1293 1294 self._scene_main_topleft.deleteLater() 1295 self._view_main_topleft.deleteLater() 1296 del self._pixmap_base_original 1297 1298 self._scene_topright.deleteLater() 1299 self._view_topright.deleteLater() 1300 del self._pixmap_topright_original 1301 1302 self._scene_bottomright.deleteLater() 1303 self._view_bottomright.deleteLater() 1304 del self._pixmap_bottomright_original 1305 1306 self._scene_bottomleft.deleteLater() 1307 self._view_bottomleft.deleteLater() 1308 del self._pixmap_bottomleft_original 1309 1310 super().closeEvent(event) 1311 gc.collect() 1312 self.became_closed.emit() 1313 1314 def scaleImage(self, factor, combine=True): 1315 """Scale image by factor. 1316 1317 :param float factor: either new :attr:`zoomFactor` or amount to scale 1318 current :attr:`zoomFactor` 1319 1320 :param bool combine: if ``True`` scales the current 1321 :attr:`zoomFactor` by factor. Otherwise 1322 just sets :attr:`zoomFactor` to factor""" 1323 if not self._pixmapItem_main_topleft.pixmap(): 1324 return 1325 1326 if combine: 1327 self.zoomFactor = self.zoomFactor * factor 1328 else: 1329 self.zoomFactor = factor 1330 1331 self._view_main_topleft.checkTransformChanged() 1332 1333 def dumpTransform(self): 1334 """Dump view transform to stdout.""" 1335 self._view_main_topleft.dumpTransform(self._view_main_topleft.transform(), " "*4) 1336 1337 1338 def create_mouse_rect(self): 1339 """Create a red 1x1 outline at the pointer in the main scene. 1340 1341 Indicates to the user the size and position of the pixel over which the mouse is hovering. 1342 Helps to understand the position of individual pixels and their scale at the current zoom. 1343 """ 1344 1345 pen = QtGui.QPen() 1346 pen.setWidth(0.1) 1347 pen.setColor(QtCore.Qt.red) 1348 pen.setCapStyle(QtCore.Qt.SquareCap) 1349 pen.setJoinStyle(QtCore.Qt.MiterJoin) 1350 1351 brush = QtGui.QBrush() 1352 brush.setColor(QtCore.Qt.transparent) 1353 1354 self.mouse_rect_width = 1 1355 self.mouse_rect_height = 1 1356 1357 self.mouse_rect_topleft = QtCore.QPointF(0,0) 1358 self.mouse_rect_bottomright = QtCore.QPointF(self.mouse_rect_width-0.01, self.mouse_rect_height-0.01) 1359 self.mouse_rect = QtCore.QRectF(self.mouse_rect_topleft, self.mouse_rect_bottomright) 1360 1361 self.mouse_rect_scene_main_topleft = QtWidgets.QGraphicsRectItem(self.mouse_rect) # To add the same item in two scenes, you need to create two unique items 1362 1363 self.mouse_rect_scene_main_topleft.setPos(0,0) 1364 1365 self.mouse_rect_scene_main_topleft.setBrush(brush) 1366 self.mouse_rect_scene_main_topleft.setPen(pen) 1367 1368 self._scene_main_topleft.addItem(self.mouse_rect_scene_main_topleft) 1369 1370 def set_mouse_rect_visible(self, boolean): 1371 """Set the visibilty of the red 1x1 outline at the pointer in the main scene. 1372 1373 Args: 1374 boolean (bool): True to show 1x1 outline; False to hide. 1375 """ 1376 self.mouse_rect_scene_main_topleft.setVisible(boolean)
Image viewing widget for individual images and sliding overlays.
Creates an interface with a base image as a main image located at the top left
and optionally 3 other images (top-left, bottom-left, bottom-right) as a sliding overlay.
Supports zoom and pan.
Enables synchronized zoom and pan via signals.
Input images for a given sliding overlay must have identical resolutions to
function properly.
Arguments:
- pixmap (QPixmap): The main image to be viewed; the basis of the sliding overlay (main; topleft)
- filename_main_topleft (str): The image filepath of the main image.
- name (str): The name of the viewing widget.
- pixmap_topright (QPixmap): The top-right image of the sliding overlay (set None to exclude).
- pixmap_bottomleft (QPixmap): The bottom-left image of the sliding overlay (set None to exclude).
- pixmap_bottomright (QPixmap): The bottom-right image of the sliding overlay (set None to exclude).
425 def set_scene_background(self, brush): 426 """Set scene background color with QBrush. 427 428 Args: 429 brush (QBrush) 430 """ 431 if not self._scene_main_topleft: 432 return 433 self._scene_main_topleft.setBackgroundBrush(brush)
Set scene background color with QBrush.
Arguments:
- brush (QBrush)
435 def set_scene_background_color(self, color: list): 436 """Set scene background color with color list. 437 438 The init for CustomQGraphicsScene contains the ground truth for selectable background colors. 439 440 Args: 441 color (list): Descriptor string and RGB int values. Example: ["White", 255, 255, 255]. 442 """ 443 if not self._scene_main_topleft: 444 return 445 rgb = color[1:4] 446 rgb_clamp = [max(min(channel, 255), 0) for channel in rgb] 447 brush = QtGui.QColor(rgb_clamp[0], rgb_clamp[1], rgb_clamp[2]) 448 self.set_scene_background(brush) 449 self._scene_main_topleft.background_color = color 450 self.refresh_close_pushbutton_stylesheet()
Set scene background color with color list.
The init for CustomQGraphicsScene contains the ground truth for selectable background colors.
Arguments:
- color (list): Descriptor string and RGB int values. Example: ["White", 255, 255, 255].
453 def pixmap_none_ify(self, pixmap): 454 """Return None if pixmap has no pixels. 455 456 Args: 457 pixmap (QPixmap) 458 459 Returns: 460 None if pixmap has no pixels; same pixmap if it has pixels 461 """ 462 if pixmap: 463 if pixmap.width()==0 or pixmap.height==0: 464 return None 465 else: 466 return pixmap 467 else: 468 return None
Return None if pixmap has no pixels.
Arguments:
- pixmap (QPixmap)
Returns:
- None if pixmap has no pixels; same pixmap if it has pixels
470 @QtCore.pyqtSlot(QtCore.QPoint) 471 def on_positionChanged(self, pos): 472 """Update the position of the split and the 1x1 pixel rectangle. 473 474 Triggered when mouse is moved. 475 476 Args: 477 pos (QPoint): The position of the mouse relative to widget. 478 """ 479 point_of_mouse_on_widget = pos 480 481 self.update_split(point_of_mouse_on_widget) 482 self.update_mouse_rect(point_of_mouse_on_widget)
Update the position of the split and the 1x1 pixel rectangle.
Triggered when mouse is moved.
Arguments:
- pos (QPoint): The position of the mouse relative to widget.
484 def set_split(self, x_percent=0.5, y_percent=0.5, ignore_lock=False, percent_of_visible=False): 485 """Set the position of the split with x and y as proportion of base image's resolution. 486 487 Sets split position using a proportion of x and y (by default of entire main pixmap; can be set to proportion of visible pixmap). 488 Top left is x=0, y=0; bottom right is x=1, y=1. 489 This is needed to position the split without mouse movement from user (for example, to preview the effect of the transparency sliders in Butterfly Viewer) 490 491 Args: 492 x_percent (float): The position of the split as a proportion (0-1) of the base image's horizontal resolution. 493 y_percent (float): The position of the split as a proportion (0-1) of the base image's vertical resolution. 494 ignore_lock (bool): True to ignore the lock status of the split; False to adhere. 495 percent_of_visible (bool): True to set split as proportion of visible area; False as proportion of the full image resolution. 496 """ 497 if percent_of_visible: 498 x = x_percent*self.width() 499 y = y_percent*self.height() 500 point_of_split_on_widget = QtCore.QPoint(x, y) 501 else: 502 width_pixmap_main_topleft = self._pixmapItem_main_topleft.pixmap().width() 503 height_pixmap_main_topleft = self._pixmapItem_main_topleft.pixmap().height() 504 505 x = x_percent*width_pixmap_main_topleft 506 y = y_percent*height_pixmap_main_topleft 507 508 point_of_split_on_scene = QtCore.QPointF(x,y) 509 510 point_of_split_on_widget = self._view_main_topleft.mapFromScene(point_of_split_on_scene) 511 512 self.update_split(point_of_split_on_widget, ignore_lock=ignore_lock)
Set the position of the split with x and y as proportion of base image's resolution.
Sets split position using a proportion of x and y (by default of entire main pixmap; can be set to proportion of visible pixmap). Top left is x=0, y=0; bottom right is x=1, y=1. This is needed to position the split without mouse movement from user (for example, to preview the effect of the transparency sliders in Butterfly Viewer)
Arguments:
- x_percent (float): The position of the split as a proportion (0-1) of the base image's horizontal resolution.
- y_percent (float): The position of the split as a proportion (0-1) of the base image's vertical resolution.
- ignore_lock (bool): True to ignore the lock status of the split; False to adhere.
- percent_of_visible (bool): True to set split as proportion of visible area; False as proportion of the full image resolution.
514 def update_split(self, pos = None, pos_is_global=False, ignore_lock=False): 515 """Update the position of the split with mouse position. 516 517 Args: 518 pos (QPoint): Position of the mouse. 519 pos_is_global (bool): True if given mouse position is relative to MdiChild; False if global position. 520 ignore_lock (bool): True to ignore (bypass) the status of the split lock. 521 """ 522 if pos is None: # Get position of the split from the mouse's global coordinates (can be slow!) 523 point_of_cursor_global = QtGui.QCursor.pos() 524 point_of_mouse_on_widget = self._view_main_topleft.mapFromGlobal(point_of_cursor_global) 525 else: 526 if pos_is_global: 527 point_of_mouse_on_widget = self._view_main_topleft.mapFromGlobal(pos) 528 else: 529 point_of_mouse_on_widget = pos 530 531 point_of_mouse_on_widget.setX(point_of_mouse_on_widget.x()+1) # Offset +1 needed to have mouse cursor be hovering over the main scene (e.g., to allow manipulation of graphics item) 532 point_of_mouse_on_widget.setY(point_of_mouse_on_widget.y()+1) 533 534 self.last_updated_point_of_split_on_scene_main = self._view_main_topleft.mapToScene(point_of_mouse_on_widget.x(), point_of_mouse_on_widget.y()) 535 536 point_of_bottom_right_on_widget = QtCore.QPointF(self.width(), self.height()) 537 538 point_of_widget_origin_on_scene_main = self._view_main_topleft.mapToScene(0,0) 539 540 point_of_split_on_scene_main = self._view_main_topleft.mapToScene(max(point_of_mouse_on_widget.x(),0),max(point_of_mouse_on_widget.y(),0)) 541 point_of_bottom_right_on_scene_main = self._view_main_topleft.mapToScene(point_of_bottom_right_on_widget.x(), point_of_bottom_right_on_widget.y()) 542 543 self._view_layoutdriving_topleft.setMaximumWidth(max(point_of_mouse_on_widget.x(),0)) 544 self._view_layoutdriving_topleft.setMaximumHeight(max(point_of_mouse_on_widget.y(),0)) 545 546 render_buffer = 100 # Needed to prevent slight pixel offset of the sliding overlays when zoomed-out below ~0.5x 547 548 top_left_of_scene_topright = QtCore.QPointF(point_of_split_on_scene_main.x(), point_of_widget_origin_on_scene_main.y()) 549 bottom_right_of_scene_topright = QtCore.QPointF(point_of_bottom_right_on_scene_main.x() + render_buffer, point_of_split_on_scene_main.y() + render_buffer) 550 551 top_left_of_scene_bottomright = QtCore.QPointF(point_of_split_on_scene_main.x(), point_of_split_on_scene_main.y()) 552 bottom_right_of_scene_bottomright = QtCore.QPointF(point_of_bottom_right_on_scene_main.x() + render_buffer, point_of_bottom_right_on_scene_main.y() + render_buffer) 553 554 top_left_of_scene_bottomleft = QtCore.QPointF(point_of_widget_origin_on_scene_main.x(), point_of_split_on_scene_main.y()) 555 bottom_right_of_scene_bottomleft = QtCore.QPointF(point_of_split_on_scene_main.x() + render_buffer, point_of_bottom_right_on_scene_main.y() + render_buffer) 556 557 rect_of_scene_topright = QtCore.QRectF(top_left_of_scene_topright, bottom_right_of_scene_topright) 558 rect_of_scene_bottomright = QtCore.QRectF(top_left_of_scene_bottomright, bottom_right_of_scene_bottomright) 559 rect_of_scene_bottomleft = QtCore.QRectF(top_left_of_scene_bottomleft, bottom_right_of_scene_bottomleft) 560 561 self._view_topright.setSceneRect(rect_of_scene_topright) 562 self._view_bottomright.setSceneRect(rect_of_scene_bottomright) 563 self._view_bottomleft.setSceneRect(rect_of_scene_bottomleft) 564 565 self._view_topright.centerOn(top_left_of_scene_topright) 566 self._view_bottomright.centerOn(top_left_of_scene_bottomright) 567 self._view_bottomleft.centerOn(top_left_of_scene_bottomleft)
Update the position of the split with mouse position.
Arguments:
- pos (QPoint): Position of the mouse.
- pos_is_global (bool): True if given mouse position is relative to MdiChild; False if global position.
- ignore_lock (bool): True to ignore (bypass) the status of the split lock.
569 def refresh_split_based_on_last_updated_point_of_split_on_scene_main(self): 570 """Refresh the position of the split using the previously recorded split location. 571 572 This is needed to maintain the position of the split during synchronized zooming and panning. 573 """ 574 point_of_mouse_on_widget = self._view_main_topleft.mapFromScene(self.last_updated_point_of_split_on_scene_main) 575 576 point_of_bottom_right_on_widget = QtCore.QPointF(self.width(), self.height()) 577 578 point_of_widget_origin_on_scene_main = self._view_main_topleft.mapToScene(0,0) 579 point_of_split_on_scene_main = self._view_main_topleft.mapToScene(max(point_of_mouse_on_widget.x(),0),max(point_of_mouse_on_widget.y(),0)) 580 point_of_bottom_right_on_scene_main = self._view_main_topleft.mapToScene(point_of_bottom_right_on_widget.x(), point_of_bottom_right_on_widget.y()) 581 582 self._view_layoutdriving_topleft.setMaximumWidth(max(point_of_mouse_on_widget.x(),0)) 583 self._view_layoutdriving_topleft.setMaximumHeight(max(point_of_mouse_on_widget.y(),0)) 584 585 render_buffer = 100 # Needed to prevent slight pixel offset of the sliding overlays when zoomed-out below ~0.5x 586 587 top_left_of_scene_topright = QtCore.QPointF(point_of_split_on_scene_main.x(), point_of_widget_origin_on_scene_main.y()) 588 bottom_right_of_scene_topright = QtCore.QPointF(point_of_bottom_right_on_scene_main.x() + render_buffer, point_of_split_on_scene_main.y() + render_buffer) 589 590 top_left_of_scene_bottomright = QtCore.QPointF(point_of_split_on_scene_main.x(), point_of_split_on_scene_main.y()) 591 bottom_right_of_scene_bottomright = QtCore.QPointF(point_of_bottom_right_on_scene_main.x() + render_buffer, point_of_bottom_right_on_scene_main.y() + render_buffer) 592 593 top_left_of_scene_bottomleft = QtCore.QPointF(point_of_widget_origin_on_scene_main.x(), point_of_split_on_scene_main.y()) 594 bottom_right_of_scene_bottomleft = QtCore.QPointF(point_of_split_on_scene_main.x() + render_buffer, point_of_bottom_right_on_scene_main.y() + render_buffer) 595 596 rect_of_scene_topright = QtCore.QRectF(top_left_of_scene_topright, bottom_right_of_scene_topright) 597 rect_of_scene_bottomright = QtCore.QRectF(top_left_of_scene_bottomright, bottom_right_of_scene_bottomright) 598 rect_of_scene_bottomleft = QtCore.QRectF(top_left_of_scene_bottomleft, bottom_right_of_scene_bottomleft) 599 600 self._view_topright.setSceneRect(rect_of_scene_topright) 601 self._view_bottomright.setSceneRect(rect_of_scene_bottomright) 602 self._view_bottomleft.setSceneRect(rect_of_scene_bottomleft) 603 604 self._view_topright.centerOn(top_left_of_scene_topright) 605 self._view_bottomright.centerOn(top_left_of_scene_bottomright) 606 self._view_bottomleft.centerOn(top_left_of_scene_bottomleft)
Refresh the position of the split using the previously recorded split location.
This is needed to maintain the position of the split during synchronized zooming and panning.
608 def update_split_given_global(self, pos_global): 609 """Update the position of the split based on given global mouse position. 610 611 Convenience function. 612 613 Args: 614 pos_global (QPoint): The position of the mouse in global coordinates. 615 """ 616 self.update_split(pos = pos_global, pos_is_global=True)
Update the position of the split based on given global mouse position.
Convenience function.
Arguments:
- pos_global (QPoint): The position of the mouse in global coordinates.
619 def on_right_click_comment(self, scene_pos): 620 """Create an editable and movable comment on the scene of the main view at the given scene position. 621 622 Args: 623 pos (QPointF): position of the comment datum on the scene. 624 """ 625 pos_on_scene = scene_pos 626 self._scene_main_topleft.addItem(CommentItem(initial_scene_pos=pos_on_scene, set_cursor_on_creation=True))
Create an editable and movable comment on the scene of the main view at the given scene position.
Arguments:
- pos (QPointF): position of the comment datum on the scene.
628 def on_right_click_ruler(self, scene_pos, relative_origin_position, unit="px", px_per_unit=1.0, update_px_per_unit_on_existing=False): 629 """Create a movable ruler on the scene of the main view at the given scene position. 630 631 Args: 632 scene_pos (QPointF): The position of the ruler center on the scene. 633 relative_origin_position (str): The position of the origin for coordinate system ("topleft" or "bottomleft"). 634 unit (str): The text for labeling units of ruler values. 635 px_per_unit (float): The conversion for pixels to units. For example, 10 means 10 pixels-per-unit, meaning the ruler value will show 1 when measuring 10 pixels. 636 update_px_per_unit_on_existing (bool): False always. (Legacy from past versions; future work should remove. 637 """ 638 placement_factor = 1/3 639 px_per_unit = px_per_unit 640 relative_origin_position = relative_origin_position 641 642 widget_width = self.width() 643 widget_height = self.height() 644 645 pos_p1 = self._view_main_topleft.mapToScene(widget_width*placement_factor, widget_height*placement_factor) 646 pos_p2 = self._view_main_topleft.mapToScene(widget_width*(1-placement_factor), widget_height*(1-placement_factor)) 647 self._scene_main_topleft.addItem(RulerItem(unit=unit, px_per_unit=px_per_unit, initial_pos_p1=pos_p1, initial_pos_p2=pos_p2, relative_origin_position=relative_origin_position))
Create a movable ruler on the scene of the main view at the given scene position.
Arguments:
- scene_pos (QPointF): The position of the ruler center on the scene.
- relative_origin_position (str): The position of the origin for coordinate system ("topleft" or "bottomleft").
- unit (str): The text for labeling units of ruler values.
- px_per_unit (float): The conversion for pixels to units. For example, 10 means 10 pixels-per-unit, meaning the ruler value will show 1 when measuring 10 pixels.
- update_px_per_unit_on_existing (bool): False always. (Legacy from past versions; future work should remove.
649 def on_changed_px_per_unit(self, unit, px_per_unit): 650 """Update the units and pixel-per-unit conversions of all rulers in main scene. 651 652 Args: 653 unit (str): The text for labeling units of ruler values. 654 px_per_unit (float): The conversion for pixels to units. For example, 10 means 10 pixels-per-unit, meaning the ruler value will show 1 when measuring 10 pixels. 655 """ 656 for item in self._scene_main_topleft.items(): 657 if isinstance(item, RulerItem): 658 if item.unit == unit: 659 item.set_and_refresh_px_per_unit(px_per_unit)
Update the units and pixel-per-unit conversions of all rulers in main scene.
Arguments:
- unit (str): The text for labeling units of ruler values.
- px_per_unit (float): The conversion for pixels to units. For example, 10 means 10 pixels-per-unit, meaning the ruler value will show 1 when measuring 10 pixels.
661 def on_right_click_save_all_comments(self): 662 """Open a dialog window for user to save all existing comments on the main scene to .csv. 663 664 Triggered from right-click menu on view. 665 """ 666 self.display_loading_grayout(True, "Selecting folder and name for saving all comments in current view...", pseudo_load_time=0) 667 668 style = [] 669 x = [] 670 y = [] 671 color = [] 672 string = [] 673 i = -1 674 for item in self._scene_main_topleft.items(): 675 676 if isinstance(item, CommentItem): 677 i += 1 678 679 comment_pos = item.get_scene_pos() 680 comment_color = item.get_color() 681 comment_string = item.get_comment_text_str() 682 683 style.append("plain text") 684 x.append(comment_pos.x()) 685 y.append(comment_pos.y()) 686 color.append(comment_color) 687 string.append(comment_string) 688 689 690 folderpath = None 691 filepath_mainview = self.currentFile 692 693 if filepath_mainview: 694 folderpath = filepath_mainview 695 folderpath = os.path.dirname(folderpath) 696 folderpath = folderpath + "\\" 697 else: 698 self.display_loading_grayout(False, pseudo_load_time=0) 699 return 700 701 header = [["Butterfly Viewer"], 702 ["1.0"], 703 ["comments"], 704 ["no details"], 705 ["origin"], 706 [os.path.basename(filepath_mainview)], 707 ["Style", "Image x", "Image y", "Appearance", "String"]] 708 709 date_and_time = datetime.now().strftime('%Y-%m-%d %H%M%S') # Sets the default filename with date and time 710 filename = "Untitled comments" + " - " + os.path.basename(filepath_mainview).split('.')[0] + " - " + date_and_time + ".csv" 711 name_filters = "CSV (*.csv)" # Allows users to select filetype of screenshot 712 713 filepath, _ = QtWidgets.QFileDialog.getSaveFileName(self, "Save all comments of current view to .csv", folderpath+filename, name_filters) 714 715 if filepath: 716 self.display_loading_grayout(True, "Saving all comments of current view to .csv...") 717 718 with open(filepath, "w", newline='') as csv_file: 719 csv_writer = csv.writer(csv_file, delimiter="|") 720 for row in header: 721 csv_writer.writerow(row) 722 for row in zip(style, x, y, color, string): 723 csv_writer.writerow(row) 724 725 self.display_loading_grayout(False)
Open a dialog window for user to save all existing comments on the main scene to .csv.
Triggered from right-click menu on view.
728 def on_right_click_load_comments(self): 729 """Open a dialog window for user to load comments to the main scene via .csv as saved previously. 730 731 Triggered from right-click menu on view. 732 """ 733 self.display_loading_grayout(True, "Selecting comment file (.csv) to load into current view...", pseudo_load_time=0) 734 735 folderpath = None 736 filepath_mainview = self.currentFile 737 738 if filepath_mainview: 739 folderpath = filepath_mainview 740 folderpath = os.path.dirname(folderpath) 741 # folderpath = folderpath + "\\" 742 else: 743 self.display_loading_grayout(False, pseudo_load_time=0) 744 return 745 746 filename, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Select comment file (.csv) to load into current view", folderpath, "Comma-Separated Value File (*.csv)") 747 748 if filename: 749 self.display_loading_grayout(True, "Loading comments from selected .csv into current view...") 750 751 with open(filename, "r", newline='') as csv_file: 752 csv_reader = csv.reader(csv_file, delimiter="|") 753 csv_list = list(csv_reader) 754 755 i = None 756 757 try: 758 i = csv_list.index(["Style", "Image x", "Image y", "Appearance", "String"]) 759 except ValueError: 760 box_type = QtWidgets.QMessageBox.Warning 761 title = "Invalid .csv comment file" 762 text = "The selected .csv comment file does not have a format accepted by this app." 763 box_buttons = QtWidgets.QMessageBox.Close 764 box = QtWidgets.QMessageBox(box_type, title, text, box_buttons) 765 box.exec_() 766 else: 767 i += 1 # Move to first comment item 768 no_comments = True 769 for row in csv_list[i:]: 770 if row[0] == "plain text": 771 no_comments = False 772 comment_x = float(row[1]) 773 comment_y = float(row[2]) 774 comment_color = row[3] 775 comment_string = row[4] 776 comment_pos = QtCore.QPointF(comment_x, comment_y) 777 self._scene_main_topleft.addItem(CommentItem(initial_scene_pos=comment_pos, color=comment_color, comment_text=comment_string, set_cursor_on_creation=False)) 778 if no_comments: 779 box_type = QtWidgets.QMessageBox.Warning 780 title = "No comments in .csv" 781 text = "No comments found in the selected .csv comment file." 782 box_buttons = QtWidgets.QMessageBox.Close 783 box = QtWidgets.QMessageBox(box_type, title, text, box_buttons) 784 box.exec_() 785 786 self.display_loading_grayout(False)
Open a dialog window for user to load comments to the main scene via .csv as saved previously.
Triggered from right-click menu on view.
788 def on_right_click_set_relative_origin_position(self, string): 789 """Set orientation of the coordinate system for rulers by positioning the relative origin. 790 791 Allows users to switch the coordinate orientation: 792 "bottomleft" for Cartesian-style (positive X right, positive Y up) 793 topleft" for image-style (positive X right, positive Y down) 794 795 Args: 796 relative_origin_position (str): The position of the origin for coordinate system ("topleft" or "bottomleft"). 797 """ 798 for item in self._scene_main_topleft.items(): 799 if isinstance(item, RulerItem): 800 item.set_and_refresh_relative_origin_position(string)
Set orientation of the coordinate system for rulers by positioning the relative origin.
Allows users to switch the coordinate orientation:
"bottomleft" for Cartesian-style (positive X right, positive Y up) topleft" for image-style (positive X right, positive Y down)
Arguments:
- relative_origin_position (str): The position of the origin for coordinate system ("topleft" or "bottomleft").
802 def display_loading_grayout(self, boolean, text="Loading...", pseudo_load_time=0.2): 803 """Emit signal for showing/hiding a grayout screen to indicate loading sequences. 804 805 Args: 806 boolean (bool): True to show grayout; False to hide. 807 text (str): The text to show on the grayout. 808 pseudo_load_time (float): The delay (in seconds) to hide the grayout to give user feeling of action. 809 """ 810 self.signal_display_loading_grayout.emit(boolean, text, pseudo_load_time)
Emit signal for showing/hiding a grayout screen to indicate loading sequences.
Arguments:
- boolean (bool): True to show grayout; False to hide.
- text (str): The text to show on the grayout.
- pseudo_load_time (float): The delay (in seconds) to hide the grayout to give user feeling of action.
812 def update_mouse_rect(self, pos = None): 813 """Update the position of red 1x1 outline at the pointer in the main scene. 814 815 Args: 816 pos (QPoint): The position of the mouse on the widget. Set to None to make the function determine the position using the mouse global coordinates. 817 """ 818 if not self.mouse_rect_scene_main_topleft: 819 return 820 821 if pos is None: # Get position of split on main scene by directly pulling mouse global coordinates 822 point_of_cursor_global = QtGui.QCursor.pos() 823 point_of_mouse_on_widget = self._view_main_topleft.mapFromGlobal(QtGui.QCursor.pos()) 824 else: 825 point_of_mouse_on_widget = pos 826 827 mouse_rect_pos_origin = self._view_main_topleft.mapToScene(point_of_mouse_on_widget.x(),point_of_mouse_on_widget.y()) 828 mouse_rect_pos_origin.setX(math.floor(mouse_rect_pos_origin.x() - self.mouse_rect_width + 1)) 829 mouse_rect_pos_origin.setY(math.floor(mouse_rect_pos_origin.y() - self.mouse_rect_height + 1)) 830 831 self.mouse_rect_scene_main_topleft.setPos(mouse_rect_pos_origin.x(), mouse_rect_pos_origin.y())
Update the position of red 1x1 outline at the pointer in the main scene.
Arguments:
- pos (QPoint): The position of the mouse on the widget. Set to None to make the function determine the position using the mouse global coordinates.
Emitted when transform mode is set for all views in right-click menu (passes it along).
Emitted when background color is set in right-click menu (passes it along).
Scene Changed Signal.
Emitted whenever the |QGraphicsScene| content changes.
Transformed Changed Signal.
Emitted whenever the |QGraphicsView| Transform matrix has been changed.
Scroll Changed Signal.
Emitted whenever the scrollbar position or range has changed.
867 def connectSbarSignals(self, slot): 868 """Connect to scrollbar changed signals. 869 870 :param slot: slot to connect scrollbar signals to.""" 871 self._view_main_topleft.connectSbarSignals(slot)
Connect to scrollbar changed signals.
Parameters
- slot: slot to connect scrollbar signals to.
889 @QtCore.pyqtSlot() 890 def set_opacity_base(self, percent): 891 """Set transparency of base image. 892 893 Args: 894 percent (float,int): The transparency as percent opacity, where 100 is opaque (not transparent) and 0 is transparent (0-100). 895 """ 896 897 self._opacity_base = percent 898 899 pixmap_to_be_transparent = QtGui.QPixmap(self._pixmap_base_original.size()) 900 pixmap_to_be_transparent.fill(QtCore.Qt.transparent) 901 painter = QtGui.QPainter(pixmap_to_be_transparent) 902 painter.setOpacity(percent/100) 903 painter.drawPixmap(QtCore.QPoint(), self._pixmap_base_original) 904 painter.end() 905 906 self._pixmapItem_main_topleft.setPixmap(pixmap_to_be_transparent)
Set transparency of base image.
Arguments:
- percent (float,int): The transparency as percent opacity, where 100 is opaque (not transparent) and 0 is transparent (0-100).
918 @QtCore.pyqtSlot() 919 def set_opacity_topright(self, percent): 920 """Set transparency of top-right of sliding overlay. 921 922 Allows users to see base image underneath. 923 Provide enhanced integration and comparison of images (for example, blending raking light with color). 924 925 Args: 926 percent (float,int): The transparency as percent opacity, where 100 is opaque (not transparent) and 0 is transparent (0-100). 927 """ 928 if not self.pixmap_topright_exists: 929 self._opacity_topright = 100 930 return 931 932 self._opacity_topright = percent 933 934 pixmap_to_be_transparent = QtGui.QPixmap(self._pixmap_topright_original.size()) 935 pixmap_to_be_transparent.fill(QtCore.Qt.transparent) 936 painter = QtGui.QPainter(pixmap_to_be_transparent) 937 painter.setOpacity(percent/100) 938 painter.drawPixmap(QtCore.QPoint(), self._pixmap_topright_original) 939 painter.end() 940 941 self._pixmapItem_topright.setPixmap(pixmap_to_be_transparent)
Set transparency of top-right of sliding overlay.
Allows users to see base image underneath. Provide enhanced integration and comparison of images (for example, blending raking light with color).
Arguments:
- percent (float,int): The transparency as percent opacity, where 100 is opaque (not transparent) and 0 is transparent (0-100).
954 @QtCore.pyqtSlot() 955 def set_opacity_bottomright(self, percent): 956 """Set transparency of bottom-right of sliding overlay. 957 958 Args: 959 percent (float,int): The transparency as percent opacity, where 100 is opaque (not transparent) and 0 is transparent (0-100). 960 """ 961 if not self.pixmap_bottomright_exists: 962 self._opacity_bottomright = 100 963 return 964 965 self._opacity_bottomright = percent 966 967 pixmap_to_be_transparent = QtGui.QPixmap(self._pixmap_bottomright_original.size()) 968 pixmap_to_be_transparent.fill(QtCore.Qt.transparent) 969 painter = QtGui.QPainter(pixmap_to_be_transparent) 970 painter.setOpacity(percent/100) 971 painter.drawPixmap(QtCore.QPoint(), self._pixmap_bottomright_original) 972 painter.end() 973 974 self._pixmapItem_bottomright.setPixmap(pixmap_to_be_transparent)
Set transparency of bottom-right of sliding overlay.
Arguments:
- percent (float,int): The transparency as percent opacity, where 100 is opaque (not transparent) and 0 is transparent (0-100).
987 @QtCore.pyqtSlot() 988 def set_opacity_bottomleft(self, percent): 989 """Set transparency of bottom-left of sliding overlay. 990 991 Args: 992 percent (float,int): The transparency as percent opacity, where 100 is opaque (not transparent) and 0 is transparent (0-100). 993 """ 994 if not self.pixmap_bottomleft_exists: 995 self._opacity_bottomleft = 100 996 return 997 998 self._opacity_bottomleft = percent 999 1000 pixmap_to_be_transparent = QtGui.QPixmap(self._pixmap_bottomleft_original.size()) 1001 pixmap_to_be_transparent.fill(QtCore.Qt.transparent) 1002 painter = QtGui.QPainter(pixmap_to_be_transparent) 1003 painter.setOpacity(percent/100) 1004 painter.drawPixmap(QtCore.QPoint(), self._pixmap_bottomleft_original) 1005 painter.end() 1006 1007 self._pixmapItem_bottomleft.setPixmap(pixmap_to_be_transparent)
Set transparency of bottom-left of sliding overlay.
Arguments:
- percent (float,int): The transparency as percent opacity, where 100 is opaque (not transparent) and 0 is transparent (0-100).
1009 def moveEvent(self, event): 1010 """Override move event of frame.""" 1011 super().moveEvent(event)
Override move event of frame.
1013 def resizeEvent(self, event): 1014 """Override resize event of frame to ensure scene is also resized.""" 1015 self.resize_scene() 1016 super().resizeEvent(event) # Equivalent to QtWidgets.QFrame.resizeEvent(self, event)
Override resize event of frame to ensure scene is also resized.
1018 def resize_scene(self): 1019 """Resize the scene to allow image to be panned just before the main pixmap leaves the viewport. 1020 1021 This is needed to expand the scene so that users can pan the pixmap such that its edges are at the center of the view. 1022 This changes the default behavior, which limits the scene to the bounds of the pixmap, thereby blocking users 1023 from panning outside the bounds of the pixmap, which can feel abrupt and restrictive. 1024 This takes care of preventing users from panning too far away from the pixmap. 1025 """ 1026 scene_to_viewport_factor = self._view_main_topleft.zoomFactor 1027 1028 width_viewport_window = self.width() 1029 height_viewport_window = self.height() 1030 1031 peek_precent = 0.1 # Percent of pixmap to be left "peeking" at bounds of pan 1032 peek_margin_x = width_viewport_window*peek_precent # [px] 1033 peek_margin_y = height_viewport_window*peek_precent 1034 1035 width_viewport = (width_viewport_window - peek_margin_x)/scene_to_viewport_factor # This is the size of the viewport on the screen 1036 height_viewport = (height_viewport_window - peek_margin_y)/scene_to_viewport_factor 1037 1038 width_pixmap = self._pixmapItem_main_topleft.pixmap().width() 1039 height_pixmap = self._pixmapItem_main_topleft.pixmap().height() 1040 1041 width_scene = 2.0*(width_viewport + width_pixmap/2.0) # The scene spans twice the viewport plus the pixmap 1042 height_scene = 2.0*(height_viewport + height_pixmap/2.0) 1043 1044 scene_rect = QtCore.QRectF(-width_scene/2.0 + width_pixmap/2.0,-height_scene/2.0 + height_pixmap/2.0,width_scene,height_scene) 1045 self._scene_main_topleft.setSceneRect(scene_rect)
Resize the scene to allow image to be panned just before the main pixmap leaves the viewport.
This is needed to expand the scene so that users can pan the pixmap such that its edges are at the center of the view. This changes the default behavior, which limits the scene to the bounds of the pixmap, thereby blocking users from panning outside the bounds of the pixmap, which can feel abrupt and restrictive. This takes care of preventing users from panning too far away from the pixmap.
1047 def set_transform_mode_smooth_on(self): 1048 """Set transform mode to smooth (interpolate) when zoomfactor is >= 1.0.""" 1049 self.transform_mode_smooth = True 1050 self._scene_main_topleft.set_single_transform_mode_smooth(True) 1051 self.refresh_transform_mode()
Set transform mode to smooth (interpolate) when zoomfactor is >= 1.0.
1053 def set_transform_mode_smooth_off(self): 1054 """Set transform mode to non-smooth (non-interpolated) when zoomfactor is >= 1.0.""" 1055 self.transform_mode_smooth = False 1056 self._scene_main_topleft.set_single_transform_mode_smooth(False) 1057 self.refresh_transform_mode()
Set transform mode to non-smooth (non-interpolated) when zoomfactor is >= 1.0.
1059 def set_transform_mode_smooth(self, boolean): 1060 """Set transform mode when zoomfactor is >= 1.0. 1061 1062 Convenience function. 1063 1064 Args: 1065 boolean (bool): True to smooth (interpolate); False to fast (not interpolate). 1066 """ 1067 if boolean: 1068 self.set_transform_mode_smooth_on() 1069 elif not boolean: 1070 self.set_transform_mode_smooth_off()
Set transform mode when zoomfactor is >= 1.0.
Convenience function.
Arguments:
- boolean (bool): True to smooth (interpolate); False to fast (not interpolate).
1072 @QtCore.pyqtSlot() 1073 def on_transformChanged(self): 1074 """Resize scene if image transform is changed (for example, when zoomed).""" 1075 self.resize_scene() 1076 self.update_split()
Resize scene if image transform is changed (for example, when zoomed).
1137 def refresh_transform_mode(self): 1138 """Refresh zoom of all views, taking into account the transform mode.""" 1139 self._view_main_topleft.zoomFactor 1140 if self.zoomFactor < 1.0: 1141 self._pixmapItem_main_topleft.setTransformationMode(QtCore.Qt.SmoothTransformation) 1142 self._pixmapItem_topright.setTransformationMode(QtCore.Qt.SmoothTransformation) 1143 self._pixmapItem_bottomright.setTransformationMode(QtCore.Qt.SmoothTransformation) 1144 self._pixmapItem_bottomleft.setTransformationMode(QtCore.Qt.SmoothTransformation) 1145 elif self.transform_mode_smooth: 1146 self._pixmapItem_main_topleft.setTransformationMode(QtCore.Qt.SmoothTransformation) 1147 self._pixmapItem_topright.setTransformationMode(QtCore.Qt.SmoothTransformation) 1148 self._pixmapItem_bottomright.setTransformationMode(QtCore.Qt.SmoothTransformation) 1149 self._pixmapItem_bottomleft.setTransformationMode(QtCore.Qt.SmoothTransformation) 1150 else: 1151 self._pixmapItem_main_topleft.setTransformationMode(QtCore.Qt.FastTransformation) 1152 self._pixmapItem_topright.setTransformationMode(QtCore.Qt.FastTransformation) 1153 self._pixmapItem_bottomright.setTransformationMode(QtCore.Qt.FastTransformation) 1154 self._pixmapItem_bottomleft.setTransformationMode(QtCore.Qt.FastTransformation)
Refresh zoom of all views, taking into account the transform mode.
1177 @QtCore.pyqtSlot() 1178 def scrollToTop(self): 1179 """Scroll to top of image.""" 1180 self._view_main_topleft.scrollToTop()
Scroll to top of image.
1182 @QtCore.pyqtSlot() 1183 def scrollToBottom(self): 1184 """Scroll to bottom of image.""" 1185 self._view_main_topleft.scrollToBottom()
Scroll to bottom of image.
1187 @QtCore.pyqtSlot() 1188 def scrollToBegin(self): 1189 """Scroll to left side of image.""" 1190 self._view_main_topleft.scrollToBegin()
Scroll to left side of image.
1192 @QtCore.pyqtSlot() 1193 def scrollToEnd(self): 1194 """Scroll to right side of image.""" 1195 self._view_main_topleft.scrollToEnd()
Scroll to right side of image.
1197 @QtCore.pyqtSlot() 1198 def centerView(self): 1199 """Center image in view.""" 1200 self._view_main_topleft.centerView()
Center image in view.
1202 @QtCore.pyqtSlot(bool) 1203 def enableScrollBars(self, enable): 1204 """Set visiblility of the view's scrollbars. 1205 1206 :param bool enable: True to enable the scrollbars """ 1207 self._view_main_topleft.enableScrollBars(enable)
Set visiblility of the view's scrollbars.
Parameters
- bool enable: True to enable the scrollbars
1209 @QtCore.pyqtSlot(bool) 1210 def enableHandDrag(self, enable): 1211 """Set whether dragging the view with the hand cursor is allowed. 1212 1213 :param bool enable: True to enable hand dragging """ 1214 self._view_main_topleft.enableHandDrag(enable)
Set whether dragging the view with the hand cursor is allowed.
Parameters
- bool enable: True to enable hand dragging
1216 @QtCore.pyqtSlot() 1217 def zoomIn(self): 1218 """Zoom in on image.""" 1219 self.scaleImage(self._zoomFactorDelta)
Zoom in on image.
1221 @QtCore.pyqtSlot() 1222 def zoomOut(self): 1223 """Zoom out on image.""" 1224 self.scaleImage(1 / self._zoomFactorDelta)
Zoom out on image.
1226 @QtCore.pyqtSlot() 1227 def actualSize(self): 1228 """Change zoom to show image at actual size. 1229 1230 (image pixel is equal to screen pixel)""" 1231 self.scaleImage(1.0, combine=False)
Change zoom to show image at actual size.
(image pixel is equal to screen pixel)
1233 @QtCore.pyqtSlot() 1234 def fitToWindow(self): 1235 """Fit image within view. 1236 1237 If the viewport is wider than the main pixmap, then fit the pixmap to height; if the viewport is narrower, then fit the pixmap to width 1238 """ 1239 if not self._pixmapItem_main_topleft.pixmap(): 1240 return 1241 1242 padding_margin = 2 # Leaves visual gap between pixmap and border of viewport 1243 viewport_rect = self._view_main_topleft.viewport().rect().adjusted(padding_margin, padding_margin, 1244 -padding_margin, -padding_margin) 1245 aspect_ratio_viewport = viewport_rect.width()/viewport_rect.height() 1246 aspect_ratio_pixmap = self._pixmapItem_main_topleft.pixmap().width()/self._pixmapItem_main_topleft.pixmap().height() 1247 if aspect_ratio_viewport > aspect_ratio_pixmap: 1248 self.fitHeight() 1249 else: 1250 self.fitWidth() 1251 1252 self.transformChanged.emit()
Fit image within view.
If the viewport is wider than the main pixmap, then fit the pixmap to height; if the viewport is narrower, then fit the pixmap to width
1254 @QtCore.pyqtSlot() 1255 def fitWidth(self): 1256 """Fit image width to view width.""" 1257 if not self._pixmapItem_main_topleft.pixmap(): 1258 return 1259 padding_margin = 2 1260 viewRect = self._view_main_topleft.viewport().rect().adjusted(padding_margin, padding_margin, 1261 -padding_margin, -padding_margin) 1262 factor = viewRect.width() / self._pixmapItem_main_topleft.pixmap().width() 1263 self.scaleImage(factor, combine=False) 1264 self._view_main_topleft.centerView()
Fit image width to view width.
1266 @QtCore.pyqtSlot() 1267 def fitHeight(self): 1268 """Fit image height to view height.""" 1269 if not self._pixmapItem_main_topleft.pixmap(): 1270 return 1271 padding_margin = 2 1272 viewRect = self._view_main_topleft.viewport().rect().adjusted(padding_margin, padding_margin, 1273 -padding_margin, -padding_margin) 1274 factor = viewRect.height() / self._pixmapItem_main_topleft.pixmap().height() 1275 self.scaleImage(factor, combine=False) 1276 self._view_main_topleft.centerView()
Fit image height to view height.
1278 def handleWheelNotches(self, notches): 1279 """Handle wheel notch event from underlying |QGraphicsView|. 1280 1281 :param float notches: Mouse wheel notches""" 1282 self.scaleImage(self._zoomFactorDelta ** notches)
Handle wheel notch event from underlying |QGraphicsView|.
Parameters
- float notches: Mouse wheel notches
1284 def closeEvent(self, event): 1285 """Overriden in order to disconnect scrollbar signals before 1286 closing. 1287 1288 :param QEvent event: instance of a |QEvent| 1289 1290 If this isn't done Python crashes!""" 1291 #self.scrollChanged.disconnect() #doesn't prevent crash 1292 self.disconnectSbarSignals() 1293 1294 self._scene_main_topleft.deleteLater() 1295 self._view_main_topleft.deleteLater() 1296 del self._pixmap_base_original 1297 1298 self._scene_topright.deleteLater() 1299 self._view_topright.deleteLater() 1300 del self._pixmap_topright_original 1301 1302 self._scene_bottomright.deleteLater() 1303 self._view_bottomright.deleteLater() 1304 del self._pixmap_bottomright_original 1305 1306 self._scene_bottomleft.deleteLater() 1307 self._view_bottomleft.deleteLater() 1308 del self._pixmap_bottomleft_original 1309 1310 super().closeEvent(event) 1311 gc.collect() 1312 self.became_closed.emit()
Overriden in order to disconnect scrollbar signals before closing.
Parameters
- QEvent event: instance of a |QEvent|
If this isn't done Python crashes!
1314 def scaleImage(self, factor, combine=True): 1315 """Scale image by factor. 1316 1317 :param float factor: either new :attr:`zoomFactor` or amount to scale 1318 current :attr:`zoomFactor` 1319 1320 :param bool combine: if ``True`` scales the current 1321 :attr:`zoomFactor` by factor. Otherwise 1322 just sets :attr:`zoomFactor` to factor""" 1323 if not self._pixmapItem_main_topleft.pixmap(): 1324 return 1325 1326 if combine: 1327 self.zoomFactor = self.zoomFactor * factor 1328 else: 1329 self.zoomFactor = factor 1330 1331 self._view_main_topleft.checkTransformChanged()
Scale image by factor.
Parameters
float factor: either new
zoomFactor
or amount to scale currentzoomFactor
bool combine: if
True
scales the currentzoomFactor
by factor. Otherwise just setszoomFactor
to factor
1333 def dumpTransform(self): 1334 """Dump view transform to stdout.""" 1335 self._view_main_topleft.dumpTransform(self._view_main_topleft.transform(), " "*4)
Dump view transform to stdout.
1338 def create_mouse_rect(self): 1339 """Create a red 1x1 outline at the pointer in the main scene. 1340 1341 Indicates to the user the size and position of the pixel over which the mouse is hovering. 1342 Helps to understand the position of individual pixels and their scale at the current zoom. 1343 """ 1344 1345 pen = QtGui.QPen() 1346 pen.setWidth(0.1) 1347 pen.setColor(QtCore.Qt.red) 1348 pen.setCapStyle(QtCore.Qt.SquareCap) 1349 pen.setJoinStyle(QtCore.Qt.MiterJoin) 1350 1351 brush = QtGui.QBrush() 1352 brush.setColor(QtCore.Qt.transparent) 1353 1354 self.mouse_rect_width = 1 1355 self.mouse_rect_height = 1 1356 1357 self.mouse_rect_topleft = QtCore.QPointF(0,0) 1358 self.mouse_rect_bottomright = QtCore.QPointF(self.mouse_rect_width-0.01, self.mouse_rect_height-0.01) 1359 self.mouse_rect = QtCore.QRectF(self.mouse_rect_topleft, self.mouse_rect_bottomright) 1360 1361 self.mouse_rect_scene_main_topleft = QtWidgets.QGraphicsRectItem(self.mouse_rect) # To add the same item in two scenes, you need to create two unique items 1362 1363 self.mouse_rect_scene_main_topleft.setPos(0,0) 1364 1365 self.mouse_rect_scene_main_topleft.setBrush(brush) 1366 self.mouse_rect_scene_main_topleft.setPen(pen) 1367 1368 self._scene_main_topleft.addItem(self.mouse_rect_scene_main_topleft)
Create a red 1x1 outline at the pointer in the main scene.
Indicates to the user the size and position of the pixel over which the mouse is hovering. Helps to understand the position of individual pixels and their scale at the current zoom.
1370 def set_mouse_rect_visible(self, boolean): 1371 """Set the visibilty of the red 1x1 outline at the pointer in the main scene. 1372 1373 Args: 1374 boolean (bool): True to show 1x1 outline; False to hide. 1375 """ 1376 self.mouse_rect_scene_main_topleft.setVisible(boolean)
Set the visibilty of the red 1x1 outline at the pointer in the main scene.
Arguments:
- boolean (bool): True to show 1x1 outline; False to hide.