butterfly_viewer.butterfly_viewer
Multi-image viewer for comparing images with synchronized zooming, panning, and sliding overlays.
Intended to be run as a script:
$ python butterfly_viewer.py
Features:
Image windows have synchronized zoom and pan by default, but can be optionally unsynced. Image windows will auto-arrange and can be set as a grid, column, or row. Users can create sliding overlays up to 2x2 and adjust their transparencies.
Credits:
PyQt MDI Image Viewer by tpgit (http://tpgit.github.io/MDIImageViewer/) for sync pan and zoom.
1#!/usr/bin/env python3 2 3"""Multi-image viewer for comparing images with synchronized zooming, panning, and sliding overlays. 4 5Intended to be run as a script: 6 $ python butterfly_viewer.py 7 8Features: 9 Image windows have synchronized zoom and pan by default, but can be optionally unsynced. 10 Image windows will auto-arrange and can be set as a grid, column, or row. 11 Users can create sliding overlays up to 2x2 and adjust their transparencies. 12 13Credits: 14 PyQt MDI Image Viewer by tpgit (http://tpgit.github.io/MDIImageViewer/) for sync pan and zoom. 15""" 16# SPDX-License-Identifier: GPL-3.0-or-later 17 18 19 20import sip 21import time 22import os 23from datetime import datetime 24 25from PyQt5 import QtCore, QtGui, QtWidgets 26 27from aux_splitview import SplitView 28from aux_functions import strippedName, toBool 29from aux_trackers import EventTrackerSplitBypassInterface 30from aux_interfaces import SplitViewCreator, SlidersOpacitySplitViews, SplitViewManager 31from aux_mdi import QMdiAreaWithCustomSignals 32from aux_layouts import GridLayoutFloatingShadow 33from aux_exif import get_exif_rotation_angle 34import icons_rc 35 36 37 38os.environ["QT_ENABLE_HIGHDPI_SCALING"] = "1" 39os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1" 40os.environ["QT_SCALE_FACTOR"] = "1" 41 42sip.setapi('QDate', 2) 43sip.setapi('QTime', 2) 44sip.setapi('QDateTime', 2) 45sip.setapi('QUrl', 2) 46sip.setapi('QTextStream', 2) 47sip.setapi('QVariant', 2) 48sip.setapi('QString', 2) 49 50__version__ = "1.0.3" 51COMPANY = "Butterfly Apps" 52DOMAIN = "https://github.com/olive-groves/butterfly_viewer/" 53APPNAME = "Butterfly Viewer" 54 55SETTING_RECENTFILELIST = "recentfilelist" 56SETTING_FILEOPEN = "fileOpenDialog" 57SETTING_SCROLLBARS = "scrollbars" 58SETTING_STATUSBAR = "statusbar" 59SETTING_SYNCHZOOM = "synchzoom" 60SETTING_SYNCHPAN = "synchpan" 61 62 63 64class SplitViewMdiChild(SplitView): 65 """Extends SplitView for use in Butterfly Viewer. 66 67 Extends SplitView with keyboard shortcut to lock the position of the split 68 in the Butterfly Viewer. 69 70 Overrides SplitView by checking split lock status before updating split. 71 72 Args: 73 See parent method for full documentation. 74 """ 75 76 shortcut_shift_x_was_activated = QtCore.pyqtSignal() 77 78 def __init__(self, pixmap, filename_main_topleft, name, pixmap_topright, pixmap_bottomleft, pixmap_bottomright, transform_mode_smooth): 79 super().__init__(pixmap, filename_main_topleft, name, pixmap_topright, pixmap_bottomleft, pixmap_bottomright, transform_mode_smooth) 80 81 self.setAttribute(QtCore.Qt.WA_DeleteOnClose) 82 self._isUntitled = True 83 84 self.toggle_lock_split_shortcut = QtWidgets.QShortcut(QtGui.QKeySequence("Shift+X"), self) 85 self.toggle_lock_split_shortcut.activated.connect(self.toggle_lock_split) 86 87 88 # Control the split of the sliding overlay 89 90 def toggle_lock_split(self): 91 """Toggle the split lock. 92 93 Toggles the status of the split lock (e.g., if locked, it will become unlocked; vice versa). 94 """ 95 self.split_locked = not self.split_locked 96 self.shortcut_shift_x_was_activated.emit() 97 98 def update_split(self, pos = None, pos_is_global=False, ignore_lock=False): 99 """Update the position of the split while considering the status of the split lock. 100 101 See parent method for full documentation. 102 """ 103 if not self.split_locked or ignore_lock: 104 super().update_split(pos,pos_is_global,ignore_lock=ignore_lock) 105 106 107 # Events 108 109 def enterEvent(self, event): 110 """Pass along enter event to parent method.""" 111 super().enterEvent(event) 112 113 114 115class MultiViewMainWindow(QtWidgets.QMainWindow): 116 """View multiple images with split-effect and synchronized panning and zooming. 117 118 Extends QMainWindow as main window of Butterfly Viewer with user interface: 119 120 - Create sliding overlays. 121 - Adjust sliding overlay transparencies. 122 - Change viewer settings. 123 """ 124 125 MaxRecentFiles = 10 126 127 def __init__(self): 128 super(MultiViewMainWindow, self).__init__() 129 130 self._recentFileActions = [] 131 self._handlingScrollChangedSignal = False 132 133 self._mdiArea = QMdiAreaWithCustomSignals() 134 self._mdiArea.file_path_dragged.connect(self.display_dragged_grayout) 135 self._mdiArea.file_path_dragged_and_dropped.connect(self.load_from_dragged_and_dropped_file) 136 self._mdiArea.shortcut_escape_was_activated.connect(self.set_fullscreen_off) 137 self._mdiArea.shortcut_f_was_activated.connect(self.toggle_fullscreen) 138 self._mdiArea.shortcut_h_was_activated.connect(self.toggle_interface) 139 self._mdiArea.shortcut_ctrl_c_was_activated.connect(self.copy_view) 140 self._mdiArea.first_subwindow_was_opened.connect(self.on_first_subwindow_was_opened) 141 self._mdiArea.last_remaining_subwindow_was_closed.connect(self.on_last_remaining_subwindow_was_closed) 142 143 self._mdiArea.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) 144 self._mdiArea.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) 145 self._mdiArea.subWindowActivated.connect(self.subWindowActivated) 146 147 self._mdiArea.setBackground(QtGui.QColor(32,32,32)) 148 149 self._label_mouse = QtWidgets.QLabel() # Pixel coordinates of mouse in a view 150 self._label_mouse.setText("") 151 self._label_mouse.adjustSize() 152 self._label_mouse.setVisible(False) 153 self._label_mouse.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignBottom) 154 self._label_mouse.setStyleSheet("QLabel {color: white; background-color: rgba(0, 0, 0, 191); border: 0px solid black; margin-left: 0.09em; margin-top: 0.09em; margin-right: 0.09em; margin-bottom: 0.09em; font-size: 7.5pt; border-radius: 0.09em; }") 155 156 self._splitview_creator = SplitViewCreator() 157 self._splitview_creator.clicked_create_splitview_pushbutton.connect(self.on_create_splitview) 158 tracker_creator = EventTrackerSplitBypassInterface(self._splitview_creator) 159 tracker_creator.mouse_position_changed.connect(self.update_split) 160 layout_mdiarea_topleft = GridLayoutFloatingShadow() 161 layout_mdiarea_topleft.addWidget(self._label_mouse, 1, 0, alignment=QtCore.Qt.AlignLeft|QtCore.Qt.AlignBottom) 162 layout_mdiarea_topleft.addWidget(self._splitview_creator, 0, 0, alignment=QtCore.Qt.AlignLeft) 163 self.interface_mdiarea_topleft = QtWidgets.QWidget() 164 self.interface_mdiarea_topleft.setLayout(layout_mdiarea_topleft) 165 166 self._mdiArea.subWindowActivated.connect(self.update_sliders) 167 self._mdiArea.subWindowActivated.connect(self.update_window_highlight) 168 self._mdiArea.subWindowActivated.connect(self.update_window_labels) 169 self._mdiArea.subWindowActivated.connect(self.updateMenus) 170 self._mdiArea.subWindowActivated.connect(self.auto_tile_subwindows_on_close) 171 self._mdiArea.subWindowActivated.connect(self.update_mdi_buttons) 172 173 self._sliders_opacity_splitviews = SlidersOpacitySplitViews() 174 self._sliders_opacity_splitviews.was_changed_slider_base_value.connect(self.on_slider_opacity_base_changed) 175 self._sliders_opacity_splitviews.was_changed_slider_topright_value.connect(self.on_slider_opacity_topright_changed) 176 self._sliders_opacity_splitviews.was_changed_slider_bottomright_value.connect(self.on_slider_opacity_bottomright_changed) 177 self._sliders_opacity_splitviews.was_changed_slider_bottomleft_value.connect(self.on_slider_opacity_bottomleft_changed) 178 tracker_sliders = EventTrackerSplitBypassInterface(self._sliders_opacity_splitviews) 179 tracker_sliders.mouse_position_changed.connect(self.update_split) 180 181 self._splitview_manager = SplitViewManager() 182 self._splitview_manager.hovered_xy.connect(self.set_split_from_manager) 183 self._splitview_manager.clicked_xy.connect(self.set_and_lock_split_from_manager) 184 self._splitview_manager.lock_split_locked.connect(self.lock_split) 185 self._splitview_manager.lock_split_unlocked.connect(self.unlock_split) 186 187 layout_mdiarea_bottomleft = GridLayoutFloatingShadow() 188 layout_mdiarea_bottomleft.addWidget(self._sliders_opacity_splitviews, 0, 0, alignment=QtCore.Qt.AlignBottom) 189 layout_mdiarea_bottomleft.addWidget(self._splitview_manager, 0, 1, alignment=QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop) 190 self.interface_mdiarea_bottomleft = QtWidgets.QWidget() 191 self.interface_mdiarea_bottomleft.setLayout(layout_mdiarea_bottomleft) 192 193 194 self.centralwidget_during_fullscreen_pushbutton = QtWidgets.QToolButton() # Needed for users to return the image viewer to the main window if the window of the viewer is lost during fullscreen 195 self.centralwidget_during_fullscreen_pushbutton.setText("Close Fullscreen") # Needed for users to return the image viewer to the main window if the window of the viewer is lost during fullscreen 196 self.centralwidget_during_fullscreen_pushbutton.clicked.connect(self.set_fullscreen_off) 197 self.centralwidget_during_fullscreen_pushbutton.setStyleSheet("font-size: 11pt") 198 self.centralwidget_during_fullscreen_layout = QtWidgets.QVBoxLayout() 199 self.centralwidget_during_fullscreen_layout.setAlignment(QtCore.Qt.AlignCenter) 200 self.centralwidget_during_fullscreen_layout.addWidget(self.centralwidget_during_fullscreen_pushbutton, alignment=QtCore.Qt.AlignCenter) 201 self.centralwidget_during_fullscreen = QtWidgets.QWidget() 202 self.centralwidget_during_fullscreen.setLayout(self.centralwidget_during_fullscreen_layout) 203 204 205 self.fullscreen_pushbutton = QtWidgets.QPushButton("⇲") 206 self.fullscreen_pushbutton.setToolTip("Fullscreen on/off (F)") 207 self.fullscreen_pushbutton.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) 208 self.fullscreen_pushbutton.setMouseTracking(True) 209 self.fullscreen_pushbutton.setStyleSheet(""" 210 QPushButton { 211 width: 1em; 212 height: 1em; 213 color: white; 214 background-color: rgba(0, 0, 0, 63); 215 border-width: 0.03em; 216 border-style: solid; 217 border-color: transparent; 218 font-size: 23pt; 219 } 220 QPushButton:hover { 221 background-color: rgba(91, 91, 255, 223); 222 } 223 QPushButton:pressed { 224 color: white; 225 background-color: rgba(127, 127, 255, 255); 226 } 227 QPushButton:checked { 228 color: white; 229 background-color: rgba(63, 63, 191, 191); 230 border-color: rgba(127, 127, 255, 255); 231 } 232 QPushButton:checked:hover { 233 color: white; 234 background-color: rgba(95, 95, 223, 223); 235 border-color: rgba(127, 127, 255, 255); 236 } 237 QPushButton:checked:pressed { 238 color: white; 239 background-color: rgba(127, 127, 255, 255); 240 border-color: rgba(127, 127, 255, 255); 241 } 242 """) 243 self.fullscreen_pushbutton.setCheckable(True) 244 self.fullscreen_pushbutton.toggled.connect(self.set_fullscreen) 245 self.is_fullscreen = False 246 247 self.interface_toggle_pushbutton = QtWidgets.QPushButton("≼") 248 self.interface_toggle_pushbutton.setToolTip("Hide interface (H)") 249 self.interface_toggle_pushbutton.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) 250 self.interface_toggle_pushbutton.setMouseTracking(True) 251 self.interface_toggle_pushbutton.setStyleSheet(""" 252 QPushButton { 253 width: 1em; 254 height: 1em; 255 color: white; 256 background-color: rgba(0, 0, 0, 63); 257 border-width: 0.03em; 258 border-style: solid; 259 border-color: rgba(127, 127, 255, 255); 260 font-size: 23pt; 261 } 262 QPushButton:hover { 263 background-color: rgba(91, 91, 255, 223); 264 } 265 QPushButton:pressed { 266 color: white; 267 background-color: rgba(127, 127, 255, 255); 268 } 269 QPushButton:checked { 270 color: white; 271 background-color: rgba(63, 63, 191, 191); 272 border-color: rgba(127, 127, 255, 255); 273 } 274 QPushButton:checked:hover { 275 color: white; 276 background-color: rgba(95, 95, 223, 223); 277 border-color: rgba(127, 127, 255, 255); 278 } 279 QPushButton:checked:pressed { 280 color: white; 281 background-color: rgba(127, 127, 255, 255); 282 border-color: rgba(127, 127, 255, 255); 283 } 284 """) 285 self.interface_toggle_pushbutton.setCheckable(True) 286 self.interface_toggle_pushbutton.setChecked(True) 287 self.interface_toggle_pushbutton.clicked.connect(self.show_interface) 288 289 self.interface_toggle_slash_label = QtWidgets.QLabel("/") 290 self.interface_toggle_slash_label.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents) 291 self.interface_toggle_slash_label.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) 292 self.interface_toggle_slash_label.setMouseTracking(True) 293 self.interface_toggle_slash_label.setStyleSheet(""" 294 QLabel { 295 width: 1em; 296 height: 1em; 297 color: white; 298 background-color: rgba(0, 0, 0, 0); 299 border-width: 0.03em; 300 border-style: solid; 301 border-color: rgba(0, 0, 0, 0); 302 font-size: 22pt; 303 } 304 """) 305 self.interface_toggle_slash_label.setVisible(False) 306 307 self.is_interface_showing = True 308 self.is_quiet_mode = False 309 self.is_global_transform_mode_smooth = False 310 self.scene_background_color = None 311 312 self.close_all_pushbutton = QtWidgets.QPushButton("⦻") 313 self.close_all_pushbutton.setToolTip("Close all image windows") 314 self.close_all_pushbutton.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) 315 self.close_all_pushbutton.setMouseTracking(True) 316 self.close_all_pushbutton.setStyleSheet(""" 317 QPushButton { 318 width: 1.03em; 319 height: 1.03em; 320 color: white; 321 background-color: rgba(0, 0, 0, 63); 322 border: 0px black; 323 font-size: 23pt; 324 } 325 QPushButton:hover { 326 color: white; 327 background-color: rgba(223, 0, 0, 223); 328 } 329 QPushButton:pressed { 330 color: white; 331 background-color: rgba(255, 0, 0, 255); 332 } 333 """) 334 self.close_all_pushbutton.clicked.connect(self._mdiArea.closeAllSubWindows) 335 336 self.tile_default_pushbutton = QtWidgets.QPushButton("⦺") 337 self.tile_default_pushbutton.setToolTip("Grid arrange windows") 338 self.tile_default_pushbutton.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) 339 self.tile_default_pushbutton.setMouseTracking(True) 340 self.tile_default_pushbutton.setStyleSheet(""" 341 QPushButton { 342 width: 1em; 343 height: 1em; 344 color: white; 345 background-color: rgba(0, 0, 0, 63); 346 border-width: 0.03em; 347 border-style: solid; 348 border-color: transparent; 349 font-size: 23pt; 350 } 351 QPushButton:hover { 352 background-color: rgba(91, 91, 91, 223); 353 border-color: transparent; 354 } 355 QPushButton:pressed { 356 background-color: rgba(116, 116, 116, 255); 357 border-color: transparent; 358 } 359 """) 360 self.tile_default_pushbutton.clicked.connect(self._mdiArea.tileSubWindows) 361 self.tile_default_pushbutton.clicked.connect(self.fit_to_window) 362 self.tile_default_pushbutton.clicked.connect(self.refreshPan) 363 364 self.tile_horizontally_pushbutton = QtWidgets.QPushButton("⦶") 365 self.tile_horizontally_pushbutton.setToolTip("Horizontally arrange windows in a single row") 366 self.tile_horizontally_pushbutton.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) 367 self.tile_horizontally_pushbutton.setMouseTracking(True) 368 self.tile_horizontally_pushbutton.setStyleSheet(""" 369 QPushButton { 370 width: 1em; 371 height: 1em; 372 color: white; 373 background-color: rgba(0, 0, 0, 63); 374 border-width: 0.03em; 375 border-style: solid; 376 border-color: transparent; 377 font-size: 23pt; 378 } 379 QPushButton:hover { 380 background-color: rgba(91, 91, 91, 223); 381 border-color: transparent; 382 } 383 QPushButton:pressed { 384 background-color: rgba(116, 116, 116, 255); 385 border-color: transparent; 386 } 387 """) 388 self.tile_horizontally_pushbutton.clicked.connect(self._mdiArea.tile_subwindows_horizontally) 389 self.tile_horizontally_pushbutton.clicked.connect(self.fit_to_window) 390 self.tile_horizontally_pushbutton.clicked.connect(self.refreshPan) 391 392 self.tile_vertically_pushbutton = QtWidgets.QPushButton("⦵") 393 self.tile_vertically_pushbutton.setToolTip("Vertically arrange windows in a single column") 394 self.tile_vertically_pushbutton.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) 395 self.tile_vertically_pushbutton.setMouseTracking(True) 396 self.tile_vertically_pushbutton.setStyleSheet(""" 397 QPushButton { 398 min-width: 1em; 399 min-height: 1em; 400 color: white; 401 background-color: rgba(0, 0, 0, 63); 402 border-width: 0.03em; 403 border-style: solid; 404 border-color: transparent; 405 font-size: 23pt; 406 } 407 QPushButton:hover { 408 background-color: rgba(91, 91, 91, 223); 409 border-color: transparent; 410 } 411 QPushButton:pressed { 412 background-color: rgba(116, 116, 116, 255); 413 border-color: transparent; 414 } 415 """) 416 self.tile_vertically_pushbutton.clicked.connect(self._mdiArea.tile_subwindows_vertically) 417 self.tile_vertically_pushbutton.clicked.connect(self.fit_to_window) 418 self.tile_vertically_pushbutton.clicked.connect(self.refreshPan) 419 420 self.fit_to_window_pushbutton = QtWidgets.QPushButton("⦾") 421 self.fit_to_window_pushbutton.setToolTip("Fit and center image in active window (affects all if synced)") 422 self.fit_to_window_pushbutton.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) 423 self.fit_to_window_pushbutton.setMouseTracking(True) 424 self.fit_to_window_pushbutton.setStyleSheet(""" 425 QPushButton { 426 width: 1em; 427 height: 1em; 428 color: white; 429 background-color: rgba(0, 0, 0, 63); 430 border-width: 0.03em; 431 border-style: solid; 432 border-color: transparent; 433 font-size: 23pt; 434 } 435 QPushButton:hover { 436 background-color: rgba(91, 91, 91, 223); 437 border-color: transparent; 438 } 439 QPushButton:pressed { 440 background-color: rgba(116, 116, 116, 255); 441 border-color: transparent; 442 } 443 """) 444 self.fit_to_window_pushbutton.clicked.connect(self.fit_to_window) 445 446 self.info_pushbutton = QtWidgets.QPushButton("ℹ︎") 447 self.info_pushbutton.setToolTip("Info...") 448 self.info_pushbutton.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) 449 self.info_pushbutton.setMouseTracking(True) 450 self.info_pushbutton.setStyleSheet(""" 451 QPushButton { 452 width: 1em; 453 height: 1em; 454 color: white; 455 background-color: rgba(0, 0, 0, 0); 456 border-width: 0.03em; 457 border-style: solid; 458 border-color: transparent; 459 font-size: 23pt; 460 } 461 QPushButton:hover { 462 background-color: rgba(91, 91, 91, 223); 463 border-color: transparent; 464 } 465 QPushButton:pressed { 466 background-color: rgba(116, 116, 116, 255); 467 border-color: transparent; 468 } 469 """) 470 self.info_pushbutton.clicked.connect(self.info_button_clicked) 471 472 self.stopsync_toggle_pushbutton = QtWidgets.QPushButton("⇆") 473 self.stopsync_toggle_pushbutton.setToolTip("Unsynchronize zoom and pan (currently synced)") 474 self.stopsync_toggle_pushbutton.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) 475 self.stopsync_toggle_pushbutton.setMouseTracking(True) 476 self.stopsync_toggle_pushbutton.setStyleSheet(""" 477 QPushButton { 478 width: 1em; 479 height: 1em; 480 color: white; 481 background-color: rgba(67, 176, 42, 191); 482 border-width: 0.03em; 483 border-style: solid; 484 border-color: rgba(80, 211, 50, 255); 485 font-size: 23pt; 486 } 487 QPushButton:hover { 488 background-color: rgba(77, 187, 51, 223); 489 border-color: rgba(80, 211, 50, 255); 490 } 491 QPushButton:pressed { 492 background-color: rgba(93, 187, 31, 255); 493 border-color: rgba(80, 211, 50, 255); 494 } 495 QPushButton:checked { 496 background-color: rgba(191, 151, 0, 191); 497 border-color: rgba(255, 181, 0, 255); 498 } 499 QPushButton:checked:hover { 500 background-color: rgba(223, 166, 0, 223); 501 border-color: rgba(255, 181, 0, 255); 502 } 503 QPushButton:checked:pressed { 504 background-color: rgba(255, 181, 0, 255); 505 border-color: rgba(255, 181, 0, 255); 506 } 507 """) 508 self.stopsync_toggle_pushbutton.setCheckable(True) 509 self.stopsync_toggle_pushbutton.toggled.connect(self.set_stopsync_pushbutton) 510 511 self.stopsync_toggle_slash_label = QtWidgets.QLabel("/") 512 self.stopsync_toggle_slash_label.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents) 513 self.stopsync_toggle_slash_label.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) 514 self.stopsync_toggle_slash_label.setMouseTracking(True) 515 self.stopsync_toggle_slash_label.setStyleSheet(""" 516 QLabel { 517 width: 1em; 518 height: 1em; 519 color: white; 520 background-color: rgba(0, 0, 0, 0); 521 border-width: 0.03em; 522 border-style: solid; 523 border-color: rgba(0, 0, 0, 0); 524 font-size: 22pt; 525 } 526 """) 527 self.stopsync_toggle_slash_label.setVisible(False) 528 529 self.save_view_pushbutton = QtWidgets.QPushButton("⤓") 530 self.save_view_pushbutton.setToolTip("Save a screenshot of the viewer... | Copy screenshot to clipboard (Ctrl·C)") 531 self.save_view_pushbutton.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) 532 self.save_view_pushbutton.setMouseTracking(True) 533 self.save_view_pushbutton.setStyleSheet(""" 534 QPushButton { 535 width: 1em; 536 height: 1em; 537 color: white; 538 background-color: rgba(0, 0, 0, 63); 539 border-width: 0.03em; 540 border-style: solid; 541 border-color: transparent; 542 font-size: 23pt; 543 } 544 QPushButton:hover { 545 background-color: rgba(91, 91, 255, 223); 546 } 547 QPushButton:pressed { 548 color: white; 549 background-color: rgba(127, 127, 255, 255); 550 } 551 """) 552 self.save_view_pushbutton.clicked.connect(self.save_view) 553 554 self.buffer_label = QtWidgets.QPushButton() 555 self.buffer_label.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents) 556 self.buffer_label.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) 557 self.buffer_label.setMouseTracking(True) 558 self.buffer_label.setStyleSheet(""" 559 QPushButton { 560 width: 1em; 561 height: 1em; 562 color: transparent; 563 background-color: rgba(0, 0, 0, 0); 564 border-width: 0.03em; 565 border-style: solid; 566 border-color: rgba(0, 0, 0, 0); 567 font-size: 23pt; 568 } 569 """) 570 571 self.label_mdiarea = QtWidgets.QLabel() 572 self.label_mdiarea.setText("Drag images directly to create individual image windows\n\n—\n\nCreate sliding overlays to compare images directly over each other\n\n—\n\nRight-click image windows to change settings and add tools") 573 self.label_mdiarea.setStyleSheet(""" 574 QLabel { 575 color: white; 576 border: 0.13em dashed gray; 577 border-radius: 0.25em; 578 background-color: transparent; 579 padding: 1em; 580 font-size: 10pt; 581 } 582 """) 583 self.label_mdiarea.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents) 584 self.label_mdiarea.setAlignment(QtCore.Qt.AlignCenter) 585 586 layout_mdiarea_bottomright_vertical = GridLayoutFloatingShadow() 587 layout_mdiarea_bottomright_vertical.addWidget(self.fullscreen_pushbutton, 5, 0) 588 layout_mdiarea_bottomright_vertical.addWidget(self.tile_default_pushbutton, 4, 0) 589 layout_mdiarea_bottomright_vertical.addWidget(self.tile_horizontally_pushbutton, 3, 0) 590 layout_mdiarea_bottomright_vertical.addWidget(self.tile_vertically_pushbutton, 2, 0) 591 layout_mdiarea_bottomright_vertical.addWidget(self.fit_to_window_pushbutton, 1, 0) 592 layout_mdiarea_bottomright_vertical.addWidget(self.info_pushbutton, 0, 0) 593 layout_mdiarea_bottomright_vertical.setContentsMargins(0,0,0,16) 594 self.interface_mdiarea_bottomright_vertical = QtWidgets.QWidget() 595 self.interface_mdiarea_bottomright_vertical.setLayout(layout_mdiarea_bottomright_vertical) 596 tracker_interface_mdiarea_bottomright_vertical = EventTrackerSplitBypassInterface(self.interface_mdiarea_bottomright_vertical) 597 tracker_interface_mdiarea_bottomright_vertical.mouse_position_changed.connect(self.update_split) 598 599 layout_mdiarea_bottomright_horizontal = GridLayoutFloatingShadow() 600 layout_mdiarea_bottomright_horizontal.addWidget(self.buffer_label, 0, 5) 601 layout_mdiarea_bottomright_horizontal.addWidget(self.interface_toggle_pushbutton, 0, 4) 602 layout_mdiarea_bottomright_horizontal.addWidget(self.interface_toggle_slash_label, 0, 4) 603 layout_mdiarea_bottomright_horizontal.addWidget(self.close_all_pushbutton, 0, 3) 604 layout_mdiarea_bottomright_horizontal.addWidget(self.stopsync_toggle_pushbutton, 0, 2) 605 layout_mdiarea_bottomright_horizontal.addWidget(self.stopsync_toggle_slash_label, 0, 2) 606 layout_mdiarea_bottomright_horizontal.addWidget(self.save_view_pushbutton, 0, 1) 607 layout_mdiarea_bottomright_horizontal.setContentsMargins(0,0,0,16) 608 self.interface_mdiarea_bottomright_horizontal = QtWidgets.QWidget() 609 self.interface_mdiarea_bottomright_horizontal.setLayout(layout_mdiarea_bottomright_horizontal) 610 tracker_interface_mdiarea_bottomright_horizontal = EventTrackerSplitBypassInterface(self.interface_mdiarea_bottomright_horizontal) 611 tracker_interface_mdiarea_bottomright_horizontal.mouse_position_changed.connect(self.update_split) 612 613 614 self.loading_grayout_label = QtWidgets.QLabel("Loading...") # Needed to give users feedback when loading views 615 self.loading_grayout_label.setWordWrap(True) 616 self.loading_grayout_label.setAlignment(QtCore.Qt.AlignCenter | QtCore.Qt.AlignVCenter) 617 self.loading_grayout_label.setVisible(False) 618 self.loading_grayout_label.setStyleSheet(""" 619 QLabel { 620 color: white; 621 background-color: rgba(0,0,0,223); 622 font-size: 10pt; 623 } 624 """) 625 626 self.dragged_grayout_label = QtWidgets.QLabel("Drop to create single view(s)...") # Needed to give users feedback when dragging in images 627 self.dragged_grayout_label.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents) 628 self.dragged_grayout_label.setWordWrap(True) 629 self.dragged_grayout_label.setAlignment(QtCore.Qt.AlignCenter | QtCore.Qt.AlignVCenter) 630 self.dragged_grayout_label.setVisible(False) 631 self.dragged_grayout_label.setStyleSheet(""" 632 QLabel { 633 color: white; 634 background-color: rgba(63,63,63,223); 635 border: 0.13em dashed gray; 636 border-radius: 0.25em; 637 margin-left: 0.25em; 638 margin-top: 0.25em; 639 margin-right: 0.25em; 640 margin-bottom: 0.25em; 641 font-size: 10pt; 642 } 643 """) 644 645 646 layout_mdiarea = QtWidgets.QGridLayout() 647 layout_mdiarea.setContentsMargins(0, 0, 0, 0) 648 layout_mdiarea.setSpacing(0) 649 layout_mdiarea.addWidget(self._mdiArea, 0, 0) 650 layout_mdiarea.addWidget(self.label_mdiarea, 0, 0, QtCore.Qt.AlignCenter) 651 layout_mdiarea.addWidget(self.dragged_grayout_label, 0, 0) 652 layout_mdiarea.addWidget(self.loading_grayout_label, 0, 0) 653 layout_mdiarea.addWidget(self.interface_mdiarea_topleft, 0, 0, QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft) 654 layout_mdiarea.addWidget(self.interface_mdiarea_bottomleft, 0, 0, QtCore.Qt.AlignBottom | QtCore.Qt.AlignLeft) 655 layout_mdiarea.addWidget(self.interface_mdiarea_bottomright_horizontal, 0, 0, QtCore.Qt.AlignBottom | QtCore.Qt.AlignRight) 656 layout_mdiarea.addWidget(self.interface_mdiarea_bottomright_vertical, 0, 0, QtCore.Qt.AlignBottom | QtCore.Qt.AlignRight) 657 658 self.mdiarea_plus_buttons = QtWidgets.QWidget() 659 self.mdiarea_plus_buttons.setLayout(layout_mdiarea) 660 661 self.setCentralWidget(self.mdiarea_plus_buttons) 662 663 self.subwindow_was_just_closed = False 664 665 self._windowMapper = QtCore.QSignalMapper(self) 666 667 self._actionMapper = QtCore.QSignalMapper(self) 668 self._actionMapper.mapped[str].connect(self.mappedImageViewerAction) 669 self._recentFileMapper = QtCore.QSignalMapper(self) 670 self._recentFileMapper.mapped[str].connect(self.openRecentFile) 671 672 self.createActions() 673 self.addAction(self._activateSubWindowSystemMenuAct) 674 675 self.createMenus() 676 self.updateMenus() 677 self.createStatusBar() 678 679 self.readSettings() 680 self.updateStatusBar() 681 682 self.setUnifiedTitleAndToolBarOnMac(True) 683 684 self.showNormal() 685 self.menuBar().hide() 686 687 self.setStyleSheet("QWidget{font-size: 9pt}") 688 689 690 # Screenshot window 691 692 def copy_view(self): 693 """Screenshot MultiViewMainWindow and copy to clipboard as image.""" 694 695 self.display_loading_grayout(True, "Screenshot copied to clipboard.") 696 697 interface_was_already_set_hidden = not self.is_interface_showing # Needed to hide the interface temporarily while grabbing a screenshot (makes sure the screenshot only shows the views) 698 if not interface_was_already_set_hidden: 699 self.show_interface_off() 700 701 pixmap = self._mdiArea.grab() 702 clipboard = QtWidgets.QApplication.clipboard() 703 clipboard.setPixmap(pixmap) 704 705 if not interface_was_already_set_hidden: 706 self.show_interface_on() 707 708 self.display_loading_grayout(False, pseudo_load_time=1) 709 710 711 def save_view(self): 712 """Screenshot MultiViewMainWindow and open Save dialog to save screenshot as image.""" 713 714 self.display_loading_grayout(True, "Saving viewer screenshot...") 715 716 folderpath = None 717 718 if self.activeMdiChild: 719 folderpath = self.activeMdiChild.currentFile 720 folderpath = os.path.dirname(folderpath) 721 folderpath = folderpath + "\\" 722 else: 723 self.display_loading_grayout(False, pseudo_load_time=0) 724 return 725 726 interface_was_already_set_hidden = not self.is_interface_showing # Needed to hide the interface temporarily while grabbing a screenshot (makes sure the screenshot only shows the views) 727 if not interface_was_already_set_hidden: 728 self.show_interface_off() 729 730 pixmap = self._mdiArea.grab() 731 732 date_and_time = datetime.now().strftime('%Y-%m-%d %H%M%S') # Sets the default filename with date and time 733 filename = "Viewer screenshot " + date_and_time + ".png" 734 name_filters = "PNG (*.png);; JPEG (*.jpeg);; TIFF (*.tiff);; JPG (*.jpg);; TIF (*.tif)" # Allows users to select filetype of screenshot 735 736 self.display_loading_grayout(True, "Selecting folder and name for the viewer screenshot...", pseudo_load_time=0) 737 738 filepath, _ = QtWidgets.QFileDialog.getSaveFileName(self, "Save a screenshot of the viewer", folderpath+filename, name_filters) 739 _, fileextension = os.path.splitext(filepath) 740 fileextension = fileextension.replace('.','') 741 if filepath: 742 pixmap.save(filepath, fileextension) 743 744 if not interface_was_already_set_hidden: 745 self.show_interface_on() 746 747 self.display_loading_grayout(False) 748 749 750 # Interface and appearance 751 752 def display_loading_grayout(self, boolean, text="Loading...", pseudo_load_time=0.2): 753 """Show/hide grayout screen for loading sequences. 754 755 Args: 756 boolean (bool): True to show grayout; False to hide. 757 text (str): The text to show on the grayout. 758 pseudo_load_time (float): The delay (in seconds) to hide the grayout to give users a feeling of action. 759 """ 760 if not boolean: 761 text = "Loading..." 762 self.loading_grayout_label.setText(text) 763 self.loading_grayout_label.setVisible(boolean) 764 if boolean: 765 self.loading_grayout_label.repaint() 766 if not boolean: 767 time.sleep(pseudo_load_time) 768 769 def display_dragged_grayout(self, boolean): 770 """Show/hide grayout screen for drag-and-drop sequences. 771 772 Args: 773 boolean (bool): True to show grayout; False to hide. 774 """ 775 self.dragged_grayout_label.setVisible(boolean) 776 if boolean: 777 self.dragged_grayout_label.repaint() 778 779 def on_last_remaining_subwindow_was_closed(self): 780 """Show instructions label of MDIArea.""" 781 self.label_mdiarea.setVisible(True) 782 783 def on_first_subwindow_was_opened(self): 784 """Hide instructions label of MDIArea.""" 785 self.label_mdiarea.setVisible(False) 786 787 def show_interface(self, boolean): 788 """Show/hide interface elements for sliding overlay creator and transparencies. 789 790 Args: 791 boolean (bool): True to show interface; False to hide. 792 """ 793 if boolean: 794 self.show_interface_on() 795 elif not boolean: 796 self.show_interface_off() 797 798 def show_interface_on(self): 799 """Show interface elements for sliding overlay creator and transparencies.""" 800 if self.is_interface_showing: 801 return 802 803 self.is_interface_showing = True 804 self.is_quiet_mode = False 805 806 self.update_window_highlight(self._mdiArea.activeSubWindow()) 807 self.update_window_labels(self._mdiArea.activeSubWindow()) 808 self.set_window_close_pushbuttons_always_visible(self._mdiArea.activeSubWindow(), True) 809 self.set_window_mouse_rect_visible(self._mdiArea.activeSubWindow(), True) 810 self.interface_mdiarea_topleft.setVisible(True) 811 self.interface_mdiarea_bottomleft.setVisible(True) 812 self.interface_toggle_slash_label.setVisible(False) 813 814 self.interface_toggle_pushbutton.setToolTip("Hide interface (studio mode)") 815 816 if self.interface_toggle_pushbutton: 817 self.interface_toggle_pushbutton.setChecked(True) 818 819 def show_interface_off(self): 820 """Hide interface elements for sliding overlay creator and transparencies.""" 821 if not self.is_interface_showing: 822 return 823 824 self.is_interface_showing = False 825 self.is_quiet_mode = True 826 827 self.update_window_highlight(self._mdiArea.activeSubWindow()) 828 self.update_window_labels(self._mdiArea.activeSubWindow()) 829 self.set_window_close_pushbuttons_always_visible(self._mdiArea.activeSubWindow(), False) 830 self.set_window_mouse_rect_visible(self._mdiArea.activeSubWindow(), False) 831 self.interface_mdiarea_topleft.setVisible(False) 832 self.interface_mdiarea_bottomleft.setVisible(False) 833 self.interface_toggle_slash_label.setVisible(True) 834 835 self.interface_toggle_pushbutton.setToolTip("Show interface (H)") 836 837 if self.interface_toggle_pushbutton: 838 self.interface_toggle_pushbutton.setChecked(False) 839 self.interface_toggle_pushbutton.setAttribute(QtCore.Qt.WA_UnderMouse, False) 840 841 def toggle_interface(self): 842 """Toggle visibilty of interface elements for sliding overlay creator and transparencies.""" 843 if self.is_interface_showing: # If interface is showing, then toggle it off; if not, then toggle it on 844 self.show_interface_off() 845 else: 846 self.show_interface_on() 847 848 def set_stopsync_pushbutton(self, boolean): 849 """Set state of synchronous zoom/pan and appearance of corresponding interface button. 850 851 Args: 852 boolean (bool): True to enable synchronized zoom/pan; False to disable. 853 """ 854 self._synchZoomAct.setChecked(not boolean) 855 self._synchPanAct.setChecked(not boolean) 856 857 if self._synchZoomAct.isChecked(): 858 if self.activeMdiChild: 859 self.activeMdiChild.fitToWindow() 860 861 if boolean: 862 self.stopsync_toggle_pushbutton.setText("⇆") 863 self.stopsync_toggle_pushbutton.setToolTip("Synchronize zoom and pan (currently unsynced)") 864 self.stopsync_toggle_slash_label.show() 865 else: 866 self.stopsync_toggle_pushbutton.setText("⇆") 867 self.stopsync_toggle_pushbutton.setToolTip("Unsynchronize zoom and pan (currently synced)") 868 self.stopsync_toggle_slash_label.hide() 869 870 def toggle_fullscreen(self): 871 """Toggle fullscreen state of app.""" 872 if self.is_fullscreen: 873 self.set_fullscreen_off() 874 else: 875 self.set_fullscreen_on() 876 877 def set_fullscreen_on(self): 878 """Enable fullscreen of MultiViewMainWindow. 879 880 Moves MDIArea to secondary window and makes it fullscreen. 881 Shows interim widget in main window. 882 """ 883 if self.is_fullscreen: 884 return 885 886 position_of_window = self.pos() 887 888 centralwidget_to_be_made_fullscreen = self.mdiarea_plus_buttons 889 widget_to_replace_central = self.centralwidget_during_fullscreen 890 891 centralwidget_to_be_made_fullscreen.setParent(None) 892 893 # move() is needed when using multiple monitors because when the widget loses its parent, its position moves to the primary screen origin (0,0) instead of retaining the app's screen 894 # The solution is to move the widget to the position of the app window and then make the widget fullscreen 895 # A timer is needed for showFullScreen() to apply on the app's screen (otherwise the command is made before the widget's move is established) 896 centralwidget_to_be_made_fullscreen.move(position_of_window) 897 QtCore.QTimer.singleShot(50, centralwidget_to_be_made_fullscreen.showFullScreen) 898 899 self.showMinimized() 900 901 self.setCentralWidget(widget_to_replace_central) 902 widget_to_replace_central.show() 903 904 self._mdiArea.tile_what_was_done_last_time() 905 self._mdiArea.activateWindow() 906 907 self.is_fullscreen = True 908 if self.fullscreen_pushbutton: 909 self.fullscreen_pushbutton.setChecked(True) 910 911 if self.activeMdiChild: 912 self.synchPan(self.activeMdiChild) 913 914 def set_fullscreen_off(self): 915 """Disable fullscreen of MultiViewMainWindow. 916 917 Removes interim widget in main window. 918 Returns MDIArea to normal (non-fullscreen) view on main window. 919 """ 920 if not self.is_fullscreen: 921 return 922 923 self.showNormal() 924 925 fullscreenwidget_to_be_made_central = self.mdiarea_plus_buttons 926 centralwidget_to_be_hidden = self.centralwidget_during_fullscreen 927 928 centralwidget_to_be_hidden.setParent(None) 929 centralwidget_to_be_hidden.hide() 930 931 self.setCentralWidget(fullscreenwidget_to_be_made_central) 932 933 self._mdiArea.tile_what_was_done_last_time() 934 self._mdiArea.activateWindow() 935 936 self.is_fullscreen = False 937 if self.fullscreen_pushbutton: 938 self.fullscreen_pushbutton.setChecked(False) 939 self.fullscreen_pushbutton.setAttribute(QtCore.Qt.WA_UnderMouse, False) 940 941 self.refreshPanDelayed(100) 942 943 def set_fullscreen(self, boolean): 944 """Enable/disable fullscreen of MultiViewMainWindow. 945 946 Args: 947 boolean (bool): True to enable fullscreen; False to disable. 948 """ 949 if boolean: 950 self.set_fullscreen_on() 951 elif not boolean: 952 self.set_fullscreen_off() 953 954 def update_window_highlight(self, window): 955 """Update highlight of subwindows in MDIArea. 956 957 Input window should be the subwindow which is active. 958 All other subwindow(s) will be shown no highlight. 959 960 Args: 961 window (QMdiSubWindow): The active subwindow to show highlight and indicate as active. 962 """ 963 if window is None: 964 return 965 changed_window = window 966 if self.is_quiet_mode: 967 changed_window.widget().frame_hud.setStyleSheet("QFrame {border: 0px solid transparent}") 968 elif self.activeMdiChild.split_locked: 969 changed_window.widget().frame_hud.setStyleSheet("QFrame {border: 0.2em orange; border-left-style: outset; border-top-style: inset; border-right-style: inset; border-bottom-style: inset}") 970 else: 971 changed_window.widget().frame_hud.setStyleSheet("QFrame {border: 0.2em blue; border-left-style: outset; border-top-style: inset; border-right-style: inset; border-bottom-style: inset}") 972 973 windows = self._mdiArea.subWindowList() 974 for window in windows: 975 if window != changed_window: 976 window.widget().frame_hud.setStyleSheet("QFrame {border: 0px solid transparent}") 977 978 def update_window_labels(self, window): 979 """Update labels of subwindows in MDIArea. 980 981 Input window should be the subwindow which is active. 982 All other subwindow(s) will be shown no labels. 983 984 Args: 985 window (QMdiSubWindow): The active subwindow to show label(s) of image(s) and indicate as active. 986 """ 987 if window is None: 988 return 989 changed_window = window 990 label_visible = True 991 if self.is_quiet_mode: 992 label_visible = False 993 changed_window.widget().label_main_topleft.set_visible_based_on_text(label_visible) 994 changed_window.widget().label_topright.set_visible_based_on_text(label_visible) 995 changed_window.widget().label_bottomright.set_visible_based_on_text(label_visible) 996 changed_window.widget().label_bottomleft.set_visible_based_on_text(label_visible) 997 998 windows = self._mdiArea.subWindowList() 999 for window in windows: 1000 if window != changed_window: 1001 window.widget().label_main_topleft.set_visible_based_on_text(False) 1002 window.widget().label_topright.set_visible_based_on_text(False) 1003 window.widget().label_bottomright.set_visible_based_on_text(False) 1004 window.widget().label_bottomleft.set_visible_based_on_text(False) 1005 1006 def set_window_close_pushbuttons_always_visible(self, window, boolean): 1007 """Enable/disable the always-on visiblilty of the close X on each subwindow. 1008 1009 Args: 1010 window (QMdiSubWindow): The active subwindow. 1011 boolean (bool): True to show the close X always; False to hide unless mouse hovers over. 1012 """ 1013 if window is None: 1014 return 1015 changed_window = window 1016 always_visible = boolean 1017 changed_window.widget().set_close_pushbutton_always_visible(always_visible) 1018 windows = self._mdiArea.subWindowList() 1019 for window in windows: 1020 if window != changed_window: 1021 window.widget().set_close_pushbutton_always_visible(always_visible) 1022 1023 def set_window_mouse_rect_visible(self, window, boolean): 1024 """Enable/disable the visiblilty of the red 1x1 outline at the pointer 1025 1026 Outline shows the relative size of a pixel in the active subwindow. 1027 1028 Args: 1029 window (QMdiSubWindow): The active subwindow. 1030 boolean (bool): True to show 1x1 outline; False to hide. 1031 """ 1032 if window is None: 1033 return 1034 changed_window = window 1035 visible = boolean 1036 changed_window.widget().set_mouse_rect_visible(visible) 1037 windows = self._mdiArea.subWindowList() 1038 for window in windows: 1039 if window != changed_window: 1040 window.widget().set_mouse_rect_visible(visible) 1041 1042 def auto_tile_subwindows_on_close(self): 1043 """Tile the subwindows of MDIArea using previously used tile method.""" 1044 if self.subwindow_was_just_closed: 1045 self.subwindow_was_just_closed = False 1046 QtCore.QTimer.singleShot(50, self._mdiArea.tile_what_was_done_last_time) 1047 self.refreshPanDelayed(50) 1048 1049 def update_mdi_buttons(self, window): 1050 """Update the interface button 'Split Lock' based on the status of the split (locked/unlocked) in the given window. 1051 1052 Args: 1053 window (QMdiSubWindow): The active subwindow. 1054 """ 1055 if window is None: 1056 self._splitview_manager.lock_split_pushbutton.setChecked(False) 1057 return 1058 1059 child = self.activeMdiChild 1060 1061 self._splitview_manager.lock_split_pushbutton.setChecked(child.split_locked) 1062 1063 1064 def set_single_window_transform_mode_smooth(self, window, boolean): 1065 """Set the transform mode of a given subwindow. 1066 1067 Args: 1068 window (QMdiSubWindow): The subwindow. 1069 boolean (bool): True to smooth (interpolate); False to fast (not interpolate). 1070 """ 1071 if window is None: 1072 return 1073 changed_window = window 1074 changed_window.widget().set_transform_mode_smooth(boolean) 1075 1076 1077 def set_all_window_transform_mode_smooth(self, boolean): 1078 """Set the transform mode of all subwindows. 1079 1080 Args: 1081 boolean (bool): True to smooth (interpolate); False to fast (not interpolate). 1082 """ 1083 if self._mdiArea.activeSubWindow() is None: 1084 return 1085 windows = self._mdiArea.subWindowList() 1086 for window in windows: 1087 window.widget().set_transform_mode_smooth(boolean) 1088 1089 def set_all_background_color(self, color): 1090 """Set the background color of all subwindows. 1091 1092 Args: 1093 color (list): Descriptor string and RGB int values. Example: ["White", 255, 255, 255]. 1094 """ 1095 if self._mdiArea.activeSubWindow() is None: 1096 return 1097 windows = self._mdiArea.subWindowList() 1098 for window in windows: 1099 window.widget().set_scene_background_color(color) 1100 self.scene_background_color = color 1101 1102 def info_button_clicked(self): 1103 """Trigger when info button is clicked.""" 1104 self.show_about() 1105 return 1106 1107 def show_about(self): 1108 """Show about box.""" 1109 sp = "<br>" 1110 title = "Butterfly Viewer" 1111 text = "Butterfly Viewer" 1112 text = text + sp + "Lars Maxfield" 1113 text = text + sp + "Version: " + __version__ 1114 text = text + sp + "License: <a href='https://www.gnu.org/licenses/gpl-3.0.en.html'>GNU GPL v3</a> or later" 1115 text = text + sp + "Source: <a href='https://github.com/olive-groves/butterfly_viewer'>github.com/olive-groves/butterfly_viewer</a>" 1116 text = text + sp + "Tutorial: <a href='https://olive-groves.github.io/butterfly_viewer'>olive-groves.github.io/butterfly_viewer</a>" 1117 box = QtWidgets.QMessageBox.about(self, title, text) 1118 1119 # View loading methods 1120 1121 def loadFile(self, filename_main_topleft, filename_topright=None, filename_bottomleft=None, filename_bottomright=None): 1122 """Load an individual image or sliding overlay into new subwindow. 1123 1124 Args: 1125 filename_main_topleft (str): The image filepath of the main image to be viewed; the basis of the sliding overlay (main; topleft) 1126 filename_topright (str): The image filepath for top-right of the sliding overlay (set None to exclude) 1127 filename_bottomleft (str): The image filepath for bottom-left of the sliding overlay (set None to exclude) 1128 filename_bottomright (str): The image filepath for bottom-right of the sliding overlay (set None to exclude) 1129 """ 1130 1131 self.display_loading_grayout(True, "Loading viewer with main image '" + filename_main_topleft.split("/")[-1] + "'...") 1132 1133 activeMdiChild = self.activeMdiChild 1134 QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) 1135 1136 transform_mode_smooth = self.is_global_transform_mode_smooth 1137 1138 pixmap = QtGui.QPixmap(filename_main_topleft) 1139 pixmap_topright = QtGui.QPixmap(filename_topright) 1140 pixmap_bottomleft = QtGui.QPixmap(filename_bottomleft) 1141 pixmap_bottomright = QtGui.QPixmap(filename_bottomright) 1142 1143 QtWidgets.QApplication.restoreOverrideCursor() 1144 1145 if (not pixmap or 1146 pixmap.width()==0 or pixmap.height==0): 1147 self.display_loading_grayout(True, "Waiting on dialog box...") 1148 QtWidgets.QMessageBox.warning(self, APPNAME, 1149 "Cannot read file %s." % (filename_main_topleft,)) 1150 self.updateRecentFileSettings(filename_main_topleft, delete=True) 1151 self.updateRecentFileActions() 1152 self.display_loading_grayout(False) 1153 return 1154 1155 angle = get_exif_rotation_angle(filename_main_topleft) 1156 if angle: 1157 pixmap = pixmap.transformed(QtGui.QTransform().rotate(angle)) 1158 1159 angle = get_exif_rotation_angle(filename_topright) 1160 if angle: 1161 pixmap_topright = pixmap_topright.transformed(QtGui.QTransform().rotate(angle)) 1162 1163 angle = get_exif_rotation_angle(filename_bottomright) 1164 if angle: 1165 pixmap_bottomright = pixmap_bottomright.transformed(QtGui.QTransform().rotate(angle)) 1166 1167 angle = get_exif_rotation_angle(filename_bottomleft) 1168 if angle: 1169 pixmap_bottomleft = pixmap_bottomleft.transformed(QtGui.QTransform().rotate(angle)) 1170 1171 child = self.createMdiChild(pixmap, filename_main_topleft, pixmap_topright, pixmap_bottomleft, pixmap_bottomright, transform_mode_smooth) 1172 1173 # Show filenames 1174 child.label_main_topleft.setText(filename_main_topleft) 1175 child.label_topright.setText(filename_topright) 1176 child.label_bottomright.setText(filename_bottomright) 1177 child.label_bottomleft.setText(filename_bottomleft) 1178 1179 child.show() 1180 1181 if activeMdiChild: 1182 if self._synchPanAct.isChecked(): 1183 self.synchPan(activeMdiChild) 1184 if self._synchZoomAct.isChecked(): 1185 self.synchZoom(activeMdiChild) 1186 1187 self._mdiArea.tile_what_was_done_last_time() 1188 1189 child.fitToWindow() 1190 child.set_close_pushbutton_always_visible(self.is_interface_showing) 1191 if self.scene_background_color is not None: 1192 child.set_scene_background_color(self.scene_background_color) 1193 1194 self.updateRecentFileSettings(filename_main_topleft) 1195 self.updateRecentFileActions() 1196 1197 self.display_loading_grayout(False) 1198 1199 self.statusBar().showMessage("File loaded", 2000) 1200 1201 def load_from_dragged_and_dropped_file(self, filename_main_topleft): 1202 """Load an individual image (convenience function — e.g., from a single emitted single filename).""" 1203 self.loadFile(filename_main_topleft) 1204 1205 def createMdiChild(self, pixmap, filename_main_topleft, pixmap_topright, pixmap_bottomleft, pixmap_bottomright, transform_mode_smooth): 1206 """Create new viewing widget for an individual image or sliding overlay to be placed in a new subwindow. 1207 1208 Args: 1209 pixmap (QPixmap): The main image to be viewed; the basis of the sliding overlay (main; topleft) 1210 filename_main_topleft (str): The image filepath of the main image. 1211 pixmap_topright (QPixmap): The top-right image of the sliding overlay (set None to exclude). 1212 pixmap_bottomleft (QPixmap): The bottom-left image of the sliding overlay (set None to exclude). 1213 pixmap_bottomright (QPixmap): The bottom-right image of the sliding overlay (set None to exclude). 1214 1215 Returns: 1216 child (SplitViewMdiChild): The viewing widget instance. 1217 """ 1218 1219 child = SplitViewMdiChild(pixmap, 1220 filename_main_topleft, 1221 "Window %d" % (len(self._mdiArea.subWindowList())+1), 1222 pixmap_topright, pixmap_bottomleft, pixmap_bottomright, 1223 transform_mode_smooth) 1224 1225 child.enableScrollBars(self._showScrollbarsAct.isChecked()) 1226 1227 self._mdiArea.addSubWindow(child, QtCore.Qt.FramelessWindowHint) # LVM: No frame, starts fitted 1228 1229 child.scrollChanged.connect(self.panChanged) 1230 child.transformChanged.connect(self.zoomChanged) 1231 1232 child.positionChanged.connect(self.on_positionChanged) 1233 child.tracker.mouse_leaved.connect(self.on_mouse_leaved) 1234 1235 child.scrollChanged.connect(self.on_scrollChanged) 1236 1237 child.became_closed.connect(self.on_subwindow_closed) 1238 child.was_clicked_close_pushbutton.connect(self._mdiArea.closeActiveSubWindow) 1239 child.shortcut_shift_x_was_activated.connect(self.shortcut_shift_x_was_activated_on_mdichild) 1240 child.signal_display_loading_grayout.connect(self.display_loading_grayout) 1241 child.was_set_global_transform_mode.connect(self.set_all_window_transform_mode_smooth) 1242 child.was_set_scene_background_color.connect(self.set_all_background_color) 1243 1244 return child 1245 1246 1247 # View and split methods 1248 1249 @QtCore.pyqtSlot() 1250 def on_create_splitview(self): 1251 """Load a sliding overlay using the filepaths of the current images in the sliding overlay creator.""" 1252 # Get filenames 1253 file_path_main_topleft = self._splitview_creator.drag_drop_area.app_main_topleft.file_path 1254 file_path_topright = self._splitview_creator.drag_drop_area.app_topright.file_path 1255 file_path_bottomleft = self._splitview_creator.drag_drop_area.app_bottomleft.file_path 1256 file_path_bottomright = self._splitview_creator.drag_drop_area.app_bottomright.file_path 1257 1258 # loadFile with those filenames 1259 self.loadFile(file_path_main_topleft, file_path_topright, file_path_bottomleft, file_path_bottomright) 1260 1261 def fit_to_window(self): 1262 """Fit the view of the active subwindow (if it exists).""" 1263 if self.activeMdiChild: 1264 self.activeMdiChild.fitToWindow() 1265 1266 def update_split(self): 1267 """Update the position of the split of the active subwindow (if it exists) relying on the global mouse coordinates.""" 1268 if self.activeMdiChild: 1269 self.activeMdiChild.update_split() # No input = Rely on global mouse position calculation 1270 1271 def lock_split(self): 1272 """Lock the position of the overlay split of active subwindow and set relevant interface elements.""" 1273 if self.activeMdiChild: 1274 self.activeMdiChild.split_locked = True 1275 self._splitview_manager.lock_split_pushbutton.setChecked(True) 1276 self.update_window_highlight(self._mdiArea.activeSubWindow()) 1277 1278 def unlock_split(self): 1279 """Unlock the position of the overlay split of active subwindow and set relevant interface elements.""" 1280 if self.activeMdiChild: 1281 self.activeMdiChild.split_locked = False 1282 self._splitview_manager.lock_split_pushbutton.setChecked(False) 1283 self.update_window_highlight(self._mdiArea.activeSubWindow()) 1284 1285 def set_split(self, x_percent=0.5, y_percent=0.5, apply_to_all=True, ignore_lock=False, percent_of_visible=False): 1286 """Set the position of the split of the active subwindow as percent of base image's resolution. 1287 1288 Args: 1289 x_percent (float): The position of the split as a proportion (0-1) of the base image's horizontal resolution. 1290 y_percent (float): The position of the split as a proportion (0-1) of the base image's vertical resolution. 1291 apply_to_all (bool): True to set all subwindow splits; False to set only the active subwindow. 1292 ignore_lock (bool): True to ignore the lock status of the split; False to adhere. 1293 percent_of_visible (bool): True to set split as proportion of visible area; False as proportion of the full image resolution. 1294 """ 1295 if self.activeMdiChild: 1296 self.activeMdiChild.set_split(x_percent, y_percent, ignore_lock=ignore_lock, percent_of_visible=percent_of_visible) 1297 if apply_to_all: 1298 windows = self._mdiArea.subWindowList() 1299 for window in windows: 1300 window.widget().set_split(x_percent, y_percent, ignore_lock=ignore_lock, percent_of_visible=percent_of_visible) 1301 self.update_window_highlight(self._mdiArea.activeSubWindow()) 1302 1303 def set_split_from_slider(self): 1304 """Set the position of the split of the active subwindow to the center of the visible area of the sliding overlay (convenience function).""" 1305 self.set_split(x_percent=0.5, y_percent=0.5, apply_to_all=False, ignore_lock=False, percent_of_visible=True) 1306 1307 def set_split_from_manager(self, x_percent, y_percent): 1308 """Set the position of the split of the active subwindow as percent of base image's resolution (convenience function). 1309 1310 Args: 1311 x_percent (float): The position of the split as a proportion of the base image's horizontal resolution (0-1). 1312 y_percent (float): The position of the split as a proportion of the base image's vertical resolution (0-1). 1313 """ 1314 self.set_split(x_percent, y_percent, apply_to_all=False, ignore_lock=False) 1315 1316 def set_and_lock_split_from_manager(self, x_percent, y_percent): 1317 """Set and lock the position of the split of the active subwindow as percent of base image's resolution (convenience function). 1318 1319 Args: 1320 x_percent (float): The position of the split as a proportion of the base image's horizontal resolution (0-1). 1321 y_percent (float): The position of the split as a proportion of the base image's vertical resolution (0-1). 1322 """ 1323 self.set_split(x_percent, y_percent, apply_to_all=False, ignore_lock=True) 1324 self.lock_split() 1325 1326 def shortcut_shift_x_was_activated_on_mdichild(self): 1327 """Update interface button for split lock based on lock status of active subwindow.""" 1328 self._splitview_manager.lock_split_pushbutton.setChecked(self.activeMdiChild.split_locked) 1329 1330 @QtCore.pyqtSlot() 1331 def on_scrollChanged(self): 1332 """Refresh position of split of all subwindows based on their respective last position.""" 1333 windows = self._mdiArea.subWindowList() 1334 for window in windows: 1335 window.widget().refresh_split_based_on_last_updated_point_of_split_on_scene_main() 1336 1337 def on_subwindow_closed(self): 1338 """Record that a subwindow was closed upon the closing of a subwindow.""" 1339 self.subwindow_was_just_closed = True 1340 1341 @QtCore.pyqtSlot() 1342 def on_mouse_leaved(self): 1343 """Update displayed coordinates of mouse as N/A upon the mouse leaving the subwindow area.""" 1344 self._label_mouse.setText("View pixel coordinates: ( N/A , N/A )") 1345 self._label_mouse.adjustSize() 1346 1347 @QtCore.pyqtSlot(QtCore.QPoint) 1348 def on_positionChanged(self, pos): 1349 """Update displayed coordinates of mouse on the active subwindow using global coordinates.""" 1350 1351 point_of_mouse_on_viewport = QtCore.QPointF(pos.x(), pos.y()) 1352 pos_qcursor_global = QtGui.QCursor.pos() 1353 1354 if self.activeMdiChild: 1355 1356 # Use mouse position to grab scene coordinates (activeMdiChild?) 1357 active_view = self.activeMdiChild._view_main_topleft 1358 point_of_mouse_on_scene = active_view.mapToScene(point_of_mouse_on_viewport.x(), point_of_mouse_on_viewport.y()) 1359 1360 if not self._label_mouse.isVisible(): 1361 self._label_mouse.show() 1362 self._label_mouse.setText("View pixel coordinates: ( x = %d , y = %d )" % (point_of_mouse_on_scene.x(), point_of_mouse_on_scene.y())) 1363 1364 pos_qcursor_view = active_view.mapFromGlobal(pos_qcursor_global) 1365 pos_qcursor_scene = active_view.mapToScene(pos_qcursor_view) 1366 # print("Cursor coords scene: ( %d , %d )" % (pos_qcursor_scene.x(), pos_qcursor_scene.y())) 1367 1368 else: 1369 1370 self._label_mouse.setText("View pixel coordinates: ( N/A , N/A )") 1371 1372 self._label_mouse.adjustSize() 1373 1374 1375 # Transparency methods 1376 1377 1378 @QtCore.pyqtSlot(int) 1379 def on_slider_opacity_base_changed(self, value): 1380 """Set transparency of base of sliding overlay of active subwindow. 1381 1382 Triggered upon change in interface transparency slider. 1383 Temporarily sets position of split to the center of the visible area to give user a preview of the transparency effect. 1384 1385 Args: 1386 value (float,int): The transparency as percent opacity, where 100 is opaque (not transparent) and 0 is transparent (0-100). 1387 """ 1388 if not self.activeMdiChild: 1389 return 1390 if not self.activeMdiChild.split_locked: 1391 self.set_split_from_slider() 1392 self.activeMdiChild.set_opacity_base(value) 1393 1394 @QtCore.pyqtSlot(int) 1395 def on_slider_opacity_topright_changed(self, value): 1396 """Set transparency of top-right of sliding overlay of active subwindow. 1397 1398 Triggered upon change in interface transparency slider. 1399 Temporarily sets position of split to the center of the visible area to give user a preview of the transparency effect. 1400 1401 Args: 1402 value (float,int): The transparency as percent opacity, where 100 is opaque (not transparent) and 0 is transparent (0-100). 1403 """ 1404 if not self.activeMdiChild: 1405 return 1406 if not self.activeMdiChild.split_locked: 1407 self.set_split_from_slider() 1408 self.activeMdiChild.set_opacity_topright(value) 1409 1410 @QtCore.pyqtSlot(int) 1411 def on_slider_opacity_bottomright_changed(self, value): 1412 """Set transparency of bottom-right of sliding overlay of active subwindow. 1413 1414 Triggered upon change in interface transparency slider. 1415 Temporarily sets position of split to the center of the visible area to give user a preview of the transparency effect. 1416 1417 Args: 1418 value (float,int): The transparency as percent opacity, where 100 is opaque (not transparent) and 0 is transparent (0-100). 1419 """ 1420 if not self.activeMdiChild: 1421 return 1422 if not self.activeMdiChild.split_locked: 1423 self.set_split_from_slider() 1424 self.activeMdiChild.set_opacity_bottomright(value) 1425 1426 @QtCore.pyqtSlot(int) 1427 def on_slider_opacity_bottomleft_changed(self, value): 1428 """Set transparency of bottom-left of sliding overlay of active subwindow. 1429 1430 Triggered upon change in interface transparency slider. 1431 Temporarily sets position of split to the center of the visible area to give user a preview of the transparency effect. 1432 1433 Args: 1434 value (float,int): The transparency as percent opacity, where 100 is opaque (not transparent) and 0 is transparent (0-100). 1435 """ 1436 if not self.activeMdiChild: 1437 return 1438 if not self.activeMdiChild.split_locked: 1439 self.set_split_from_slider() 1440 self.activeMdiChild.set_opacity_bottomleft(value) 1441 1442 def update_sliders(self, window): 1443 """Update interface transparency sliders upon subwindow activating using the subwindow transparency values. 1444 1445 Args: 1446 window (QMdiSubWindow): The active subwindow. 1447 """ 1448 if window is None: 1449 self._sliders_opacity_splitviews.reset_sliders() 1450 return 1451 1452 child = self.activeMdiChild 1453 1454 self._sliders_opacity_splitviews.set_enabled(True, child.pixmap_topright_exists, child.pixmap_bottomright_exists, child.pixmap_bottomleft_exists) 1455 1456 opacity_base_of_activeMdiChild = child._opacity_base 1457 opacity_topright_of_activeMdiChild = child._opacity_topright 1458 opacity_bottomright_of_activeMdiChild = child._opacity_bottomright 1459 opacity_bottomleft_of_activeMdiChild = child._opacity_bottomleft 1460 1461 self._sliders_opacity_splitviews.update_sliders(opacity_base_of_activeMdiChild, opacity_topright_of_activeMdiChild, opacity_bottomright_of_activeMdiChild, opacity_bottomleft_of_activeMdiChild) 1462 1463 1464 # [Legacy methods from derived MDI Image Viewer] 1465 1466 def createMappedAction(self, icon, text, parent, shortcut, methodName): 1467 """Create |QAction| that is mapped via methodName to call. 1468 1469 :param icon: icon associated with |QAction| 1470 :type icon: |QIcon| or None 1471 :param str text: the |QAction| descriptive text 1472 :param QObject parent: the parent |QObject| 1473 :param QKeySequence shortcut: the shortcut |QKeySequence| 1474 :param str methodName: name of method to call when |QAction| is 1475 triggered 1476 :rtype: |QAction|""" 1477 1478 if icon is not None: 1479 action = QtWidgets.QAction(icon, text, parent, 1480 shortcut=shortcut, 1481 triggered=self._actionMapper.map) 1482 else: 1483 action = QtWidgets.QAction(text, parent, 1484 shortcut=shortcut, 1485 triggered=self._actionMapper.map) 1486 self._actionMapper.setMapping(action, methodName) 1487 return action 1488 1489 def createActions(self): 1490 """Create actions used in menus.""" 1491 #File menu actions 1492 self._openAct = QtWidgets.QAction( 1493 "&Open...", self, 1494 shortcut=QtGui.QKeySequence.Open, 1495 statusTip="Open an existing file", 1496 triggered=self.open) 1497 1498 self._switchLayoutDirectionAct = QtWidgets.QAction( 1499 "Switch &layout direction", self, 1500 triggered=self.switchLayoutDirection) 1501 1502 #create dummy recent file actions 1503 for i in range(MultiViewMainWindow.MaxRecentFiles): 1504 self._recentFileActions.append( 1505 QtWidgets.QAction(self, visible=False, 1506 triggered=self._recentFileMapper.map)) 1507 1508 self._exitAct = QtWidgets.QAction( 1509 "E&xit", self, 1510 shortcut=QtGui.QKeySequence.Quit, 1511 statusTip="Exit the application", 1512 triggered=QtWidgets.QApplication.closeAllWindows) 1513 1514 #View menu actions 1515 self._showScrollbarsAct = QtWidgets.QAction( 1516 "&Scrollbars", self, 1517 checkable=True, 1518 statusTip="Toggle display of subwindow scrollbars", 1519 triggered=self.toggleScrollbars) 1520 1521 self._showStatusbarAct = QtWidgets.QAction( 1522 "S&tatusbar", self, 1523 checkable=True, 1524 statusTip="Toggle display of statusbar", 1525 triggered=self.toggleStatusbar) 1526 1527 self._synchZoomAct = QtWidgets.QAction( 1528 "Synch &Zoom", self, 1529 checkable=True, 1530 statusTip="Synch zooming of subwindows", 1531 triggered=self.toggleSynchZoom) 1532 1533 self._synchPanAct = QtWidgets.QAction( 1534 "Synch &Pan", self, 1535 checkable=True, 1536 statusTip="Synch panning of subwindows", 1537 triggered=self.toggleSynchPan) 1538 1539 #Scroll menu actions 1540 self._scrollActions = [ 1541 self.createMappedAction( 1542 None, 1543 "&Top", self, 1544 QtGui.QKeySequence.MoveToStartOfDocument, 1545 "scrollToTop"), 1546 1547 self.createMappedAction( 1548 None, 1549 "&Bottom", self, 1550 QtGui.QKeySequence.MoveToEndOfDocument, 1551 "scrollToBottom"), 1552 1553 self.createMappedAction( 1554 None, 1555 "&Left Edge", self, 1556 QtGui.QKeySequence.MoveToStartOfLine, 1557 "scrollToBegin"), 1558 1559 self.createMappedAction( 1560 None, 1561 "&Right Edge", self, 1562 QtGui.QKeySequence.MoveToEndOfLine, 1563 "scrollToEnd"), 1564 1565 self.createMappedAction( 1566 None, 1567 "&Center", self, 1568 "5", 1569 "centerView"), 1570 ] 1571 1572 #zoom menu actions 1573 separatorAct = QtWidgets.QAction(self) 1574 separatorAct.setSeparator(True) 1575 1576 self._zoomActions = [ 1577 self.createMappedAction( 1578 None, 1579 "Zoo&m In (25%)", self, 1580 QtGui.QKeySequence.ZoomIn, 1581 "zoomIn"), 1582 1583 self.createMappedAction( 1584 None, 1585 "Zoom &Out (25%)", self, 1586 QtGui.QKeySequence.ZoomOut, 1587 "zoomOut"), 1588 1589 #self.createMappedAction( 1590 #None, 1591 #"&Zoom To...", self, 1592 #"Z", 1593 #"zoomTo"), 1594 1595 separatorAct, 1596 1597 self.createMappedAction( 1598 None, 1599 "Actual &Size", self, 1600 "/", 1601 "actualSize"), 1602 1603 self.createMappedAction( 1604 None, 1605 "Fit &Image", self, 1606 "*", 1607 "fitToWindow"), 1608 1609 self.createMappedAction( 1610 None, 1611 "Fit &Width", self, 1612 "Alt+Right", 1613 "fitWidth"), 1614 1615 self.createMappedAction( 1616 None, 1617 "Fit &Height", self, 1618 "Alt+Down", 1619 "fitHeight"), 1620 ] 1621 1622 #Window menu actions 1623 self._activateSubWindowSystemMenuAct = QtWidgets.QAction( 1624 "Activate &System Menu", self, 1625 shortcut="Ctrl+ ", 1626 statusTip="Activate subwindow System Menu", 1627 triggered=self.activateSubwindowSystemMenu) 1628 1629 self._closeAct = QtWidgets.QAction( 1630 "Cl&ose", self, 1631 shortcut=QtGui.QKeySequence.Close, 1632 shortcutContext=QtCore.Qt.WidgetShortcut, 1633 #shortcut="Ctrl+Alt+F4", 1634 statusTip="Close the active window", 1635 triggered=self._mdiArea.closeActiveSubWindow) 1636 1637 self._closeAllAct = QtWidgets.QAction( 1638 "Close &All", self, 1639 statusTip="Close all the windows", 1640 triggered=self._mdiArea.closeAllSubWindows) 1641 1642 self._tileAct = QtWidgets.QAction( 1643 "&Tile", self, 1644 statusTip="Tile the windows", 1645 triggered=self._mdiArea.tileSubWindows) 1646 1647 self._tileAct.triggered.connect(self.tile_and_fit_mdiArea) 1648 1649 self._cascadeAct = QtWidgets.QAction( 1650 "&Cascade", self, 1651 statusTip="Cascade the windows", 1652 triggered=self._mdiArea.cascadeSubWindows) 1653 1654 self._nextAct = QtWidgets.QAction( 1655 "Ne&xt", self, 1656 shortcut=QtGui.QKeySequence.NextChild, 1657 statusTip="Move the focus to the next window", 1658 triggered=self._mdiArea.activateNextSubWindow) 1659 1660 self._previousAct = QtWidgets.QAction( 1661 "Pre&vious", self, 1662 shortcut=QtGui.QKeySequence.PreviousChild, 1663 statusTip="Move the focus to the previous window", 1664 triggered=self._mdiArea.activatePreviousSubWindow) 1665 1666 self._separatorAct = QtWidgets.QAction(self) 1667 self._separatorAct.setSeparator(True) 1668 1669 self._aboutAct = QtWidgets.QAction( 1670 "&About", self, 1671 statusTip="Show the application's About box", 1672 triggered=self.about) 1673 1674 self._aboutQtAct = QtWidgets.QAction( 1675 "About &Qt", self, 1676 statusTip="Show the Qt library's About box", 1677 triggered=QtWidgets.QApplication.aboutQt) 1678 1679 def createMenus(self): 1680 """Create menus.""" 1681 self._fileMenu = self.menuBar().addMenu("&File") 1682 self._fileMenu.addAction(self._openAct) 1683 self._fileMenu.addAction(self._switchLayoutDirectionAct) 1684 1685 self._fileSeparatorAct = self._fileMenu.addSeparator() 1686 for action in self._recentFileActions: 1687 self._fileMenu.addAction(action) 1688 self.updateRecentFileActions() 1689 self._fileMenu.addSeparator() 1690 self._fileMenu.addAction(self._exitAct) 1691 1692 self._viewMenu = self.menuBar().addMenu("&View") 1693 self._viewMenu.addAction(self._showScrollbarsAct) 1694 self._viewMenu.addAction(self._showStatusbarAct) 1695 self._viewMenu.addSeparator() 1696 self._viewMenu.addAction(self._synchZoomAct) 1697 self._viewMenu.addAction(self._synchPanAct) 1698 1699 self._scrollMenu = self.menuBar().addMenu("&Scroll") 1700 [self._scrollMenu.addAction(action) for action in self._scrollActions] 1701 1702 self._zoomMenu = self.menuBar().addMenu("&Zoom") 1703 [self._zoomMenu.addAction(action) for action in self._zoomActions] 1704 1705 self._windowMenu = self.menuBar().addMenu("&Window") 1706 self.updateWindowMenu() 1707 self._windowMenu.aboutToShow.connect(self.updateWindowMenu) 1708 1709 self.menuBar().addSeparator() 1710 1711 self._helpMenu = self.menuBar().addMenu("&Help") 1712 self._helpMenu.addAction(self._aboutAct) 1713 self._helpMenu.addAction(self._aboutQtAct) 1714 1715 def updateMenus(self): 1716 """Update menus.""" 1717 hasMdiChild = (self.activeMdiChild is not None) 1718 1719 self._scrollMenu.setEnabled(hasMdiChild) 1720 self._zoomMenu.setEnabled(hasMdiChild) 1721 1722 self._closeAct.setEnabled(hasMdiChild) 1723 self._closeAllAct.setEnabled(hasMdiChild) 1724 1725 self._tileAct.setEnabled(hasMdiChild) 1726 self._cascadeAct.setEnabled(hasMdiChild) 1727 self._nextAct.setEnabled(hasMdiChild) 1728 self._previousAct.setEnabled(hasMdiChild) 1729 self._separatorAct.setVisible(hasMdiChild) 1730 1731 def updateRecentFileActions(self): 1732 """Update recent file menu items.""" 1733 settings = QtCore.QSettings() 1734 files = settings.value(SETTING_RECENTFILELIST) 1735 numRecentFiles = min(len(files) if files else 0, 1736 MultiViewMainWindow.MaxRecentFiles) 1737 1738 for i in range(numRecentFiles): 1739 text = "&%d %s" % (i + 1, strippedName(files[i])) 1740 self._recentFileActions[i].setText(text) 1741 self._recentFileActions[i].setData(files[i]) 1742 self._recentFileActions[i].setVisible(True) 1743 self._recentFileMapper.setMapping(self._recentFileActions[i], 1744 files[i]) 1745 1746 for j in range(numRecentFiles, MultiViewMainWindow.MaxRecentFiles): 1747 self._recentFileActions[j].setVisible(False) 1748 1749 self._fileSeparatorAct.setVisible((numRecentFiles > 0)) 1750 1751 def updateWindowMenu(self): 1752 """Update the Window menu.""" 1753 self._windowMenu.clear() 1754 self._windowMenu.addAction(self._closeAct) 1755 self._windowMenu.addAction(self._closeAllAct) 1756 self._windowMenu.addSeparator() 1757 self._windowMenu.addAction(self._tileAct) 1758 self._windowMenu.addAction(self._cascadeAct) 1759 self._windowMenu.addSeparator() 1760 self._windowMenu.addAction(self._nextAct) 1761 self._windowMenu.addAction(self._previousAct) 1762 self._windowMenu.addAction(self._separatorAct) 1763 1764 windows = self._mdiArea.subWindowList() 1765 self._separatorAct.setVisible(len(windows) != 0) 1766 1767 for i, window in enumerate(windows): 1768 child = window.widget() 1769 1770 text = "%d %s" % (i + 1, child.userFriendlyCurrentFile) 1771 if i < 9: 1772 text = '&' + text 1773 1774 action = self._windowMenu.addAction(text) 1775 action.setCheckable(True) 1776 action.setChecked(child == self.activeMdiChild) 1777 action.triggered.connect(self._windowMapper.map) 1778 self._windowMapper.setMapping(action, window) 1779 1780 def createStatusBarLabel(self, stretch=0): 1781 """Create status bar label. 1782 1783 :param int stretch: stretch factor 1784 :rtype: |QLabel|""" 1785 label = QtWidgets.QLabel() 1786 label.setFrameStyle(QtWidgets.QFrame.Panel | QtWidgets.QFrame.Sunken) 1787 label.setLineWidth(2) 1788 self.statusBar().addWidget(label, stretch) 1789 return label 1790 1791 def createStatusBar(self): 1792 """Create status bar.""" 1793 statusBar = self.statusBar() 1794 1795 self._sbLabelName = self.createStatusBarLabel(1) 1796 self._sbLabelSize = self.createStatusBarLabel() 1797 self._sbLabelDimensions = self.createStatusBarLabel() 1798 self._sbLabelDate = self.createStatusBarLabel() 1799 self._sbLabelZoom = self.createStatusBarLabel() 1800 1801 statusBar.showMessage("Ready") 1802 1803 1804 @property 1805 def activeMdiChild(self): 1806 """Get active MDI child (:class:`SplitViewMdiChild` or *None*).""" 1807 activeSubWindow = self._mdiArea.activeSubWindow() 1808 if activeSubWindow: 1809 return activeSubWindow.widget() 1810 return None 1811 1812 1813 def closeEvent(self, event): 1814 """Overrides close event to save application settings. 1815 1816 :param QEvent event: instance of |QEvent|""" 1817 1818 if self.is_fullscreen: # Needed to properly close the image viewer if the main window is closed while the viewer is fullscreen 1819 self.is_fullscreen = False 1820 self.setCentralWidget(self.mdiarea_plus_buttons) 1821 1822 self._mdiArea.closeAllSubWindows() 1823 if self.activeMdiChild: 1824 event.ignore() 1825 else: 1826 self.writeSettings() 1827 event.accept() 1828 1829 1830 def tile_and_fit_mdiArea(self): 1831 self._mdiArea.tileSubWindows() 1832 1833 1834 # Synchronized pan and zoom methods 1835 1836 @QtCore.pyqtSlot(str) 1837 def mappedImageViewerAction(self, methodName): 1838 """Perform action mapped to :class:`aux_splitview.SplitView` 1839 methodName. 1840 1841 :param str methodName: method to call""" 1842 activeViewer = self.activeMdiChild 1843 if hasattr(activeViewer, str(methodName)): 1844 getattr(activeViewer, str(methodName))() 1845 1846 @QtCore.pyqtSlot() 1847 def toggleSynchPan(self): 1848 """Toggle synchronized subwindow panning.""" 1849 if self._synchPanAct.isChecked(): 1850 self.synchPan(self.activeMdiChild) 1851 1852 @QtCore.pyqtSlot() 1853 def panChanged(self): 1854 """Synchronize subwindow pans.""" 1855 mdiChild = self.sender() 1856 while mdiChild is not None and type(mdiChild) != SplitViewMdiChild: 1857 mdiChild = mdiChild.parent() 1858 if mdiChild and self._synchPanAct.isChecked(): 1859 self.synchPan(mdiChild) 1860 1861 @QtCore.pyqtSlot() 1862 def toggleSynchZoom(self): 1863 """Toggle synchronized subwindow zooming.""" 1864 if self._synchZoomAct.isChecked(): 1865 self.synchZoom(self.activeMdiChild) 1866 1867 @QtCore.pyqtSlot() 1868 def zoomChanged(self): 1869 """Synchronize subwindow zooms.""" 1870 mdiChild = self.sender() 1871 if self._synchZoomAct.isChecked(): 1872 self.synchZoom(mdiChild) 1873 self.updateStatusBar() 1874 1875 def synchPan(self, fromViewer): 1876 """Synch panning of all subwindowws to the same as *fromViewer*. 1877 1878 :param fromViewer: :class:`SplitViewMdiChild` that initiated synching""" 1879 1880 assert isinstance(fromViewer, SplitViewMdiChild) 1881 if not fromViewer: 1882 return 1883 if self._handlingScrollChangedSignal: 1884 return 1885 if fromViewer.parent() != self._mdiArea.activeSubWindow(): # Prevent circular scroll state change signals from propagating 1886 if fromViewer.parent() != self: 1887 return 1888 self._handlingScrollChangedSignal = True 1889 1890 newState = fromViewer.scrollState 1891 changedWindow = fromViewer.parent() 1892 windows = self._mdiArea.subWindowList() 1893 for window in windows: 1894 if window != changedWindow: 1895 window.widget().scrollState = newState 1896 window.widget().resize_scene() 1897 1898 self._handlingScrollChangedSignal = False 1899 1900 def synchZoom(self, fromViewer): 1901 """Synch zoom of all subwindowws to the same as *fromViewer*. 1902 1903 :param fromViewer: :class:`SplitViewMdiChild` that initiated synching""" 1904 if not fromViewer: 1905 return 1906 newZoomFactor = fromViewer.zoomFactor 1907 changedWindow = fromViewer.parent() 1908 windows = self._mdiArea.subWindowList() 1909 for window in windows: 1910 if window != changedWindow: 1911 window.widget().zoomFactor = newZoomFactor 1912 window.widget().resize_scene() 1913 self.refreshPan() 1914 1915 def refreshPan(self): 1916 if self.activeMdiChild: 1917 self.synchPan(self.activeMdiChild) 1918 1919 def refreshPanDelayed(self, ms=0): 1920 QtCore.QTimer.singleShot(ms, self.refreshPan) 1921 1922 1923 # Methods from PyQt MDI Image Viewer left unaltered 1924 1925 @QtCore.pyqtSlot() 1926 def activateSubwindowSystemMenu(self): 1927 """Activate current subwindow's System Menu.""" 1928 activeSubWindow = self._mdiArea.activeSubWindow() 1929 if activeSubWindow: 1930 activeSubWindow.showSystemMenu() 1931 1932 @QtCore.pyqtSlot(str) 1933 def openRecentFile(self, filename_main_topleft): 1934 """Open a recent file. 1935 1936 :param str filename_main_topleft: filename_main_topleft to view""" 1937 self.loadFile(filename_main_topleft, None, None, None) 1938 1939 @QtCore.pyqtSlot() 1940 def open(self): 1941 """Handle the open action.""" 1942 fileDialog = QtWidgets.QFileDialog(self) 1943 settings = QtCore.QSettings() 1944 fileDialog.setNameFilters(["Image Files (*.jpg *.png *.tif)", 1945 "All Files (*)"]) 1946 if not settings.contains(SETTING_FILEOPEN + "/state"): 1947 fileDialog.setDirectory(".") 1948 else: 1949 self.restoreDialogState(fileDialog, SETTING_FILEOPEN) 1950 fileDialog.setFileMode(QtWidgets.QFileDialog.ExistingFile) 1951 if not fileDialog.exec_(): 1952 return 1953 self.saveDialogState(fileDialog, SETTING_FILEOPEN) 1954 1955 filename_main_topleft = fileDialog.selectedFiles()[0] 1956 self.loadFile(filename_main_topleft, None, None, None) 1957 1958 1959 @QtCore.pyqtSlot() 1960 def toggleScrollbars(self): 1961 """Toggle subwindow scrollbar visibility.""" 1962 checked = self._showScrollbarsAct.isChecked() 1963 1964 windows = self._mdiArea.subWindowList() 1965 for window in windows: 1966 child = window.widget() 1967 child.enableScrollBars(checked) 1968 1969 @QtCore.pyqtSlot() 1970 def toggleStatusbar(self): 1971 """Toggle status bar visibility.""" 1972 self.statusBar().setVisible(self._showStatusbarAct.isChecked()) 1973 1974 1975 @QtCore.pyqtSlot() 1976 def about(self): 1977 """Display About dialog box.""" 1978 QtWidgets.QMessageBox.about(self, "About MDI", 1979 "<b>MDI Image Viewer</b> demonstrates how to" 1980 "synchronize the panning and zooming of multiple image" 1981 "viewer windows using Qt.") 1982 @QtCore.pyqtSlot(QtWidgets.QMdiSubWindow) 1983 def subWindowActivated(self, window): 1984 """Handle |QMdiSubWindow| activated signal. 1985 1986 :param |QMdiSubWindow| window: |QMdiSubWindow| that was just 1987 activated""" 1988 self.updateStatusBar() 1989 1990 @QtCore.pyqtSlot(QtWidgets.QMdiSubWindow) 1991 def setActiveSubWindow(self, window): 1992 """Set active |QMdiSubWindow|. 1993 1994 :param |QMdiSubWindow| window: |QMdiSubWindow| to activate """ 1995 if window: 1996 self._mdiArea.setActiveSubWindow(window) 1997 1998 1999 def updateStatusBar(self): 2000 """Update status bar.""" 2001 self.statusBar().setVisible(self._showStatusbarAct.isChecked()) 2002 imageViewer = self.activeMdiChild 2003 if not imageViewer: 2004 self._sbLabelName.setText("") 2005 self._sbLabelSize.setText("") 2006 self._sbLabelDimensions.setText("") 2007 self._sbLabelDate.setText("") 2008 self._sbLabelZoom.setText("") 2009 2010 self._sbLabelSize.hide() 2011 self._sbLabelDimensions.hide() 2012 self._sbLabelDate.hide() 2013 self._sbLabelZoom.hide() 2014 return 2015 2016 filename_main_topleft = imageViewer.currentFile 2017 self._sbLabelName.setText(" %s " % filename_main_topleft) 2018 2019 fi = QtCore.QFileInfo(filename_main_topleft) 2020 size = fi.size() 2021 fmt = " %.1f %s " 2022 if size > 1024*1024*1024: 2023 unit = "MB" 2024 size /= 1024*1024*1024 2025 elif size > 1024*1024: 2026 unit = "MB" 2027 size /= 1024*1024 2028 elif size > 1024: 2029 unit = "KB" 2030 size /= 1024 2031 else: 2032 unit = "Bytes" 2033 fmt = " %d %s " 2034 self._sbLabelSize.setText(fmt % (size, unit)) 2035 2036 pixmap = imageViewer.pixmap_main_topleft 2037 self._sbLabelDimensions.setText(" %dx%dx%d " % 2038 (pixmap.width(), 2039 pixmap.height(), 2040 pixmap.depth())) 2041 2042 self._sbLabelDate.setText( 2043 " %s " % 2044 fi.lastModified().toString(QtCore.Qt.SystemLocaleShortDate)) 2045 self._sbLabelZoom.setText(" %0.f%% " % (imageViewer.zoomFactor*100,)) 2046 2047 self._sbLabelSize.show() 2048 self._sbLabelDimensions.show() 2049 self._sbLabelDate.show() 2050 self._sbLabelZoom.show() 2051 2052 def switchLayoutDirection(self): 2053 """Switch MDI subwindow layout direction.""" 2054 if self.layoutDirection() == QtCore.Qt.LeftToRight: 2055 QtWidgets.QApplication.setLayoutDirection(QtCore.Qt.RightToLeft) 2056 else: 2057 QtWidgets.QApplication.setLayoutDirection(QtCore.Qt.LeftToRight) 2058 2059 def saveDialogState(self, dialog, groupName): 2060 """Save dialog state, position & size. 2061 2062 :param |QDialog| dialog: dialog to save state of 2063 :param str groupName: |QSettings| group name""" 2064 assert isinstance(dialog, QtWidgets.QDialog) 2065 2066 settings = QtCore.QSettings() 2067 settings.beginGroup(groupName) 2068 2069 settings.setValue('state', dialog.saveState()) 2070 settings.setValue('geometry', dialog.saveGeometry()) 2071 settings.setValue('filter', dialog.selectedNameFilter()) 2072 2073 settings.endGroup() 2074 2075 def restoreDialogState(self, dialog, groupName): 2076 """Restore dialog state, position & size. 2077 2078 :param str groupName: |QSettings| group name""" 2079 assert isinstance(dialog, QtWidgets.QDialog) 2080 2081 settings = QtCore.QSettings() 2082 settings.beginGroup(groupName) 2083 2084 dialog.restoreState(settings.value('state')) 2085 dialog.restoreGeometry(settings.value('geometry')) 2086 dialog.selectNameFilter(settings.value('filter', "")) 2087 2088 settings.endGroup() 2089 2090 def writeSettings(self): 2091 """Write application settings.""" 2092 settings = QtCore.QSettings() 2093 settings.setValue('pos', self.pos()) 2094 settings.setValue('size', self.size()) 2095 settings.setValue('windowgeometry', self.saveGeometry()) 2096 settings.setValue('windowstate', self.saveState()) 2097 2098 settings.setValue(SETTING_SCROLLBARS, 2099 self._showScrollbarsAct.isChecked()) 2100 settings.setValue(SETTING_STATUSBAR, 2101 self._showStatusbarAct.isChecked()) 2102 settings.setValue(SETTING_SYNCHZOOM, 2103 self._synchZoomAct.isChecked()) 2104 settings.setValue(SETTING_SYNCHPAN, 2105 self._synchPanAct.isChecked()) 2106 2107 def readSettings(self): 2108 """Read application settings.""" 2109 2110 scrollbars_always_checked_off_at_startup = True 2111 statusbar_always_checked_off_at_startup = True 2112 sync_always_checked_on_at_startup = True 2113 2114 settings = QtCore.QSettings() 2115 2116 pos = settings.value('pos', QtCore.QPoint(100, 100)) 2117 size = settings.value('size', QtCore.QSize(1100, 600)) 2118 self.move(pos) 2119 self.resize(size) 2120 2121 if settings.contains('windowgeometry'): 2122 self.restoreGeometry(settings.value('windowgeometry')) 2123 if settings.contains('windowstate'): 2124 self.restoreState(settings.value('windowstate')) 2125 2126 2127 if scrollbars_always_checked_off_at_startup: 2128 self._showScrollbarsAct.setChecked(False) 2129 else: 2130 self._showScrollbarsAct.setChecked( 2131 toBool(settings.value(SETTING_SCROLLBARS, False))) 2132 2133 if statusbar_always_checked_off_at_startup: 2134 self._showStatusbarAct.setChecked(False) 2135 else: 2136 self._showStatusbarAct.setChecked( 2137 toBool(settings.value(SETTING_STATUSBAR, False))) 2138 2139 if sync_always_checked_on_at_startup: 2140 self._synchZoomAct.setChecked(True) 2141 self._synchPanAct.setChecked(True) 2142 else: 2143 self._synchZoomAct.setChecked( 2144 toBool(settings.value(SETTING_SYNCHZOOM, False))) 2145 self._synchPanAct.setChecked( 2146 toBool(settings.value(SETTING_SYNCHPAN, False))) 2147 2148 def updateRecentFileSettings(self, filename_main_topleft, delete=False): 2149 """Update recent file list setting. 2150 2151 :param str filename_main_topleft: filename_main_topleft to add or remove from recent file 2152 list 2153 :param bool delete: if True then filename_main_topleft removed, otherwise added""" 2154 settings = QtCore.QSettings() 2155 files = list(settings.value(SETTING_RECENTFILELIST, [])) 2156 2157 try: 2158 files.remove(filename_main_topleft) 2159 except ValueError: 2160 pass 2161 2162 if not delete: 2163 files.insert(0, filename_main_topleft) 2164 del files[MultiViewMainWindow.MaxRecentFiles:] 2165 2166 settings.setValue(SETTING_RECENTFILELIST, files) 2167 2168 2169 2170def main(): 2171 """Run MultiViewMainWindow as main app. 2172 2173 Attributes: 2174 app (QApplication): Starts and holds the main event loop of application. 2175 mainWin (MultiViewMainWindow): The main window. 2176 """ 2177 import sys 2178 2179 app = QtWidgets.QApplication(sys.argv) 2180 QtCore.QSettings.setDefaultFormat(QtCore.QSettings.IniFormat) 2181 app.setOrganizationName(COMPANY) 2182 app.setOrganizationDomain(DOMAIN) 2183 app.setApplicationName(APPNAME) 2184 app.setWindowIcon(QtGui.QIcon(":/icon.png")) 2185 2186 mainWin = MultiViewMainWindow() 2187 mainWin.setWindowTitle(APPNAME) 2188 2189 mainWin.show() 2190 2191 sys.exit(app.exec_()) 2192 2193if __name__ == '__main__': 2194 main()
65class SplitViewMdiChild(SplitView): 66 """Extends SplitView for use in Butterfly Viewer. 67 68 Extends SplitView with keyboard shortcut to lock the position of the split 69 in the Butterfly Viewer. 70 71 Overrides SplitView by checking split lock status before updating split. 72 73 Args: 74 See parent method for full documentation. 75 """ 76 77 shortcut_shift_x_was_activated = QtCore.pyqtSignal() 78 79 def __init__(self, pixmap, filename_main_topleft, name, pixmap_topright, pixmap_bottomleft, pixmap_bottomright, transform_mode_smooth): 80 super().__init__(pixmap, filename_main_topleft, name, pixmap_topright, pixmap_bottomleft, pixmap_bottomright, transform_mode_smooth) 81 82 self.setAttribute(QtCore.Qt.WA_DeleteOnClose) 83 self._isUntitled = True 84 85 self.toggle_lock_split_shortcut = QtWidgets.QShortcut(QtGui.QKeySequence("Shift+X"), self) 86 self.toggle_lock_split_shortcut.activated.connect(self.toggle_lock_split) 87 88 89 # Control the split of the sliding overlay 90 91 def toggle_lock_split(self): 92 """Toggle the split lock. 93 94 Toggles the status of the split lock (e.g., if locked, it will become unlocked; vice versa). 95 """ 96 self.split_locked = not self.split_locked 97 self.shortcut_shift_x_was_activated.emit() 98 99 def update_split(self, pos = None, pos_is_global=False, ignore_lock=False): 100 """Update the position of the split while considering the status of the split lock. 101 102 See parent method for full documentation. 103 """ 104 if not self.split_locked or ignore_lock: 105 super().update_split(pos,pos_is_global,ignore_lock=ignore_lock) 106 107 108 # Events 109 110 def enterEvent(self, event): 111 """Pass along enter event to parent method.""" 112 super().enterEvent(event)
Extends SplitView for use in Butterfly Viewer.
Extends SplitView with keyboard shortcut to lock the position of the split in the Butterfly Viewer.
Overrides SplitView by checking split lock status before updating split.
Arguments:
- See parent method for full documentation.
91 def toggle_lock_split(self): 92 """Toggle the split lock. 93 94 Toggles the status of the split lock (e.g., if locked, it will become unlocked; vice versa). 95 """ 96 self.split_locked = not self.split_locked 97 self.shortcut_shift_x_was_activated.emit()
Toggle the split lock.
Toggles the status of the split lock (e.g., if locked, it will become unlocked; vice versa).
99 def update_split(self, pos = None, pos_is_global=False, ignore_lock=False): 100 """Update the position of the split while considering the status of the split lock. 101 102 See parent method for full documentation. 103 """ 104 if not self.split_locked or ignore_lock: 105 super().update_split(pos,pos_is_global,ignore_lock=ignore_lock)
Update the position of the split while considering the status of the split lock.
See parent method for full documentation.
116class MultiViewMainWindow(QtWidgets.QMainWindow): 117 """View multiple images with split-effect and synchronized panning and zooming. 118 119 Extends QMainWindow as main window of Butterfly Viewer with user interface: 120 121 - Create sliding overlays. 122 - Adjust sliding overlay transparencies. 123 - Change viewer settings. 124 """ 125 126 MaxRecentFiles = 10 127 128 def __init__(self): 129 super(MultiViewMainWindow, self).__init__() 130 131 self._recentFileActions = [] 132 self._handlingScrollChangedSignal = False 133 134 self._mdiArea = QMdiAreaWithCustomSignals() 135 self._mdiArea.file_path_dragged.connect(self.display_dragged_grayout) 136 self._mdiArea.file_path_dragged_and_dropped.connect(self.load_from_dragged_and_dropped_file) 137 self._mdiArea.shortcut_escape_was_activated.connect(self.set_fullscreen_off) 138 self._mdiArea.shortcut_f_was_activated.connect(self.toggle_fullscreen) 139 self._mdiArea.shortcut_h_was_activated.connect(self.toggle_interface) 140 self._mdiArea.shortcut_ctrl_c_was_activated.connect(self.copy_view) 141 self._mdiArea.first_subwindow_was_opened.connect(self.on_first_subwindow_was_opened) 142 self._mdiArea.last_remaining_subwindow_was_closed.connect(self.on_last_remaining_subwindow_was_closed) 143 144 self._mdiArea.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) 145 self._mdiArea.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) 146 self._mdiArea.subWindowActivated.connect(self.subWindowActivated) 147 148 self._mdiArea.setBackground(QtGui.QColor(32,32,32)) 149 150 self._label_mouse = QtWidgets.QLabel() # Pixel coordinates of mouse in a view 151 self._label_mouse.setText("") 152 self._label_mouse.adjustSize() 153 self._label_mouse.setVisible(False) 154 self._label_mouse.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignBottom) 155 self._label_mouse.setStyleSheet("QLabel {color: white; background-color: rgba(0, 0, 0, 191); border: 0px solid black; margin-left: 0.09em; margin-top: 0.09em; margin-right: 0.09em; margin-bottom: 0.09em; font-size: 7.5pt; border-radius: 0.09em; }") 156 157 self._splitview_creator = SplitViewCreator() 158 self._splitview_creator.clicked_create_splitview_pushbutton.connect(self.on_create_splitview) 159 tracker_creator = EventTrackerSplitBypassInterface(self._splitview_creator) 160 tracker_creator.mouse_position_changed.connect(self.update_split) 161 layout_mdiarea_topleft = GridLayoutFloatingShadow() 162 layout_mdiarea_topleft.addWidget(self._label_mouse, 1, 0, alignment=QtCore.Qt.AlignLeft|QtCore.Qt.AlignBottom) 163 layout_mdiarea_topleft.addWidget(self._splitview_creator, 0, 0, alignment=QtCore.Qt.AlignLeft) 164 self.interface_mdiarea_topleft = QtWidgets.QWidget() 165 self.interface_mdiarea_topleft.setLayout(layout_mdiarea_topleft) 166 167 self._mdiArea.subWindowActivated.connect(self.update_sliders) 168 self._mdiArea.subWindowActivated.connect(self.update_window_highlight) 169 self._mdiArea.subWindowActivated.connect(self.update_window_labels) 170 self._mdiArea.subWindowActivated.connect(self.updateMenus) 171 self._mdiArea.subWindowActivated.connect(self.auto_tile_subwindows_on_close) 172 self._mdiArea.subWindowActivated.connect(self.update_mdi_buttons) 173 174 self._sliders_opacity_splitviews = SlidersOpacitySplitViews() 175 self._sliders_opacity_splitviews.was_changed_slider_base_value.connect(self.on_slider_opacity_base_changed) 176 self._sliders_opacity_splitviews.was_changed_slider_topright_value.connect(self.on_slider_opacity_topright_changed) 177 self._sliders_opacity_splitviews.was_changed_slider_bottomright_value.connect(self.on_slider_opacity_bottomright_changed) 178 self._sliders_opacity_splitviews.was_changed_slider_bottomleft_value.connect(self.on_slider_opacity_bottomleft_changed) 179 tracker_sliders = EventTrackerSplitBypassInterface(self._sliders_opacity_splitviews) 180 tracker_sliders.mouse_position_changed.connect(self.update_split) 181 182 self._splitview_manager = SplitViewManager() 183 self._splitview_manager.hovered_xy.connect(self.set_split_from_manager) 184 self._splitview_manager.clicked_xy.connect(self.set_and_lock_split_from_manager) 185 self._splitview_manager.lock_split_locked.connect(self.lock_split) 186 self._splitview_manager.lock_split_unlocked.connect(self.unlock_split) 187 188 layout_mdiarea_bottomleft = GridLayoutFloatingShadow() 189 layout_mdiarea_bottomleft.addWidget(self._sliders_opacity_splitviews, 0, 0, alignment=QtCore.Qt.AlignBottom) 190 layout_mdiarea_bottomleft.addWidget(self._splitview_manager, 0, 1, alignment=QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop) 191 self.interface_mdiarea_bottomleft = QtWidgets.QWidget() 192 self.interface_mdiarea_bottomleft.setLayout(layout_mdiarea_bottomleft) 193 194 195 self.centralwidget_during_fullscreen_pushbutton = QtWidgets.QToolButton() # Needed for users to return the image viewer to the main window if the window of the viewer is lost during fullscreen 196 self.centralwidget_during_fullscreen_pushbutton.setText("Close Fullscreen") # Needed for users to return the image viewer to the main window if the window of the viewer is lost during fullscreen 197 self.centralwidget_during_fullscreen_pushbutton.clicked.connect(self.set_fullscreen_off) 198 self.centralwidget_during_fullscreen_pushbutton.setStyleSheet("font-size: 11pt") 199 self.centralwidget_during_fullscreen_layout = QtWidgets.QVBoxLayout() 200 self.centralwidget_during_fullscreen_layout.setAlignment(QtCore.Qt.AlignCenter) 201 self.centralwidget_during_fullscreen_layout.addWidget(self.centralwidget_during_fullscreen_pushbutton, alignment=QtCore.Qt.AlignCenter) 202 self.centralwidget_during_fullscreen = QtWidgets.QWidget() 203 self.centralwidget_during_fullscreen.setLayout(self.centralwidget_during_fullscreen_layout) 204 205 206 self.fullscreen_pushbutton = QtWidgets.QPushButton("⇲") 207 self.fullscreen_pushbutton.setToolTip("Fullscreen on/off (F)") 208 self.fullscreen_pushbutton.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) 209 self.fullscreen_pushbutton.setMouseTracking(True) 210 self.fullscreen_pushbutton.setStyleSheet(""" 211 QPushButton { 212 width: 1em; 213 height: 1em; 214 color: white; 215 background-color: rgba(0, 0, 0, 63); 216 border-width: 0.03em; 217 border-style: solid; 218 border-color: transparent; 219 font-size: 23pt; 220 } 221 QPushButton:hover { 222 background-color: rgba(91, 91, 255, 223); 223 } 224 QPushButton:pressed { 225 color: white; 226 background-color: rgba(127, 127, 255, 255); 227 } 228 QPushButton:checked { 229 color: white; 230 background-color: rgba(63, 63, 191, 191); 231 border-color: rgba(127, 127, 255, 255); 232 } 233 QPushButton:checked:hover { 234 color: white; 235 background-color: rgba(95, 95, 223, 223); 236 border-color: rgba(127, 127, 255, 255); 237 } 238 QPushButton:checked:pressed { 239 color: white; 240 background-color: rgba(127, 127, 255, 255); 241 border-color: rgba(127, 127, 255, 255); 242 } 243 """) 244 self.fullscreen_pushbutton.setCheckable(True) 245 self.fullscreen_pushbutton.toggled.connect(self.set_fullscreen) 246 self.is_fullscreen = False 247 248 self.interface_toggle_pushbutton = QtWidgets.QPushButton("≼") 249 self.interface_toggle_pushbutton.setToolTip("Hide interface (H)") 250 self.interface_toggle_pushbutton.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) 251 self.interface_toggle_pushbutton.setMouseTracking(True) 252 self.interface_toggle_pushbutton.setStyleSheet(""" 253 QPushButton { 254 width: 1em; 255 height: 1em; 256 color: white; 257 background-color: rgba(0, 0, 0, 63); 258 border-width: 0.03em; 259 border-style: solid; 260 border-color: rgba(127, 127, 255, 255); 261 font-size: 23pt; 262 } 263 QPushButton:hover { 264 background-color: rgba(91, 91, 255, 223); 265 } 266 QPushButton:pressed { 267 color: white; 268 background-color: rgba(127, 127, 255, 255); 269 } 270 QPushButton:checked { 271 color: white; 272 background-color: rgba(63, 63, 191, 191); 273 border-color: rgba(127, 127, 255, 255); 274 } 275 QPushButton:checked:hover { 276 color: white; 277 background-color: rgba(95, 95, 223, 223); 278 border-color: rgba(127, 127, 255, 255); 279 } 280 QPushButton:checked:pressed { 281 color: white; 282 background-color: rgba(127, 127, 255, 255); 283 border-color: rgba(127, 127, 255, 255); 284 } 285 """) 286 self.interface_toggle_pushbutton.setCheckable(True) 287 self.interface_toggle_pushbutton.setChecked(True) 288 self.interface_toggle_pushbutton.clicked.connect(self.show_interface) 289 290 self.interface_toggle_slash_label = QtWidgets.QLabel("/") 291 self.interface_toggle_slash_label.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents) 292 self.interface_toggle_slash_label.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) 293 self.interface_toggle_slash_label.setMouseTracking(True) 294 self.interface_toggle_slash_label.setStyleSheet(""" 295 QLabel { 296 width: 1em; 297 height: 1em; 298 color: white; 299 background-color: rgba(0, 0, 0, 0); 300 border-width: 0.03em; 301 border-style: solid; 302 border-color: rgba(0, 0, 0, 0); 303 font-size: 22pt; 304 } 305 """) 306 self.interface_toggle_slash_label.setVisible(False) 307 308 self.is_interface_showing = True 309 self.is_quiet_mode = False 310 self.is_global_transform_mode_smooth = False 311 self.scene_background_color = None 312 313 self.close_all_pushbutton = QtWidgets.QPushButton("⦻") 314 self.close_all_pushbutton.setToolTip("Close all image windows") 315 self.close_all_pushbutton.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) 316 self.close_all_pushbutton.setMouseTracking(True) 317 self.close_all_pushbutton.setStyleSheet(""" 318 QPushButton { 319 width: 1.03em; 320 height: 1.03em; 321 color: white; 322 background-color: rgba(0, 0, 0, 63); 323 border: 0px black; 324 font-size: 23pt; 325 } 326 QPushButton:hover { 327 color: white; 328 background-color: rgba(223, 0, 0, 223); 329 } 330 QPushButton:pressed { 331 color: white; 332 background-color: rgba(255, 0, 0, 255); 333 } 334 """) 335 self.close_all_pushbutton.clicked.connect(self._mdiArea.closeAllSubWindows) 336 337 self.tile_default_pushbutton = QtWidgets.QPushButton("⦺") 338 self.tile_default_pushbutton.setToolTip("Grid arrange windows") 339 self.tile_default_pushbutton.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) 340 self.tile_default_pushbutton.setMouseTracking(True) 341 self.tile_default_pushbutton.setStyleSheet(""" 342 QPushButton { 343 width: 1em; 344 height: 1em; 345 color: white; 346 background-color: rgba(0, 0, 0, 63); 347 border-width: 0.03em; 348 border-style: solid; 349 border-color: transparent; 350 font-size: 23pt; 351 } 352 QPushButton:hover { 353 background-color: rgba(91, 91, 91, 223); 354 border-color: transparent; 355 } 356 QPushButton:pressed { 357 background-color: rgba(116, 116, 116, 255); 358 border-color: transparent; 359 } 360 """) 361 self.tile_default_pushbutton.clicked.connect(self._mdiArea.tileSubWindows) 362 self.tile_default_pushbutton.clicked.connect(self.fit_to_window) 363 self.tile_default_pushbutton.clicked.connect(self.refreshPan) 364 365 self.tile_horizontally_pushbutton = QtWidgets.QPushButton("⦶") 366 self.tile_horizontally_pushbutton.setToolTip("Horizontally arrange windows in a single row") 367 self.tile_horizontally_pushbutton.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) 368 self.tile_horizontally_pushbutton.setMouseTracking(True) 369 self.tile_horizontally_pushbutton.setStyleSheet(""" 370 QPushButton { 371 width: 1em; 372 height: 1em; 373 color: white; 374 background-color: rgba(0, 0, 0, 63); 375 border-width: 0.03em; 376 border-style: solid; 377 border-color: transparent; 378 font-size: 23pt; 379 } 380 QPushButton:hover { 381 background-color: rgba(91, 91, 91, 223); 382 border-color: transparent; 383 } 384 QPushButton:pressed { 385 background-color: rgba(116, 116, 116, 255); 386 border-color: transparent; 387 } 388 """) 389 self.tile_horizontally_pushbutton.clicked.connect(self._mdiArea.tile_subwindows_horizontally) 390 self.tile_horizontally_pushbutton.clicked.connect(self.fit_to_window) 391 self.tile_horizontally_pushbutton.clicked.connect(self.refreshPan) 392 393 self.tile_vertically_pushbutton = QtWidgets.QPushButton("⦵") 394 self.tile_vertically_pushbutton.setToolTip("Vertically arrange windows in a single column") 395 self.tile_vertically_pushbutton.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) 396 self.tile_vertically_pushbutton.setMouseTracking(True) 397 self.tile_vertically_pushbutton.setStyleSheet(""" 398 QPushButton { 399 min-width: 1em; 400 min-height: 1em; 401 color: white; 402 background-color: rgba(0, 0, 0, 63); 403 border-width: 0.03em; 404 border-style: solid; 405 border-color: transparent; 406 font-size: 23pt; 407 } 408 QPushButton:hover { 409 background-color: rgba(91, 91, 91, 223); 410 border-color: transparent; 411 } 412 QPushButton:pressed { 413 background-color: rgba(116, 116, 116, 255); 414 border-color: transparent; 415 } 416 """) 417 self.tile_vertically_pushbutton.clicked.connect(self._mdiArea.tile_subwindows_vertically) 418 self.tile_vertically_pushbutton.clicked.connect(self.fit_to_window) 419 self.tile_vertically_pushbutton.clicked.connect(self.refreshPan) 420 421 self.fit_to_window_pushbutton = QtWidgets.QPushButton("⦾") 422 self.fit_to_window_pushbutton.setToolTip("Fit and center image in active window (affects all if synced)") 423 self.fit_to_window_pushbutton.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) 424 self.fit_to_window_pushbutton.setMouseTracking(True) 425 self.fit_to_window_pushbutton.setStyleSheet(""" 426 QPushButton { 427 width: 1em; 428 height: 1em; 429 color: white; 430 background-color: rgba(0, 0, 0, 63); 431 border-width: 0.03em; 432 border-style: solid; 433 border-color: transparent; 434 font-size: 23pt; 435 } 436 QPushButton:hover { 437 background-color: rgba(91, 91, 91, 223); 438 border-color: transparent; 439 } 440 QPushButton:pressed { 441 background-color: rgba(116, 116, 116, 255); 442 border-color: transparent; 443 } 444 """) 445 self.fit_to_window_pushbutton.clicked.connect(self.fit_to_window) 446 447 self.info_pushbutton = QtWidgets.QPushButton("ℹ︎") 448 self.info_pushbutton.setToolTip("Info...") 449 self.info_pushbutton.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) 450 self.info_pushbutton.setMouseTracking(True) 451 self.info_pushbutton.setStyleSheet(""" 452 QPushButton { 453 width: 1em; 454 height: 1em; 455 color: white; 456 background-color: rgba(0, 0, 0, 0); 457 border-width: 0.03em; 458 border-style: solid; 459 border-color: transparent; 460 font-size: 23pt; 461 } 462 QPushButton:hover { 463 background-color: rgba(91, 91, 91, 223); 464 border-color: transparent; 465 } 466 QPushButton:pressed { 467 background-color: rgba(116, 116, 116, 255); 468 border-color: transparent; 469 } 470 """) 471 self.info_pushbutton.clicked.connect(self.info_button_clicked) 472 473 self.stopsync_toggle_pushbutton = QtWidgets.QPushButton("⇆") 474 self.stopsync_toggle_pushbutton.setToolTip("Unsynchronize zoom and pan (currently synced)") 475 self.stopsync_toggle_pushbutton.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) 476 self.stopsync_toggle_pushbutton.setMouseTracking(True) 477 self.stopsync_toggle_pushbutton.setStyleSheet(""" 478 QPushButton { 479 width: 1em; 480 height: 1em; 481 color: white; 482 background-color: rgba(67, 176, 42, 191); 483 border-width: 0.03em; 484 border-style: solid; 485 border-color: rgba(80, 211, 50, 255); 486 font-size: 23pt; 487 } 488 QPushButton:hover { 489 background-color: rgba(77, 187, 51, 223); 490 border-color: rgba(80, 211, 50, 255); 491 } 492 QPushButton:pressed { 493 background-color: rgba(93, 187, 31, 255); 494 border-color: rgba(80, 211, 50, 255); 495 } 496 QPushButton:checked { 497 background-color: rgba(191, 151, 0, 191); 498 border-color: rgba(255, 181, 0, 255); 499 } 500 QPushButton:checked:hover { 501 background-color: rgba(223, 166, 0, 223); 502 border-color: rgba(255, 181, 0, 255); 503 } 504 QPushButton:checked:pressed { 505 background-color: rgba(255, 181, 0, 255); 506 border-color: rgba(255, 181, 0, 255); 507 } 508 """) 509 self.stopsync_toggle_pushbutton.setCheckable(True) 510 self.stopsync_toggle_pushbutton.toggled.connect(self.set_stopsync_pushbutton) 511 512 self.stopsync_toggle_slash_label = QtWidgets.QLabel("/") 513 self.stopsync_toggle_slash_label.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents) 514 self.stopsync_toggle_slash_label.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) 515 self.stopsync_toggle_slash_label.setMouseTracking(True) 516 self.stopsync_toggle_slash_label.setStyleSheet(""" 517 QLabel { 518 width: 1em; 519 height: 1em; 520 color: white; 521 background-color: rgba(0, 0, 0, 0); 522 border-width: 0.03em; 523 border-style: solid; 524 border-color: rgba(0, 0, 0, 0); 525 font-size: 22pt; 526 } 527 """) 528 self.stopsync_toggle_slash_label.setVisible(False) 529 530 self.save_view_pushbutton = QtWidgets.QPushButton("⤓") 531 self.save_view_pushbutton.setToolTip("Save a screenshot of the viewer... | Copy screenshot to clipboard (Ctrl·C)") 532 self.save_view_pushbutton.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) 533 self.save_view_pushbutton.setMouseTracking(True) 534 self.save_view_pushbutton.setStyleSheet(""" 535 QPushButton { 536 width: 1em; 537 height: 1em; 538 color: white; 539 background-color: rgba(0, 0, 0, 63); 540 border-width: 0.03em; 541 border-style: solid; 542 border-color: transparent; 543 font-size: 23pt; 544 } 545 QPushButton:hover { 546 background-color: rgba(91, 91, 255, 223); 547 } 548 QPushButton:pressed { 549 color: white; 550 background-color: rgba(127, 127, 255, 255); 551 } 552 """) 553 self.save_view_pushbutton.clicked.connect(self.save_view) 554 555 self.buffer_label = QtWidgets.QPushButton() 556 self.buffer_label.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents) 557 self.buffer_label.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) 558 self.buffer_label.setMouseTracking(True) 559 self.buffer_label.setStyleSheet(""" 560 QPushButton { 561 width: 1em; 562 height: 1em; 563 color: transparent; 564 background-color: rgba(0, 0, 0, 0); 565 border-width: 0.03em; 566 border-style: solid; 567 border-color: rgba(0, 0, 0, 0); 568 font-size: 23pt; 569 } 570 """) 571 572 self.label_mdiarea = QtWidgets.QLabel() 573 self.label_mdiarea.setText("Drag images directly to create individual image windows\n\n—\n\nCreate sliding overlays to compare images directly over each other\n\n—\n\nRight-click image windows to change settings and add tools") 574 self.label_mdiarea.setStyleSheet(""" 575 QLabel { 576 color: white; 577 border: 0.13em dashed gray; 578 border-radius: 0.25em; 579 background-color: transparent; 580 padding: 1em; 581 font-size: 10pt; 582 } 583 """) 584 self.label_mdiarea.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents) 585 self.label_mdiarea.setAlignment(QtCore.Qt.AlignCenter) 586 587 layout_mdiarea_bottomright_vertical = GridLayoutFloatingShadow() 588 layout_mdiarea_bottomright_vertical.addWidget(self.fullscreen_pushbutton, 5, 0) 589 layout_mdiarea_bottomright_vertical.addWidget(self.tile_default_pushbutton, 4, 0) 590 layout_mdiarea_bottomright_vertical.addWidget(self.tile_horizontally_pushbutton, 3, 0) 591 layout_mdiarea_bottomright_vertical.addWidget(self.tile_vertically_pushbutton, 2, 0) 592 layout_mdiarea_bottomright_vertical.addWidget(self.fit_to_window_pushbutton, 1, 0) 593 layout_mdiarea_bottomright_vertical.addWidget(self.info_pushbutton, 0, 0) 594 layout_mdiarea_bottomright_vertical.setContentsMargins(0,0,0,16) 595 self.interface_mdiarea_bottomright_vertical = QtWidgets.QWidget() 596 self.interface_mdiarea_bottomright_vertical.setLayout(layout_mdiarea_bottomright_vertical) 597 tracker_interface_mdiarea_bottomright_vertical = EventTrackerSplitBypassInterface(self.interface_mdiarea_bottomright_vertical) 598 tracker_interface_mdiarea_bottomright_vertical.mouse_position_changed.connect(self.update_split) 599 600 layout_mdiarea_bottomright_horizontal = GridLayoutFloatingShadow() 601 layout_mdiarea_bottomright_horizontal.addWidget(self.buffer_label, 0, 5) 602 layout_mdiarea_bottomright_horizontal.addWidget(self.interface_toggle_pushbutton, 0, 4) 603 layout_mdiarea_bottomright_horizontal.addWidget(self.interface_toggle_slash_label, 0, 4) 604 layout_mdiarea_bottomright_horizontal.addWidget(self.close_all_pushbutton, 0, 3) 605 layout_mdiarea_bottomright_horizontal.addWidget(self.stopsync_toggle_pushbutton, 0, 2) 606 layout_mdiarea_bottomright_horizontal.addWidget(self.stopsync_toggle_slash_label, 0, 2) 607 layout_mdiarea_bottomright_horizontal.addWidget(self.save_view_pushbutton, 0, 1) 608 layout_mdiarea_bottomright_horizontal.setContentsMargins(0,0,0,16) 609 self.interface_mdiarea_bottomright_horizontal = QtWidgets.QWidget() 610 self.interface_mdiarea_bottomright_horizontal.setLayout(layout_mdiarea_bottomright_horizontal) 611 tracker_interface_mdiarea_bottomright_horizontal = EventTrackerSplitBypassInterface(self.interface_mdiarea_bottomright_horizontal) 612 tracker_interface_mdiarea_bottomright_horizontal.mouse_position_changed.connect(self.update_split) 613 614 615 self.loading_grayout_label = QtWidgets.QLabel("Loading...") # Needed to give users feedback when loading views 616 self.loading_grayout_label.setWordWrap(True) 617 self.loading_grayout_label.setAlignment(QtCore.Qt.AlignCenter | QtCore.Qt.AlignVCenter) 618 self.loading_grayout_label.setVisible(False) 619 self.loading_grayout_label.setStyleSheet(""" 620 QLabel { 621 color: white; 622 background-color: rgba(0,0,0,223); 623 font-size: 10pt; 624 } 625 """) 626 627 self.dragged_grayout_label = QtWidgets.QLabel("Drop to create single view(s)...") # Needed to give users feedback when dragging in images 628 self.dragged_grayout_label.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents) 629 self.dragged_grayout_label.setWordWrap(True) 630 self.dragged_grayout_label.setAlignment(QtCore.Qt.AlignCenter | QtCore.Qt.AlignVCenter) 631 self.dragged_grayout_label.setVisible(False) 632 self.dragged_grayout_label.setStyleSheet(""" 633 QLabel { 634 color: white; 635 background-color: rgba(63,63,63,223); 636 border: 0.13em dashed gray; 637 border-radius: 0.25em; 638 margin-left: 0.25em; 639 margin-top: 0.25em; 640 margin-right: 0.25em; 641 margin-bottom: 0.25em; 642 font-size: 10pt; 643 } 644 """) 645 646 647 layout_mdiarea = QtWidgets.QGridLayout() 648 layout_mdiarea.setContentsMargins(0, 0, 0, 0) 649 layout_mdiarea.setSpacing(0) 650 layout_mdiarea.addWidget(self._mdiArea, 0, 0) 651 layout_mdiarea.addWidget(self.label_mdiarea, 0, 0, QtCore.Qt.AlignCenter) 652 layout_mdiarea.addWidget(self.dragged_grayout_label, 0, 0) 653 layout_mdiarea.addWidget(self.loading_grayout_label, 0, 0) 654 layout_mdiarea.addWidget(self.interface_mdiarea_topleft, 0, 0, QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft) 655 layout_mdiarea.addWidget(self.interface_mdiarea_bottomleft, 0, 0, QtCore.Qt.AlignBottom | QtCore.Qt.AlignLeft) 656 layout_mdiarea.addWidget(self.interface_mdiarea_bottomright_horizontal, 0, 0, QtCore.Qt.AlignBottom | QtCore.Qt.AlignRight) 657 layout_mdiarea.addWidget(self.interface_mdiarea_bottomright_vertical, 0, 0, QtCore.Qt.AlignBottom | QtCore.Qt.AlignRight) 658 659 self.mdiarea_plus_buttons = QtWidgets.QWidget() 660 self.mdiarea_plus_buttons.setLayout(layout_mdiarea) 661 662 self.setCentralWidget(self.mdiarea_plus_buttons) 663 664 self.subwindow_was_just_closed = False 665 666 self._windowMapper = QtCore.QSignalMapper(self) 667 668 self._actionMapper = QtCore.QSignalMapper(self) 669 self._actionMapper.mapped[str].connect(self.mappedImageViewerAction) 670 self._recentFileMapper = QtCore.QSignalMapper(self) 671 self._recentFileMapper.mapped[str].connect(self.openRecentFile) 672 673 self.createActions() 674 self.addAction(self._activateSubWindowSystemMenuAct) 675 676 self.createMenus() 677 self.updateMenus() 678 self.createStatusBar() 679 680 self.readSettings() 681 self.updateStatusBar() 682 683 self.setUnifiedTitleAndToolBarOnMac(True) 684 685 self.showNormal() 686 self.menuBar().hide() 687 688 self.setStyleSheet("QWidget{font-size: 9pt}") 689 690 691 # Screenshot window 692 693 def copy_view(self): 694 """Screenshot MultiViewMainWindow and copy to clipboard as image.""" 695 696 self.display_loading_grayout(True, "Screenshot copied to clipboard.") 697 698 interface_was_already_set_hidden = not self.is_interface_showing # Needed to hide the interface temporarily while grabbing a screenshot (makes sure the screenshot only shows the views) 699 if not interface_was_already_set_hidden: 700 self.show_interface_off() 701 702 pixmap = self._mdiArea.grab() 703 clipboard = QtWidgets.QApplication.clipboard() 704 clipboard.setPixmap(pixmap) 705 706 if not interface_was_already_set_hidden: 707 self.show_interface_on() 708 709 self.display_loading_grayout(False, pseudo_load_time=1) 710 711 712 def save_view(self): 713 """Screenshot MultiViewMainWindow and open Save dialog to save screenshot as image.""" 714 715 self.display_loading_grayout(True, "Saving viewer screenshot...") 716 717 folderpath = None 718 719 if self.activeMdiChild: 720 folderpath = self.activeMdiChild.currentFile 721 folderpath = os.path.dirname(folderpath) 722 folderpath = folderpath + "\\" 723 else: 724 self.display_loading_grayout(False, pseudo_load_time=0) 725 return 726 727 interface_was_already_set_hidden = not self.is_interface_showing # Needed to hide the interface temporarily while grabbing a screenshot (makes sure the screenshot only shows the views) 728 if not interface_was_already_set_hidden: 729 self.show_interface_off() 730 731 pixmap = self._mdiArea.grab() 732 733 date_and_time = datetime.now().strftime('%Y-%m-%d %H%M%S') # Sets the default filename with date and time 734 filename = "Viewer screenshot " + date_and_time + ".png" 735 name_filters = "PNG (*.png);; JPEG (*.jpeg);; TIFF (*.tiff);; JPG (*.jpg);; TIF (*.tif)" # Allows users to select filetype of screenshot 736 737 self.display_loading_grayout(True, "Selecting folder and name for the viewer screenshot...", pseudo_load_time=0) 738 739 filepath, _ = QtWidgets.QFileDialog.getSaveFileName(self, "Save a screenshot of the viewer", folderpath+filename, name_filters) 740 _, fileextension = os.path.splitext(filepath) 741 fileextension = fileextension.replace('.','') 742 if filepath: 743 pixmap.save(filepath, fileextension) 744 745 if not interface_was_already_set_hidden: 746 self.show_interface_on() 747 748 self.display_loading_grayout(False) 749 750 751 # Interface and appearance 752 753 def display_loading_grayout(self, boolean, text="Loading...", pseudo_load_time=0.2): 754 """Show/hide grayout screen for loading sequences. 755 756 Args: 757 boolean (bool): True to show grayout; False to hide. 758 text (str): The text to show on the grayout. 759 pseudo_load_time (float): The delay (in seconds) to hide the grayout to give users a feeling of action. 760 """ 761 if not boolean: 762 text = "Loading..." 763 self.loading_grayout_label.setText(text) 764 self.loading_grayout_label.setVisible(boolean) 765 if boolean: 766 self.loading_grayout_label.repaint() 767 if not boolean: 768 time.sleep(pseudo_load_time) 769 770 def display_dragged_grayout(self, boolean): 771 """Show/hide grayout screen for drag-and-drop sequences. 772 773 Args: 774 boolean (bool): True to show grayout; False to hide. 775 """ 776 self.dragged_grayout_label.setVisible(boolean) 777 if boolean: 778 self.dragged_grayout_label.repaint() 779 780 def on_last_remaining_subwindow_was_closed(self): 781 """Show instructions label of MDIArea.""" 782 self.label_mdiarea.setVisible(True) 783 784 def on_first_subwindow_was_opened(self): 785 """Hide instructions label of MDIArea.""" 786 self.label_mdiarea.setVisible(False) 787 788 def show_interface(self, boolean): 789 """Show/hide interface elements for sliding overlay creator and transparencies. 790 791 Args: 792 boolean (bool): True to show interface; False to hide. 793 """ 794 if boolean: 795 self.show_interface_on() 796 elif not boolean: 797 self.show_interface_off() 798 799 def show_interface_on(self): 800 """Show interface elements for sliding overlay creator and transparencies.""" 801 if self.is_interface_showing: 802 return 803 804 self.is_interface_showing = True 805 self.is_quiet_mode = False 806 807 self.update_window_highlight(self._mdiArea.activeSubWindow()) 808 self.update_window_labels(self._mdiArea.activeSubWindow()) 809 self.set_window_close_pushbuttons_always_visible(self._mdiArea.activeSubWindow(), True) 810 self.set_window_mouse_rect_visible(self._mdiArea.activeSubWindow(), True) 811 self.interface_mdiarea_topleft.setVisible(True) 812 self.interface_mdiarea_bottomleft.setVisible(True) 813 self.interface_toggle_slash_label.setVisible(False) 814 815 self.interface_toggle_pushbutton.setToolTip("Hide interface (studio mode)") 816 817 if self.interface_toggle_pushbutton: 818 self.interface_toggle_pushbutton.setChecked(True) 819 820 def show_interface_off(self): 821 """Hide interface elements for sliding overlay creator and transparencies.""" 822 if not self.is_interface_showing: 823 return 824 825 self.is_interface_showing = False 826 self.is_quiet_mode = True 827 828 self.update_window_highlight(self._mdiArea.activeSubWindow()) 829 self.update_window_labels(self._mdiArea.activeSubWindow()) 830 self.set_window_close_pushbuttons_always_visible(self._mdiArea.activeSubWindow(), False) 831 self.set_window_mouse_rect_visible(self._mdiArea.activeSubWindow(), False) 832 self.interface_mdiarea_topleft.setVisible(False) 833 self.interface_mdiarea_bottomleft.setVisible(False) 834 self.interface_toggle_slash_label.setVisible(True) 835 836 self.interface_toggle_pushbutton.setToolTip("Show interface (H)") 837 838 if self.interface_toggle_pushbutton: 839 self.interface_toggle_pushbutton.setChecked(False) 840 self.interface_toggle_pushbutton.setAttribute(QtCore.Qt.WA_UnderMouse, False) 841 842 def toggle_interface(self): 843 """Toggle visibilty of interface elements for sliding overlay creator and transparencies.""" 844 if self.is_interface_showing: # If interface is showing, then toggle it off; if not, then toggle it on 845 self.show_interface_off() 846 else: 847 self.show_interface_on() 848 849 def set_stopsync_pushbutton(self, boolean): 850 """Set state of synchronous zoom/pan and appearance of corresponding interface button. 851 852 Args: 853 boolean (bool): True to enable synchronized zoom/pan; False to disable. 854 """ 855 self._synchZoomAct.setChecked(not boolean) 856 self._synchPanAct.setChecked(not boolean) 857 858 if self._synchZoomAct.isChecked(): 859 if self.activeMdiChild: 860 self.activeMdiChild.fitToWindow() 861 862 if boolean: 863 self.stopsync_toggle_pushbutton.setText("⇆") 864 self.stopsync_toggle_pushbutton.setToolTip("Synchronize zoom and pan (currently unsynced)") 865 self.stopsync_toggle_slash_label.show() 866 else: 867 self.stopsync_toggle_pushbutton.setText("⇆") 868 self.stopsync_toggle_pushbutton.setToolTip("Unsynchronize zoom and pan (currently synced)") 869 self.stopsync_toggle_slash_label.hide() 870 871 def toggle_fullscreen(self): 872 """Toggle fullscreen state of app.""" 873 if self.is_fullscreen: 874 self.set_fullscreen_off() 875 else: 876 self.set_fullscreen_on() 877 878 def set_fullscreen_on(self): 879 """Enable fullscreen of MultiViewMainWindow. 880 881 Moves MDIArea to secondary window and makes it fullscreen. 882 Shows interim widget in main window. 883 """ 884 if self.is_fullscreen: 885 return 886 887 position_of_window = self.pos() 888 889 centralwidget_to_be_made_fullscreen = self.mdiarea_plus_buttons 890 widget_to_replace_central = self.centralwidget_during_fullscreen 891 892 centralwidget_to_be_made_fullscreen.setParent(None) 893 894 # move() is needed when using multiple monitors because when the widget loses its parent, its position moves to the primary screen origin (0,0) instead of retaining the app's screen 895 # The solution is to move the widget to the position of the app window and then make the widget fullscreen 896 # A timer is needed for showFullScreen() to apply on the app's screen (otherwise the command is made before the widget's move is established) 897 centralwidget_to_be_made_fullscreen.move(position_of_window) 898 QtCore.QTimer.singleShot(50, centralwidget_to_be_made_fullscreen.showFullScreen) 899 900 self.showMinimized() 901 902 self.setCentralWidget(widget_to_replace_central) 903 widget_to_replace_central.show() 904 905 self._mdiArea.tile_what_was_done_last_time() 906 self._mdiArea.activateWindow() 907 908 self.is_fullscreen = True 909 if self.fullscreen_pushbutton: 910 self.fullscreen_pushbutton.setChecked(True) 911 912 if self.activeMdiChild: 913 self.synchPan(self.activeMdiChild) 914 915 def set_fullscreen_off(self): 916 """Disable fullscreen of MultiViewMainWindow. 917 918 Removes interim widget in main window. 919 Returns MDIArea to normal (non-fullscreen) view on main window. 920 """ 921 if not self.is_fullscreen: 922 return 923 924 self.showNormal() 925 926 fullscreenwidget_to_be_made_central = self.mdiarea_plus_buttons 927 centralwidget_to_be_hidden = self.centralwidget_during_fullscreen 928 929 centralwidget_to_be_hidden.setParent(None) 930 centralwidget_to_be_hidden.hide() 931 932 self.setCentralWidget(fullscreenwidget_to_be_made_central) 933 934 self._mdiArea.tile_what_was_done_last_time() 935 self._mdiArea.activateWindow() 936 937 self.is_fullscreen = False 938 if self.fullscreen_pushbutton: 939 self.fullscreen_pushbutton.setChecked(False) 940 self.fullscreen_pushbutton.setAttribute(QtCore.Qt.WA_UnderMouse, False) 941 942 self.refreshPanDelayed(100) 943 944 def set_fullscreen(self, boolean): 945 """Enable/disable fullscreen of MultiViewMainWindow. 946 947 Args: 948 boolean (bool): True to enable fullscreen; False to disable. 949 """ 950 if boolean: 951 self.set_fullscreen_on() 952 elif not boolean: 953 self.set_fullscreen_off() 954 955 def update_window_highlight(self, window): 956 """Update highlight of subwindows in MDIArea. 957 958 Input window should be the subwindow which is active. 959 All other subwindow(s) will be shown no highlight. 960 961 Args: 962 window (QMdiSubWindow): The active subwindow to show highlight and indicate as active. 963 """ 964 if window is None: 965 return 966 changed_window = window 967 if self.is_quiet_mode: 968 changed_window.widget().frame_hud.setStyleSheet("QFrame {border: 0px solid transparent}") 969 elif self.activeMdiChild.split_locked: 970 changed_window.widget().frame_hud.setStyleSheet("QFrame {border: 0.2em orange; border-left-style: outset; border-top-style: inset; border-right-style: inset; border-bottom-style: inset}") 971 else: 972 changed_window.widget().frame_hud.setStyleSheet("QFrame {border: 0.2em blue; border-left-style: outset; border-top-style: inset; border-right-style: inset; border-bottom-style: inset}") 973 974 windows = self._mdiArea.subWindowList() 975 for window in windows: 976 if window != changed_window: 977 window.widget().frame_hud.setStyleSheet("QFrame {border: 0px solid transparent}") 978 979 def update_window_labels(self, window): 980 """Update labels of subwindows in MDIArea. 981 982 Input window should be the subwindow which is active. 983 All other subwindow(s) will be shown no labels. 984 985 Args: 986 window (QMdiSubWindow): The active subwindow to show label(s) of image(s) and indicate as active. 987 """ 988 if window is None: 989 return 990 changed_window = window 991 label_visible = True 992 if self.is_quiet_mode: 993 label_visible = False 994 changed_window.widget().label_main_topleft.set_visible_based_on_text(label_visible) 995 changed_window.widget().label_topright.set_visible_based_on_text(label_visible) 996 changed_window.widget().label_bottomright.set_visible_based_on_text(label_visible) 997 changed_window.widget().label_bottomleft.set_visible_based_on_text(label_visible) 998 999 windows = self._mdiArea.subWindowList() 1000 for window in windows: 1001 if window != changed_window: 1002 window.widget().label_main_topleft.set_visible_based_on_text(False) 1003 window.widget().label_topright.set_visible_based_on_text(False) 1004 window.widget().label_bottomright.set_visible_based_on_text(False) 1005 window.widget().label_bottomleft.set_visible_based_on_text(False) 1006 1007 def set_window_close_pushbuttons_always_visible(self, window, boolean): 1008 """Enable/disable the always-on visiblilty of the close X on each subwindow. 1009 1010 Args: 1011 window (QMdiSubWindow): The active subwindow. 1012 boolean (bool): True to show the close X always; False to hide unless mouse hovers over. 1013 """ 1014 if window is None: 1015 return 1016 changed_window = window 1017 always_visible = boolean 1018 changed_window.widget().set_close_pushbutton_always_visible(always_visible) 1019 windows = self._mdiArea.subWindowList() 1020 for window in windows: 1021 if window != changed_window: 1022 window.widget().set_close_pushbutton_always_visible(always_visible) 1023 1024 def set_window_mouse_rect_visible(self, window, boolean): 1025 """Enable/disable the visiblilty of the red 1x1 outline at the pointer 1026 1027 Outline shows the relative size of a pixel in the active subwindow. 1028 1029 Args: 1030 window (QMdiSubWindow): The active subwindow. 1031 boolean (bool): True to show 1x1 outline; False to hide. 1032 """ 1033 if window is None: 1034 return 1035 changed_window = window 1036 visible = boolean 1037 changed_window.widget().set_mouse_rect_visible(visible) 1038 windows = self._mdiArea.subWindowList() 1039 for window in windows: 1040 if window != changed_window: 1041 window.widget().set_mouse_rect_visible(visible) 1042 1043 def auto_tile_subwindows_on_close(self): 1044 """Tile the subwindows of MDIArea using previously used tile method.""" 1045 if self.subwindow_was_just_closed: 1046 self.subwindow_was_just_closed = False 1047 QtCore.QTimer.singleShot(50, self._mdiArea.tile_what_was_done_last_time) 1048 self.refreshPanDelayed(50) 1049 1050 def update_mdi_buttons(self, window): 1051 """Update the interface button 'Split Lock' based on the status of the split (locked/unlocked) in the given window. 1052 1053 Args: 1054 window (QMdiSubWindow): The active subwindow. 1055 """ 1056 if window is None: 1057 self._splitview_manager.lock_split_pushbutton.setChecked(False) 1058 return 1059 1060 child = self.activeMdiChild 1061 1062 self._splitview_manager.lock_split_pushbutton.setChecked(child.split_locked) 1063 1064 1065 def set_single_window_transform_mode_smooth(self, window, boolean): 1066 """Set the transform mode of a given subwindow. 1067 1068 Args: 1069 window (QMdiSubWindow): The subwindow. 1070 boolean (bool): True to smooth (interpolate); False to fast (not interpolate). 1071 """ 1072 if window is None: 1073 return 1074 changed_window = window 1075 changed_window.widget().set_transform_mode_smooth(boolean) 1076 1077 1078 def set_all_window_transform_mode_smooth(self, boolean): 1079 """Set the transform mode of all subwindows. 1080 1081 Args: 1082 boolean (bool): True to smooth (interpolate); False to fast (not interpolate). 1083 """ 1084 if self._mdiArea.activeSubWindow() is None: 1085 return 1086 windows = self._mdiArea.subWindowList() 1087 for window in windows: 1088 window.widget().set_transform_mode_smooth(boolean) 1089 1090 def set_all_background_color(self, color): 1091 """Set the background color of all subwindows. 1092 1093 Args: 1094 color (list): Descriptor string and RGB int values. Example: ["White", 255, 255, 255]. 1095 """ 1096 if self._mdiArea.activeSubWindow() is None: 1097 return 1098 windows = self._mdiArea.subWindowList() 1099 for window in windows: 1100 window.widget().set_scene_background_color(color) 1101 self.scene_background_color = color 1102 1103 def info_button_clicked(self): 1104 """Trigger when info button is clicked.""" 1105 self.show_about() 1106 return 1107 1108 def show_about(self): 1109 """Show about box.""" 1110 sp = "<br>" 1111 title = "Butterfly Viewer" 1112 text = "Butterfly Viewer" 1113 text = text + sp + "Lars Maxfield" 1114 text = text + sp + "Version: " + __version__ 1115 text = text + sp + "License: <a href='https://www.gnu.org/licenses/gpl-3.0.en.html'>GNU GPL v3</a> or later" 1116 text = text + sp + "Source: <a href='https://github.com/olive-groves/butterfly_viewer'>github.com/olive-groves/butterfly_viewer</a>" 1117 text = text + sp + "Tutorial: <a href='https://olive-groves.github.io/butterfly_viewer'>olive-groves.github.io/butterfly_viewer</a>" 1118 box = QtWidgets.QMessageBox.about(self, title, text) 1119 1120 # View loading methods 1121 1122 def loadFile(self, filename_main_topleft, filename_topright=None, filename_bottomleft=None, filename_bottomright=None): 1123 """Load an individual image or sliding overlay into new subwindow. 1124 1125 Args: 1126 filename_main_topleft (str): The image filepath of the main image to be viewed; the basis of the sliding overlay (main; topleft) 1127 filename_topright (str): The image filepath for top-right of the sliding overlay (set None to exclude) 1128 filename_bottomleft (str): The image filepath for bottom-left of the sliding overlay (set None to exclude) 1129 filename_bottomright (str): The image filepath for bottom-right of the sliding overlay (set None to exclude) 1130 """ 1131 1132 self.display_loading_grayout(True, "Loading viewer with main image '" + filename_main_topleft.split("/")[-1] + "'...") 1133 1134 activeMdiChild = self.activeMdiChild 1135 QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) 1136 1137 transform_mode_smooth = self.is_global_transform_mode_smooth 1138 1139 pixmap = QtGui.QPixmap(filename_main_topleft) 1140 pixmap_topright = QtGui.QPixmap(filename_topright) 1141 pixmap_bottomleft = QtGui.QPixmap(filename_bottomleft) 1142 pixmap_bottomright = QtGui.QPixmap(filename_bottomright) 1143 1144 QtWidgets.QApplication.restoreOverrideCursor() 1145 1146 if (not pixmap or 1147 pixmap.width()==0 or pixmap.height==0): 1148 self.display_loading_grayout(True, "Waiting on dialog box...") 1149 QtWidgets.QMessageBox.warning(self, APPNAME, 1150 "Cannot read file %s." % (filename_main_topleft,)) 1151 self.updateRecentFileSettings(filename_main_topleft, delete=True) 1152 self.updateRecentFileActions() 1153 self.display_loading_grayout(False) 1154 return 1155 1156 angle = get_exif_rotation_angle(filename_main_topleft) 1157 if angle: 1158 pixmap = pixmap.transformed(QtGui.QTransform().rotate(angle)) 1159 1160 angle = get_exif_rotation_angle(filename_topright) 1161 if angle: 1162 pixmap_topright = pixmap_topright.transformed(QtGui.QTransform().rotate(angle)) 1163 1164 angle = get_exif_rotation_angle(filename_bottomright) 1165 if angle: 1166 pixmap_bottomright = pixmap_bottomright.transformed(QtGui.QTransform().rotate(angle)) 1167 1168 angle = get_exif_rotation_angle(filename_bottomleft) 1169 if angle: 1170 pixmap_bottomleft = pixmap_bottomleft.transformed(QtGui.QTransform().rotate(angle)) 1171 1172 child = self.createMdiChild(pixmap, filename_main_topleft, pixmap_topright, pixmap_bottomleft, pixmap_bottomright, transform_mode_smooth) 1173 1174 # Show filenames 1175 child.label_main_topleft.setText(filename_main_topleft) 1176 child.label_topright.setText(filename_topright) 1177 child.label_bottomright.setText(filename_bottomright) 1178 child.label_bottomleft.setText(filename_bottomleft) 1179 1180 child.show() 1181 1182 if activeMdiChild: 1183 if self._synchPanAct.isChecked(): 1184 self.synchPan(activeMdiChild) 1185 if self._synchZoomAct.isChecked(): 1186 self.synchZoom(activeMdiChild) 1187 1188 self._mdiArea.tile_what_was_done_last_time() 1189 1190 child.fitToWindow() 1191 child.set_close_pushbutton_always_visible(self.is_interface_showing) 1192 if self.scene_background_color is not None: 1193 child.set_scene_background_color(self.scene_background_color) 1194 1195 self.updateRecentFileSettings(filename_main_topleft) 1196 self.updateRecentFileActions() 1197 1198 self.display_loading_grayout(False) 1199 1200 self.statusBar().showMessage("File loaded", 2000) 1201 1202 def load_from_dragged_and_dropped_file(self, filename_main_topleft): 1203 """Load an individual image (convenience function — e.g., from a single emitted single filename).""" 1204 self.loadFile(filename_main_topleft) 1205 1206 def createMdiChild(self, pixmap, filename_main_topleft, pixmap_topright, pixmap_bottomleft, pixmap_bottomright, transform_mode_smooth): 1207 """Create new viewing widget for an individual image or sliding overlay to be placed in a new subwindow. 1208 1209 Args: 1210 pixmap (QPixmap): The main image to be viewed; the basis of the sliding overlay (main; topleft) 1211 filename_main_topleft (str): The image filepath of the main image. 1212 pixmap_topright (QPixmap): The top-right image of the sliding overlay (set None to exclude). 1213 pixmap_bottomleft (QPixmap): The bottom-left image of the sliding overlay (set None to exclude). 1214 pixmap_bottomright (QPixmap): The bottom-right image of the sliding overlay (set None to exclude). 1215 1216 Returns: 1217 child (SplitViewMdiChild): The viewing widget instance. 1218 """ 1219 1220 child = SplitViewMdiChild(pixmap, 1221 filename_main_topleft, 1222 "Window %d" % (len(self._mdiArea.subWindowList())+1), 1223 pixmap_topright, pixmap_bottomleft, pixmap_bottomright, 1224 transform_mode_smooth) 1225 1226 child.enableScrollBars(self._showScrollbarsAct.isChecked()) 1227 1228 self._mdiArea.addSubWindow(child, QtCore.Qt.FramelessWindowHint) # LVM: No frame, starts fitted 1229 1230 child.scrollChanged.connect(self.panChanged) 1231 child.transformChanged.connect(self.zoomChanged) 1232 1233 child.positionChanged.connect(self.on_positionChanged) 1234 child.tracker.mouse_leaved.connect(self.on_mouse_leaved) 1235 1236 child.scrollChanged.connect(self.on_scrollChanged) 1237 1238 child.became_closed.connect(self.on_subwindow_closed) 1239 child.was_clicked_close_pushbutton.connect(self._mdiArea.closeActiveSubWindow) 1240 child.shortcut_shift_x_was_activated.connect(self.shortcut_shift_x_was_activated_on_mdichild) 1241 child.signal_display_loading_grayout.connect(self.display_loading_grayout) 1242 child.was_set_global_transform_mode.connect(self.set_all_window_transform_mode_smooth) 1243 child.was_set_scene_background_color.connect(self.set_all_background_color) 1244 1245 return child 1246 1247 1248 # View and split methods 1249 1250 @QtCore.pyqtSlot() 1251 def on_create_splitview(self): 1252 """Load a sliding overlay using the filepaths of the current images in the sliding overlay creator.""" 1253 # Get filenames 1254 file_path_main_topleft = self._splitview_creator.drag_drop_area.app_main_topleft.file_path 1255 file_path_topright = self._splitview_creator.drag_drop_area.app_topright.file_path 1256 file_path_bottomleft = self._splitview_creator.drag_drop_area.app_bottomleft.file_path 1257 file_path_bottomright = self._splitview_creator.drag_drop_area.app_bottomright.file_path 1258 1259 # loadFile with those filenames 1260 self.loadFile(file_path_main_topleft, file_path_topright, file_path_bottomleft, file_path_bottomright) 1261 1262 def fit_to_window(self): 1263 """Fit the view of the active subwindow (if it exists).""" 1264 if self.activeMdiChild: 1265 self.activeMdiChild.fitToWindow() 1266 1267 def update_split(self): 1268 """Update the position of the split of the active subwindow (if it exists) relying on the global mouse coordinates.""" 1269 if self.activeMdiChild: 1270 self.activeMdiChild.update_split() # No input = Rely on global mouse position calculation 1271 1272 def lock_split(self): 1273 """Lock the position of the overlay split of active subwindow and set relevant interface elements.""" 1274 if self.activeMdiChild: 1275 self.activeMdiChild.split_locked = True 1276 self._splitview_manager.lock_split_pushbutton.setChecked(True) 1277 self.update_window_highlight(self._mdiArea.activeSubWindow()) 1278 1279 def unlock_split(self): 1280 """Unlock the position of the overlay split of active subwindow and set relevant interface elements.""" 1281 if self.activeMdiChild: 1282 self.activeMdiChild.split_locked = False 1283 self._splitview_manager.lock_split_pushbutton.setChecked(False) 1284 self.update_window_highlight(self._mdiArea.activeSubWindow()) 1285 1286 def set_split(self, x_percent=0.5, y_percent=0.5, apply_to_all=True, ignore_lock=False, percent_of_visible=False): 1287 """Set the position of the split of the active subwindow as percent of base image's resolution. 1288 1289 Args: 1290 x_percent (float): The position of the split as a proportion (0-1) of the base image's horizontal resolution. 1291 y_percent (float): The position of the split as a proportion (0-1) of the base image's vertical resolution. 1292 apply_to_all (bool): True to set all subwindow splits; False to set only the active subwindow. 1293 ignore_lock (bool): True to ignore the lock status of the split; False to adhere. 1294 percent_of_visible (bool): True to set split as proportion of visible area; False as proportion of the full image resolution. 1295 """ 1296 if self.activeMdiChild: 1297 self.activeMdiChild.set_split(x_percent, y_percent, ignore_lock=ignore_lock, percent_of_visible=percent_of_visible) 1298 if apply_to_all: 1299 windows = self._mdiArea.subWindowList() 1300 for window in windows: 1301 window.widget().set_split(x_percent, y_percent, ignore_lock=ignore_lock, percent_of_visible=percent_of_visible) 1302 self.update_window_highlight(self._mdiArea.activeSubWindow()) 1303 1304 def set_split_from_slider(self): 1305 """Set the position of the split of the active subwindow to the center of the visible area of the sliding overlay (convenience function).""" 1306 self.set_split(x_percent=0.5, y_percent=0.5, apply_to_all=False, ignore_lock=False, percent_of_visible=True) 1307 1308 def set_split_from_manager(self, x_percent, y_percent): 1309 """Set the position of the split of the active subwindow as percent of base image's resolution (convenience function). 1310 1311 Args: 1312 x_percent (float): The position of the split as a proportion of the base image's horizontal resolution (0-1). 1313 y_percent (float): The position of the split as a proportion of the base image's vertical resolution (0-1). 1314 """ 1315 self.set_split(x_percent, y_percent, apply_to_all=False, ignore_lock=False) 1316 1317 def set_and_lock_split_from_manager(self, x_percent, y_percent): 1318 """Set and lock the position of the split of the active subwindow as percent of base image's resolution (convenience function). 1319 1320 Args: 1321 x_percent (float): The position of the split as a proportion of the base image's horizontal resolution (0-1). 1322 y_percent (float): The position of the split as a proportion of the base image's vertical resolution (0-1). 1323 """ 1324 self.set_split(x_percent, y_percent, apply_to_all=False, ignore_lock=True) 1325 self.lock_split() 1326 1327 def shortcut_shift_x_was_activated_on_mdichild(self): 1328 """Update interface button for split lock based on lock status of active subwindow.""" 1329 self._splitview_manager.lock_split_pushbutton.setChecked(self.activeMdiChild.split_locked) 1330 1331 @QtCore.pyqtSlot() 1332 def on_scrollChanged(self): 1333 """Refresh position of split of all subwindows based on their respective last position.""" 1334 windows = self._mdiArea.subWindowList() 1335 for window in windows: 1336 window.widget().refresh_split_based_on_last_updated_point_of_split_on_scene_main() 1337 1338 def on_subwindow_closed(self): 1339 """Record that a subwindow was closed upon the closing of a subwindow.""" 1340 self.subwindow_was_just_closed = True 1341 1342 @QtCore.pyqtSlot() 1343 def on_mouse_leaved(self): 1344 """Update displayed coordinates of mouse as N/A upon the mouse leaving the subwindow area.""" 1345 self._label_mouse.setText("View pixel coordinates: ( N/A , N/A )") 1346 self._label_mouse.adjustSize() 1347 1348 @QtCore.pyqtSlot(QtCore.QPoint) 1349 def on_positionChanged(self, pos): 1350 """Update displayed coordinates of mouse on the active subwindow using global coordinates.""" 1351 1352 point_of_mouse_on_viewport = QtCore.QPointF(pos.x(), pos.y()) 1353 pos_qcursor_global = QtGui.QCursor.pos() 1354 1355 if self.activeMdiChild: 1356 1357 # Use mouse position to grab scene coordinates (activeMdiChild?) 1358 active_view = self.activeMdiChild._view_main_topleft 1359 point_of_mouse_on_scene = active_view.mapToScene(point_of_mouse_on_viewport.x(), point_of_mouse_on_viewport.y()) 1360 1361 if not self._label_mouse.isVisible(): 1362 self._label_mouse.show() 1363 self._label_mouse.setText("View pixel coordinates: ( x = %d , y = %d )" % (point_of_mouse_on_scene.x(), point_of_mouse_on_scene.y())) 1364 1365 pos_qcursor_view = active_view.mapFromGlobal(pos_qcursor_global) 1366 pos_qcursor_scene = active_view.mapToScene(pos_qcursor_view) 1367 # print("Cursor coords scene: ( %d , %d )" % (pos_qcursor_scene.x(), pos_qcursor_scene.y())) 1368 1369 else: 1370 1371 self._label_mouse.setText("View pixel coordinates: ( N/A , N/A )") 1372 1373 self._label_mouse.adjustSize() 1374 1375 1376 # Transparency methods 1377 1378 1379 @QtCore.pyqtSlot(int) 1380 def on_slider_opacity_base_changed(self, value): 1381 """Set transparency of base of sliding overlay of active subwindow. 1382 1383 Triggered upon change in interface transparency slider. 1384 Temporarily sets position of split to the center of the visible area to give user a preview of the transparency effect. 1385 1386 Args: 1387 value (float,int): The transparency as percent opacity, where 100 is opaque (not transparent) and 0 is transparent (0-100). 1388 """ 1389 if not self.activeMdiChild: 1390 return 1391 if not self.activeMdiChild.split_locked: 1392 self.set_split_from_slider() 1393 self.activeMdiChild.set_opacity_base(value) 1394 1395 @QtCore.pyqtSlot(int) 1396 def on_slider_opacity_topright_changed(self, value): 1397 """Set transparency of top-right of sliding overlay of active subwindow. 1398 1399 Triggered upon change in interface transparency slider. 1400 Temporarily sets position of split to the center of the visible area to give user a preview of the transparency effect. 1401 1402 Args: 1403 value (float,int): The transparency as percent opacity, where 100 is opaque (not transparent) and 0 is transparent (0-100). 1404 """ 1405 if not self.activeMdiChild: 1406 return 1407 if not self.activeMdiChild.split_locked: 1408 self.set_split_from_slider() 1409 self.activeMdiChild.set_opacity_topright(value) 1410 1411 @QtCore.pyqtSlot(int) 1412 def on_slider_opacity_bottomright_changed(self, value): 1413 """Set transparency of bottom-right of sliding overlay of active subwindow. 1414 1415 Triggered upon change in interface transparency slider. 1416 Temporarily sets position of split to the center of the visible area to give user a preview of the transparency effect. 1417 1418 Args: 1419 value (float,int): The transparency as percent opacity, where 100 is opaque (not transparent) and 0 is transparent (0-100). 1420 """ 1421 if not self.activeMdiChild: 1422 return 1423 if not self.activeMdiChild.split_locked: 1424 self.set_split_from_slider() 1425 self.activeMdiChild.set_opacity_bottomright(value) 1426 1427 @QtCore.pyqtSlot(int) 1428 def on_slider_opacity_bottomleft_changed(self, value): 1429 """Set transparency of bottom-left of sliding overlay of active subwindow. 1430 1431 Triggered upon change in interface transparency slider. 1432 Temporarily sets position of split to the center of the visible area to give user a preview of the transparency effect. 1433 1434 Args: 1435 value (float,int): The transparency as percent opacity, where 100 is opaque (not transparent) and 0 is transparent (0-100). 1436 """ 1437 if not self.activeMdiChild: 1438 return 1439 if not self.activeMdiChild.split_locked: 1440 self.set_split_from_slider() 1441 self.activeMdiChild.set_opacity_bottomleft(value) 1442 1443 def update_sliders(self, window): 1444 """Update interface transparency sliders upon subwindow activating using the subwindow transparency values. 1445 1446 Args: 1447 window (QMdiSubWindow): The active subwindow. 1448 """ 1449 if window is None: 1450 self._sliders_opacity_splitviews.reset_sliders() 1451 return 1452 1453 child = self.activeMdiChild 1454 1455 self._sliders_opacity_splitviews.set_enabled(True, child.pixmap_topright_exists, child.pixmap_bottomright_exists, child.pixmap_bottomleft_exists) 1456 1457 opacity_base_of_activeMdiChild = child._opacity_base 1458 opacity_topright_of_activeMdiChild = child._opacity_topright 1459 opacity_bottomright_of_activeMdiChild = child._opacity_bottomright 1460 opacity_bottomleft_of_activeMdiChild = child._opacity_bottomleft 1461 1462 self._sliders_opacity_splitviews.update_sliders(opacity_base_of_activeMdiChild, opacity_topright_of_activeMdiChild, opacity_bottomright_of_activeMdiChild, opacity_bottomleft_of_activeMdiChild) 1463 1464 1465 # [Legacy methods from derived MDI Image Viewer] 1466 1467 def createMappedAction(self, icon, text, parent, shortcut, methodName): 1468 """Create |QAction| that is mapped via methodName to call. 1469 1470 :param icon: icon associated with |QAction| 1471 :type icon: |QIcon| or None 1472 :param str text: the |QAction| descriptive text 1473 :param QObject parent: the parent |QObject| 1474 :param QKeySequence shortcut: the shortcut |QKeySequence| 1475 :param str methodName: name of method to call when |QAction| is 1476 triggered 1477 :rtype: |QAction|""" 1478 1479 if icon is not None: 1480 action = QtWidgets.QAction(icon, text, parent, 1481 shortcut=shortcut, 1482 triggered=self._actionMapper.map) 1483 else: 1484 action = QtWidgets.QAction(text, parent, 1485 shortcut=shortcut, 1486 triggered=self._actionMapper.map) 1487 self._actionMapper.setMapping(action, methodName) 1488 return action 1489 1490 def createActions(self): 1491 """Create actions used in menus.""" 1492 #File menu actions 1493 self._openAct = QtWidgets.QAction( 1494 "&Open...", self, 1495 shortcut=QtGui.QKeySequence.Open, 1496 statusTip="Open an existing file", 1497 triggered=self.open) 1498 1499 self._switchLayoutDirectionAct = QtWidgets.QAction( 1500 "Switch &layout direction", self, 1501 triggered=self.switchLayoutDirection) 1502 1503 #create dummy recent file actions 1504 for i in range(MultiViewMainWindow.MaxRecentFiles): 1505 self._recentFileActions.append( 1506 QtWidgets.QAction(self, visible=False, 1507 triggered=self._recentFileMapper.map)) 1508 1509 self._exitAct = QtWidgets.QAction( 1510 "E&xit", self, 1511 shortcut=QtGui.QKeySequence.Quit, 1512 statusTip="Exit the application", 1513 triggered=QtWidgets.QApplication.closeAllWindows) 1514 1515 #View menu actions 1516 self._showScrollbarsAct = QtWidgets.QAction( 1517 "&Scrollbars", self, 1518 checkable=True, 1519 statusTip="Toggle display of subwindow scrollbars", 1520 triggered=self.toggleScrollbars) 1521 1522 self._showStatusbarAct = QtWidgets.QAction( 1523 "S&tatusbar", self, 1524 checkable=True, 1525 statusTip="Toggle display of statusbar", 1526 triggered=self.toggleStatusbar) 1527 1528 self._synchZoomAct = QtWidgets.QAction( 1529 "Synch &Zoom", self, 1530 checkable=True, 1531 statusTip="Synch zooming of subwindows", 1532 triggered=self.toggleSynchZoom) 1533 1534 self._synchPanAct = QtWidgets.QAction( 1535 "Synch &Pan", self, 1536 checkable=True, 1537 statusTip="Synch panning of subwindows", 1538 triggered=self.toggleSynchPan) 1539 1540 #Scroll menu actions 1541 self._scrollActions = [ 1542 self.createMappedAction( 1543 None, 1544 "&Top", self, 1545 QtGui.QKeySequence.MoveToStartOfDocument, 1546 "scrollToTop"), 1547 1548 self.createMappedAction( 1549 None, 1550 "&Bottom", self, 1551 QtGui.QKeySequence.MoveToEndOfDocument, 1552 "scrollToBottom"), 1553 1554 self.createMappedAction( 1555 None, 1556 "&Left Edge", self, 1557 QtGui.QKeySequence.MoveToStartOfLine, 1558 "scrollToBegin"), 1559 1560 self.createMappedAction( 1561 None, 1562 "&Right Edge", self, 1563 QtGui.QKeySequence.MoveToEndOfLine, 1564 "scrollToEnd"), 1565 1566 self.createMappedAction( 1567 None, 1568 "&Center", self, 1569 "5", 1570 "centerView"), 1571 ] 1572 1573 #zoom menu actions 1574 separatorAct = QtWidgets.QAction(self) 1575 separatorAct.setSeparator(True) 1576 1577 self._zoomActions = [ 1578 self.createMappedAction( 1579 None, 1580 "Zoo&m In (25%)", self, 1581 QtGui.QKeySequence.ZoomIn, 1582 "zoomIn"), 1583 1584 self.createMappedAction( 1585 None, 1586 "Zoom &Out (25%)", self, 1587 QtGui.QKeySequence.ZoomOut, 1588 "zoomOut"), 1589 1590 #self.createMappedAction( 1591 #None, 1592 #"&Zoom To...", self, 1593 #"Z", 1594 #"zoomTo"), 1595 1596 separatorAct, 1597 1598 self.createMappedAction( 1599 None, 1600 "Actual &Size", self, 1601 "/", 1602 "actualSize"), 1603 1604 self.createMappedAction( 1605 None, 1606 "Fit &Image", self, 1607 "*", 1608 "fitToWindow"), 1609 1610 self.createMappedAction( 1611 None, 1612 "Fit &Width", self, 1613 "Alt+Right", 1614 "fitWidth"), 1615 1616 self.createMappedAction( 1617 None, 1618 "Fit &Height", self, 1619 "Alt+Down", 1620 "fitHeight"), 1621 ] 1622 1623 #Window menu actions 1624 self._activateSubWindowSystemMenuAct = QtWidgets.QAction( 1625 "Activate &System Menu", self, 1626 shortcut="Ctrl+ ", 1627 statusTip="Activate subwindow System Menu", 1628 triggered=self.activateSubwindowSystemMenu) 1629 1630 self._closeAct = QtWidgets.QAction( 1631 "Cl&ose", self, 1632 shortcut=QtGui.QKeySequence.Close, 1633 shortcutContext=QtCore.Qt.WidgetShortcut, 1634 #shortcut="Ctrl+Alt+F4", 1635 statusTip="Close the active window", 1636 triggered=self._mdiArea.closeActiveSubWindow) 1637 1638 self._closeAllAct = QtWidgets.QAction( 1639 "Close &All", self, 1640 statusTip="Close all the windows", 1641 triggered=self._mdiArea.closeAllSubWindows) 1642 1643 self._tileAct = QtWidgets.QAction( 1644 "&Tile", self, 1645 statusTip="Tile the windows", 1646 triggered=self._mdiArea.tileSubWindows) 1647 1648 self._tileAct.triggered.connect(self.tile_and_fit_mdiArea) 1649 1650 self._cascadeAct = QtWidgets.QAction( 1651 "&Cascade", self, 1652 statusTip="Cascade the windows", 1653 triggered=self._mdiArea.cascadeSubWindows) 1654 1655 self._nextAct = QtWidgets.QAction( 1656 "Ne&xt", self, 1657 shortcut=QtGui.QKeySequence.NextChild, 1658 statusTip="Move the focus to the next window", 1659 triggered=self._mdiArea.activateNextSubWindow) 1660 1661 self._previousAct = QtWidgets.QAction( 1662 "Pre&vious", self, 1663 shortcut=QtGui.QKeySequence.PreviousChild, 1664 statusTip="Move the focus to the previous window", 1665 triggered=self._mdiArea.activatePreviousSubWindow) 1666 1667 self._separatorAct = QtWidgets.QAction(self) 1668 self._separatorAct.setSeparator(True) 1669 1670 self._aboutAct = QtWidgets.QAction( 1671 "&About", self, 1672 statusTip="Show the application's About box", 1673 triggered=self.about) 1674 1675 self._aboutQtAct = QtWidgets.QAction( 1676 "About &Qt", self, 1677 statusTip="Show the Qt library's About box", 1678 triggered=QtWidgets.QApplication.aboutQt) 1679 1680 def createMenus(self): 1681 """Create menus.""" 1682 self._fileMenu = self.menuBar().addMenu("&File") 1683 self._fileMenu.addAction(self._openAct) 1684 self._fileMenu.addAction(self._switchLayoutDirectionAct) 1685 1686 self._fileSeparatorAct = self._fileMenu.addSeparator() 1687 for action in self._recentFileActions: 1688 self._fileMenu.addAction(action) 1689 self.updateRecentFileActions() 1690 self._fileMenu.addSeparator() 1691 self._fileMenu.addAction(self._exitAct) 1692 1693 self._viewMenu = self.menuBar().addMenu("&View") 1694 self._viewMenu.addAction(self._showScrollbarsAct) 1695 self._viewMenu.addAction(self._showStatusbarAct) 1696 self._viewMenu.addSeparator() 1697 self._viewMenu.addAction(self._synchZoomAct) 1698 self._viewMenu.addAction(self._synchPanAct) 1699 1700 self._scrollMenu = self.menuBar().addMenu("&Scroll") 1701 [self._scrollMenu.addAction(action) for action in self._scrollActions] 1702 1703 self._zoomMenu = self.menuBar().addMenu("&Zoom") 1704 [self._zoomMenu.addAction(action) for action in self._zoomActions] 1705 1706 self._windowMenu = self.menuBar().addMenu("&Window") 1707 self.updateWindowMenu() 1708 self._windowMenu.aboutToShow.connect(self.updateWindowMenu) 1709 1710 self.menuBar().addSeparator() 1711 1712 self._helpMenu = self.menuBar().addMenu("&Help") 1713 self._helpMenu.addAction(self._aboutAct) 1714 self._helpMenu.addAction(self._aboutQtAct) 1715 1716 def updateMenus(self): 1717 """Update menus.""" 1718 hasMdiChild = (self.activeMdiChild is not None) 1719 1720 self._scrollMenu.setEnabled(hasMdiChild) 1721 self._zoomMenu.setEnabled(hasMdiChild) 1722 1723 self._closeAct.setEnabled(hasMdiChild) 1724 self._closeAllAct.setEnabled(hasMdiChild) 1725 1726 self._tileAct.setEnabled(hasMdiChild) 1727 self._cascadeAct.setEnabled(hasMdiChild) 1728 self._nextAct.setEnabled(hasMdiChild) 1729 self._previousAct.setEnabled(hasMdiChild) 1730 self._separatorAct.setVisible(hasMdiChild) 1731 1732 def updateRecentFileActions(self): 1733 """Update recent file menu items.""" 1734 settings = QtCore.QSettings() 1735 files = settings.value(SETTING_RECENTFILELIST) 1736 numRecentFiles = min(len(files) if files else 0, 1737 MultiViewMainWindow.MaxRecentFiles) 1738 1739 for i in range(numRecentFiles): 1740 text = "&%d %s" % (i + 1, strippedName(files[i])) 1741 self._recentFileActions[i].setText(text) 1742 self._recentFileActions[i].setData(files[i]) 1743 self._recentFileActions[i].setVisible(True) 1744 self._recentFileMapper.setMapping(self._recentFileActions[i], 1745 files[i]) 1746 1747 for j in range(numRecentFiles, MultiViewMainWindow.MaxRecentFiles): 1748 self._recentFileActions[j].setVisible(False) 1749 1750 self._fileSeparatorAct.setVisible((numRecentFiles > 0)) 1751 1752 def updateWindowMenu(self): 1753 """Update the Window menu.""" 1754 self._windowMenu.clear() 1755 self._windowMenu.addAction(self._closeAct) 1756 self._windowMenu.addAction(self._closeAllAct) 1757 self._windowMenu.addSeparator() 1758 self._windowMenu.addAction(self._tileAct) 1759 self._windowMenu.addAction(self._cascadeAct) 1760 self._windowMenu.addSeparator() 1761 self._windowMenu.addAction(self._nextAct) 1762 self._windowMenu.addAction(self._previousAct) 1763 self._windowMenu.addAction(self._separatorAct) 1764 1765 windows = self._mdiArea.subWindowList() 1766 self._separatorAct.setVisible(len(windows) != 0) 1767 1768 for i, window in enumerate(windows): 1769 child = window.widget() 1770 1771 text = "%d %s" % (i + 1, child.userFriendlyCurrentFile) 1772 if i < 9: 1773 text = '&' + text 1774 1775 action = self._windowMenu.addAction(text) 1776 action.setCheckable(True) 1777 action.setChecked(child == self.activeMdiChild) 1778 action.triggered.connect(self._windowMapper.map) 1779 self._windowMapper.setMapping(action, window) 1780 1781 def createStatusBarLabel(self, stretch=0): 1782 """Create status bar label. 1783 1784 :param int stretch: stretch factor 1785 :rtype: |QLabel|""" 1786 label = QtWidgets.QLabel() 1787 label.setFrameStyle(QtWidgets.QFrame.Panel | QtWidgets.QFrame.Sunken) 1788 label.setLineWidth(2) 1789 self.statusBar().addWidget(label, stretch) 1790 return label 1791 1792 def createStatusBar(self): 1793 """Create status bar.""" 1794 statusBar = self.statusBar() 1795 1796 self._sbLabelName = self.createStatusBarLabel(1) 1797 self._sbLabelSize = self.createStatusBarLabel() 1798 self._sbLabelDimensions = self.createStatusBarLabel() 1799 self._sbLabelDate = self.createStatusBarLabel() 1800 self._sbLabelZoom = self.createStatusBarLabel() 1801 1802 statusBar.showMessage("Ready") 1803 1804 1805 @property 1806 def activeMdiChild(self): 1807 """Get active MDI child (:class:`SplitViewMdiChild` or *None*).""" 1808 activeSubWindow = self._mdiArea.activeSubWindow() 1809 if activeSubWindow: 1810 return activeSubWindow.widget() 1811 return None 1812 1813 1814 def closeEvent(self, event): 1815 """Overrides close event to save application settings. 1816 1817 :param QEvent event: instance of |QEvent|""" 1818 1819 if self.is_fullscreen: # Needed to properly close the image viewer if the main window is closed while the viewer is fullscreen 1820 self.is_fullscreen = False 1821 self.setCentralWidget(self.mdiarea_plus_buttons) 1822 1823 self._mdiArea.closeAllSubWindows() 1824 if self.activeMdiChild: 1825 event.ignore() 1826 else: 1827 self.writeSettings() 1828 event.accept() 1829 1830 1831 def tile_and_fit_mdiArea(self): 1832 self._mdiArea.tileSubWindows() 1833 1834 1835 # Synchronized pan and zoom methods 1836 1837 @QtCore.pyqtSlot(str) 1838 def mappedImageViewerAction(self, methodName): 1839 """Perform action mapped to :class:`aux_splitview.SplitView` 1840 methodName. 1841 1842 :param str methodName: method to call""" 1843 activeViewer = self.activeMdiChild 1844 if hasattr(activeViewer, str(methodName)): 1845 getattr(activeViewer, str(methodName))() 1846 1847 @QtCore.pyqtSlot() 1848 def toggleSynchPan(self): 1849 """Toggle synchronized subwindow panning.""" 1850 if self._synchPanAct.isChecked(): 1851 self.synchPan(self.activeMdiChild) 1852 1853 @QtCore.pyqtSlot() 1854 def panChanged(self): 1855 """Synchronize subwindow pans.""" 1856 mdiChild = self.sender() 1857 while mdiChild is not None and type(mdiChild) != SplitViewMdiChild: 1858 mdiChild = mdiChild.parent() 1859 if mdiChild and self._synchPanAct.isChecked(): 1860 self.synchPan(mdiChild) 1861 1862 @QtCore.pyqtSlot() 1863 def toggleSynchZoom(self): 1864 """Toggle synchronized subwindow zooming.""" 1865 if self._synchZoomAct.isChecked(): 1866 self.synchZoom(self.activeMdiChild) 1867 1868 @QtCore.pyqtSlot() 1869 def zoomChanged(self): 1870 """Synchronize subwindow zooms.""" 1871 mdiChild = self.sender() 1872 if self._synchZoomAct.isChecked(): 1873 self.synchZoom(mdiChild) 1874 self.updateStatusBar() 1875 1876 def synchPan(self, fromViewer): 1877 """Synch panning of all subwindowws to the same as *fromViewer*. 1878 1879 :param fromViewer: :class:`SplitViewMdiChild` that initiated synching""" 1880 1881 assert isinstance(fromViewer, SplitViewMdiChild) 1882 if not fromViewer: 1883 return 1884 if self._handlingScrollChangedSignal: 1885 return 1886 if fromViewer.parent() != self._mdiArea.activeSubWindow(): # Prevent circular scroll state change signals from propagating 1887 if fromViewer.parent() != self: 1888 return 1889 self._handlingScrollChangedSignal = True 1890 1891 newState = fromViewer.scrollState 1892 changedWindow = fromViewer.parent() 1893 windows = self._mdiArea.subWindowList() 1894 for window in windows: 1895 if window != changedWindow: 1896 window.widget().scrollState = newState 1897 window.widget().resize_scene() 1898 1899 self._handlingScrollChangedSignal = False 1900 1901 def synchZoom(self, fromViewer): 1902 """Synch zoom of all subwindowws to the same as *fromViewer*. 1903 1904 :param fromViewer: :class:`SplitViewMdiChild` that initiated synching""" 1905 if not fromViewer: 1906 return 1907 newZoomFactor = fromViewer.zoomFactor 1908 changedWindow = fromViewer.parent() 1909 windows = self._mdiArea.subWindowList() 1910 for window in windows: 1911 if window != changedWindow: 1912 window.widget().zoomFactor = newZoomFactor 1913 window.widget().resize_scene() 1914 self.refreshPan() 1915 1916 def refreshPan(self): 1917 if self.activeMdiChild: 1918 self.synchPan(self.activeMdiChild) 1919 1920 def refreshPanDelayed(self, ms=0): 1921 QtCore.QTimer.singleShot(ms, self.refreshPan) 1922 1923 1924 # Methods from PyQt MDI Image Viewer left unaltered 1925 1926 @QtCore.pyqtSlot() 1927 def activateSubwindowSystemMenu(self): 1928 """Activate current subwindow's System Menu.""" 1929 activeSubWindow = self._mdiArea.activeSubWindow() 1930 if activeSubWindow: 1931 activeSubWindow.showSystemMenu() 1932 1933 @QtCore.pyqtSlot(str) 1934 def openRecentFile(self, filename_main_topleft): 1935 """Open a recent file. 1936 1937 :param str filename_main_topleft: filename_main_topleft to view""" 1938 self.loadFile(filename_main_topleft, None, None, None) 1939 1940 @QtCore.pyqtSlot() 1941 def open(self): 1942 """Handle the open action.""" 1943 fileDialog = QtWidgets.QFileDialog(self) 1944 settings = QtCore.QSettings() 1945 fileDialog.setNameFilters(["Image Files (*.jpg *.png *.tif)", 1946 "All Files (*)"]) 1947 if not settings.contains(SETTING_FILEOPEN + "/state"): 1948 fileDialog.setDirectory(".") 1949 else: 1950 self.restoreDialogState(fileDialog, SETTING_FILEOPEN) 1951 fileDialog.setFileMode(QtWidgets.QFileDialog.ExistingFile) 1952 if not fileDialog.exec_(): 1953 return 1954 self.saveDialogState(fileDialog, SETTING_FILEOPEN) 1955 1956 filename_main_topleft = fileDialog.selectedFiles()[0] 1957 self.loadFile(filename_main_topleft, None, None, None) 1958 1959 1960 @QtCore.pyqtSlot() 1961 def toggleScrollbars(self): 1962 """Toggle subwindow scrollbar visibility.""" 1963 checked = self._showScrollbarsAct.isChecked() 1964 1965 windows = self._mdiArea.subWindowList() 1966 for window in windows: 1967 child = window.widget() 1968 child.enableScrollBars(checked) 1969 1970 @QtCore.pyqtSlot() 1971 def toggleStatusbar(self): 1972 """Toggle status bar visibility.""" 1973 self.statusBar().setVisible(self._showStatusbarAct.isChecked()) 1974 1975 1976 @QtCore.pyqtSlot() 1977 def about(self): 1978 """Display About dialog box.""" 1979 QtWidgets.QMessageBox.about(self, "About MDI", 1980 "<b>MDI Image Viewer</b> demonstrates how to" 1981 "synchronize the panning and zooming of multiple image" 1982 "viewer windows using Qt.") 1983 @QtCore.pyqtSlot(QtWidgets.QMdiSubWindow) 1984 def subWindowActivated(self, window): 1985 """Handle |QMdiSubWindow| activated signal. 1986 1987 :param |QMdiSubWindow| window: |QMdiSubWindow| that was just 1988 activated""" 1989 self.updateStatusBar() 1990 1991 @QtCore.pyqtSlot(QtWidgets.QMdiSubWindow) 1992 def setActiveSubWindow(self, window): 1993 """Set active |QMdiSubWindow|. 1994 1995 :param |QMdiSubWindow| window: |QMdiSubWindow| to activate """ 1996 if window: 1997 self._mdiArea.setActiveSubWindow(window) 1998 1999 2000 def updateStatusBar(self): 2001 """Update status bar.""" 2002 self.statusBar().setVisible(self._showStatusbarAct.isChecked()) 2003 imageViewer = self.activeMdiChild 2004 if not imageViewer: 2005 self._sbLabelName.setText("") 2006 self._sbLabelSize.setText("") 2007 self._sbLabelDimensions.setText("") 2008 self._sbLabelDate.setText("") 2009 self._sbLabelZoom.setText("") 2010 2011 self._sbLabelSize.hide() 2012 self._sbLabelDimensions.hide() 2013 self._sbLabelDate.hide() 2014 self._sbLabelZoom.hide() 2015 return 2016 2017 filename_main_topleft = imageViewer.currentFile 2018 self._sbLabelName.setText(" %s " % filename_main_topleft) 2019 2020 fi = QtCore.QFileInfo(filename_main_topleft) 2021 size = fi.size() 2022 fmt = " %.1f %s " 2023 if size > 1024*1024*1024: 2024 unit = "MB" 2025 size /= 1024*1024*1024 2026 elif size > 1024*1024: 2027 unit = "MB" 2028 size /= 1024*1024 2029 elif size > 1024: 2030 unit = "KB" 2031 size /= 1024 2032 else: 2033 unit = "Bytes" 2034 fmt = " %d %s " 2035 self._sbLabelSize.setText(fmt % (size, unit)) 2036 2037 pixmap = imageViewer.pixmap_main_topleft 2038 self._sbLabelDimensions.setText(" %dx%dx%d " % 2039 (pixmap.width(), 2040 pixmap.height(), 2041 pixmap.depth())) 2042 2043 self._sbLabelDate.setText( 2044 " %s " % 2045 fi.lastModified().toString(QtCore.Qt.SystemLocaleShortDate)) 2046 self._sbLabelZoom.setText(" %0.f%% " % (imageViewer.zoomFactor*100,)) 2047 2048 self._sbLabelSize.show() 2049 self._sbLabelDimensions.show() 2050 self._sbLabelDate.show() 2051 self._sbLabelZoom.show() 2052 2053 def switchLayoutDirection(self): 2054 """Switch MDI subwindow layout direction.""" 2055 if self.layoutDirection() == QtCore.Qt.LeftToRight: 2056 QtWidgets.QApplication.setLayoutDirection(QtCore.Qt.RightToLeft) 2057 else: 2058 QtWidgets.QApplication.setLayoutDirection(QtCore.Qt.LeftToRight) 2059 2060 def saveDialogState(self, dialog, groupName): 2061 """Save dialog state, position & size. 2062 2063 :param |QDialog| dialog: dialog to save state of 2064 :param str groupName: |QSettings| group name""" 2065 assert isinstance(dialog, QtWidgets.QDialog) 2066 2067 settings = QtCore.QSettings() 2068 settings.beginGroup(groupName) 2069 2070 settings.setValue('state', dialog.saveState()) 2071 settings.setValue('geometry', dialog.saveGeometry()) 2072 settings.setValue('filter', dialog.selectedNameFilter()) 2073 2074 settings.endGroup() 2075 2076 def restoreDialogState(self, dialog, groupName): 2077 """Restore dialog state, position & size. 2078 2079 :param str groupName: |QSettings| group name""" 2080 assert isinstance(dialog, QtWidgets.QDialog) 2081 2082 settings = QtCore.QSettings() 2083 settings.beginGroup(groupName) 2084 2085 dialog.restoreState(settings.value('state')) 2086 dialog.restoreGeometry(settings.value('geometry')) 2087 dialog.selectNameFilter(settings.value('filter', "")) 2088 2089 settings.endGroup() 2090 2091 def writeSettings(self): 2092 """Write application settings.""" 2093 settings = QtCore.QSettings() 2094 settings.setValue('pos', self.pos()) 2095 settings.setValue('size', self.size()) 2096 settings.setValue('windowgeometry', self.saveGeometry()) 2097 settings.setValue('windowstate', self.saveState()) 2098 2099 settings.setValue(SETTING_SCROLLBARS, 2100 self._showScrollbarsAct.isChecked()) 2101 settings.setValue(SETTING_STATUSBAR, 2102 self._showStatusbarAct.isChecked()) 2103 settings.setValue(SETTING_SYNCHZOOM, 2104 self._synchZoomAct.isChecked()) 2105 settings.setValue(SETTING_SYNCHPAN, 2106 self._synchPanAct.isChecked()) 2107 2108 def readSettings(self): 2109 """Read application settings.""" 2110 2111 scrollbars_always_checked_off_at_startup = True 2112 statusbar_always_checked_off_at_startup = True 2113 sync_always_checked_on_at_startup = True 2114 2115 settings = QtCore.QSettings() 2116 2117 pos = settings.value('pos', QtCore.QPoint(100, 100)) 2118 size = settings.value('size', QtCore.QSize(1100, 600)) 2119 self.move(pos) 2120 self.resize(size) 2121 2122 if settings.contains('windowgeometry'): 2123 self.restoreGeometry(settings.value('windowgeometry')) 2124 if settings.contains('windowstate'): 2125 self.restoreState(settings.value('windowstate')) 2126 2127 2128 if scrollbars_always_checked_off_at_startup: 2129 self._showScrollbarsAct.setChecked(False) 2130 else: 2131 self._showScrollbarsAct.setChecked( 2132 toBool(settings.value(SETTING_SCROLLBARS, False))) 2133 2134 if statusbar_always_checked_off_at_startup: 2135 self._showStatusbarAct.setChecked(False) 2136 else: 2137 self._showStatusbarAct.setChecked( 2138 toBool(settings.value(SETTING_STATUSBAR, False))) 2139 2140 if sync_always_checked_on_at_startup: 2141 self._synchZoomAct.setChecked(True) 2142 self._synchPanAct.setChecked(True) 2143 else: 2144 self._synchZoomAct.setChecked( 2145 toBool(settings.value(SETTING_SYNCHZOOM, False))) 2146 self._synchPanAct.setChecked( 2147 toBool(settings.value(SETTING_SYNCHPAN, False))) 2148 2149 def updateRecentFileSettings(self, filename_main_topleft, delete=False): 2150 """Update recent file list setting. 2151 2152 :param str filename_main_topleft: filename_main_topleft to add or remove from recent file 2153 list 2154 :param bool delete: if True then filename_main_topleft removed, otherwise added""" 2155 settings = QtCore.QSettings() 2156 files = list(settings.value(SETTING_RECENTFILELIST, [])) 2157 2158 try: 2159 files.remove(filename_main_topleft) 2160 except ValueError: 2161 pass 2162 2163 if not delete: 2164 files.insert(0, filename_main_topleft) 2165 del files[MultiViewMainWindow.MaxRecentFiles:] 2166 2167 settings.setValue(SETTING_RECENTFILELIST, files)
View multiple images with split-effect and synchronized panning and zooming.
Extends QMainWindow as main window of Butterfly Viewer with user interface:
- Create sliding overlays.
- Adjust sliding overlay transparencies.
- Change viewer settings.
693 def copy_view(self): 694 """Screenshot MultiViewMainWindow and copy to clipboard as image.""" 695 696 self.display_loading_grayout(True, "Screenshot copied to clipboard.") 697 698 interface_was_already_set_hidden = not self.is_interface_showing # Needed to hide the interface temporarily while grabbing a screenshot (makes sure the screenshot only shows the views) 699 if not interface_was_already_set_hidden: 700 self.show_interface_off() 701 702 pixmap = self._mdiArea.grab() 703 clipboard = QtWidgets.QApplication.clipboard() 704 clipboard.setPixmap(pixmap) 705 706 if not interface_was_already_set_hidden: 707 self.show_interface_on() 708 709 self.display_loading_grayout(False, pseudo_load_time=1)
Screenshot MultiViewMainWindow and copy to clipboard as image.
712 def save_view(self): 713 """Screenshot MultiViewMainWindow and open Save dialog to save screenshot as image.""" 714 715 self.display_loading_grayout(True, "Saving viewer screenshot...") 716 717 folderpath = None 718 719 if self.activeMdiChild: 720 folderpath = self.activeMdiChild.currentFile 721 folderpath = os.path.dirname(folderpath) 722 folderpath = folderpath + "\\" 723 else: 724 self.display_loading_grayout(False, pseudo_load_time=0) 725 return 726 727 interface_was_already_set_hidden = not self.is_interface_showing # Needed to hide the interface temporarily while grabbing a screenshot (makes sure the screenshot only shows the views) 728 if not interface_was_already_set_hidden: 729 self.show_interface_off() 730 731 pixmap = self._mdiArea.grab() 732 733 date_and_time = datetime.now().strftime('%Y-%m-%d %H%M%S') # Sets the default filename with date and time 734 filename = "Viewer screenshot " + date_and_time + ".png" 735 name_filters = "PNG (*.png);; JPEG (*.jpeg);; TIFF (*.tiff);; JPG (*.jpg);; TIF (*.tif)" # Allows users to select filetype of screenshot 736 737 self.display_loading_grayout(True, "Selecting folder and name for the viewer screenshot...", pseudo_load_time=0) 738 739 filepath, _ = QtWidgets.QFileDialog.getSaveFileName(self, "Save a screenshot of the viewer", folderpath+filename, name_filters) 740 _, fileextension = os.path.splitext(filepath) 741 fileextension = fileextension.replace('.','') 742 if filepath: 743 pixmap.save(filepath, fileextension) 744 745 if not interface_was_already_set_hidden: 746 self.show_interface_on() 747 748 self.display_loading_grayout(False)
Screenshot MultiViewMainWindow and open Save dialog to save screenshot as image.
753 def display_loading_grayout(self, boolean, text="Loading...", pseudo_load_time=0.2): 754 """Show/hide grayout screen for loading sequences. 755 756 Args: 757 boolean (bool): True to show grayout; False to hide. 758 text (str): The text to show on the grayout. 759 pseudo_load_time (float): The delay (in seconds) to hide the grayout to give users a feeling of action. 760 """ 761 if not boolean: 762 text = "Loading..." 763 self.loading_grayout_label.setText(text) 764 self.loading_grayout_label.setVisible(boolean) 765 if boolean: 766 self.loading_grayout_label.repaint() 767 if not boolean: 768 time.sleep(pseudo_load_time)
Show/hide grayout screen for 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 users a feeling of action.
770 def display_dragged_grayout(self, boolean): 771 """Show/hide grayout screen for drag-and-drop sequences. 772 773 Args: 774 boolean (bool): True to show grayout; False to hide. 775 """ 776 self.dragged_grayout_label.setVisible(boolean) 777 if boolean: 778 self.dragged_grayout_label.repaint()
Show/hide grayout screen for drag-and-drop sequences.
Arguments:
- boolean (bool): True to show grayout; False to hide.
780 def on_last_remaining_subwindow_was_closed(self): 781 """Show instructions label of MDIArea.""" 782 self.label_mdiarea.setVisible(True)
Show instructions label of MDIArea.
784 def on_first_subwindow_was_opened(self): 785 """Hide instructions label of MDIArea.""" 786 self.label_mdiarea.setVisible(False)
Hide instructions label of MDIArea.
788 def show_interface(self, boolean): 789 """Show/hide interface elements for sliding overlay creator and transparencies. 790 791 Args: 792 boolean (bool): True to show interface; False to hide. 793 """ 794 if boolean: 795 self.show_interface_on() 796 elif not boolean: 797 self.show_interface_off()
Show/hide interface elements for sliding overlay creator and transparencies.
Arguments:
- boolean (bool): True to show interface; False to hide.
799 def show_interface_on(self): 800 """Show interface elements for sliding overlay creator and transparencies.""" 801 if self.is_interface_showing: 802 return 803 804 self.is_interface_showing = True 805 self.is_quiet_mode = False 806 807 self.update_window_highlight(self._mdiArea.activeSubWindow()) 808 self.update_window_labels(self._mdiArea.activeSubWindow()) 809 self.set_window_close_pushbuttons_always_visible(self._mdiArea.activeSubWindow(), True) 810 self.set_window_mouse_rect_visible(self._mdiArea.activeSubWindow(), True) 811 self.interface_mdiarea_topleft.setVisible(True) 812 self.interface_mdiarea_bottomleft.setVisible(True) 813 self.interface_toggle_slash_label.setVisible(False) 814 815 self.interface_toggle_pushbutton.setToolTip("Hide interface (studio mode)") 816 817 if self.interface_toggle_pushbutton: 818 self.interface_toggle_pushbutton.setChecked(True)
Show interface elements for sliding overlay creator and transparencies.
820 def show_interface_off(self): 821 """Hide interface elements for sliding overlay creator and transparencies.""" 822 if not self.is_interface_showing: 823 return 824 825 self.is_interface_showing = False 826 self.is_quiet_mode = True 827 828 self.update_window_highlight(self._mdiArea.activeSubWindow()) 829 self.update_window_labels(self._mdiArea.activeSubWindow()) 830 self.set_window_close_pushbuttons_always_visible(self._mdiArea.activeSubWindow(), False) 831 self.set_window_mouse_rect_visible(self._mdiArea.activeSubWindow(), False) 832 self.interface_mdiarea_topleft.setVisible(False) 833 self.interface_mdiarea_bottomleft.setVisible(False) 834 self.interface_toggle_slash_label.setVisible(True) 835 836 self.interface_toggle_pushbutton.setToolTip("Show interface (H)") 837 838 if self.interface_toggle_pushbutton: 839 self.interface_toggle_pushbutton.setChecked(False) 840 self.interface_toggle_pushbutton.setAttribute(QtCore.Qt.WA_UnderMouse, False)
Hide interface elements for sliding overlay creator and transparencies.
842 def toggle_interface(self): 843 """Toggle visibilty of interface elements for sliding overlay creator and transparencies.""" 844 if self.is_interface_showing: # If interface is showing, then toggle it off; if not, then toggle it on 845 self.show_interface_off() 846 else: 847 self.show_interface_on()
Toggle visibilty of interface elements for sliding overlay creator and transparencies.
871 def toggle_fullscreen(self): 872 """Toggle fullscreen state of app.""" 873 if self.is_fullscreen: 874 self.set_fullscreen_off() 875 else: 876 self.set_fullscreen_on()
Toggle fullscreen state of app.
878 def set_fullscreen_on(self): 879 """Enable fullscreen of MultiViewMainWindow. 880 881 Moves MDIArea to secondary window and makes it fullscreen. 882 Shows interim widget in main window. 883 """ 884 if self.is_fullscreen: 885 return 886 887 position_of_window = self.pos() 888 889 centralwidget_to_be_made_fullscreen = self.mdiarea_plus_buttons 890 widget_to_replace_central = self.centralwidget_during_fullscreen 891 892 centralwidget_to_be_made_fullscreen.setParent(None) 893 894 # move() is needed when using multiple monitors because when the widget loses its parent, its position moves to the primary screen origin (0,0) instead of retaining the app's screen 895 # The solution is to move the widget to the position of the app window and then make the widget fullscreen 896 # A timer is needed for showFullScreen() to apply on the app's screen (otherwise the command is made before the widget's move is established) 897 centralwidget_to_be_made_fullscreen.move(position_of_window) 898 QtCore.QTimer.singleShot(50, centralwidget_to_be_made_fullscreen.showFullScreen) 899 900 self.showMinimized() 901 902 self.setCentralWidget(widget_to_replace_central) 903 widget_to_replace_central.show() 904 905 self._mdiArea.tile_what_was_done_last_time() 906 self._mdiArea.activateWindow() 907 908 self.is_fullscreen = True 909 if self.fullscreen_pushbutton: 910 self.fullscreen_pushbutton.setChecked(True) 911 912 if self.activeMdiChild: 913 self.synchPan(self.activeMdiChild)
Enable fullscreen of MultiViewMainWindow.
Moves MDIArea to secondary window and makes it fullscreen. Shows interim widget in main window.
915 def set_fullscreen_off(self): 916 """Disable fullscreen of MultiViewMainWindow. 917 918 Removes interim widget in main window. 919 Returns MDIArea to normal (non-fullscreen) view on main window. 920 """ 921 if not self.is_fullscreen: 922 return 923 924 self.showNormal() 925 926 fullscreenwidget_to_be_made_central = self.mdiarea_plus_buttons 927 centralwidget_to_be_hidden = self.centralwidget_during_fullscreen 928 929 centralwidget_to_be_hidden.setParent(None) 930 centralwidget_to_be_hidden.hide() 931 932 self.setCentralWidget(fullscreenwidget_to_be_made_central) 933 934 self._mdiArea.tile_what_was_done_last_time() 935 self._mdiArea.activateWindow() 936 937 self.is_fullscreen = False 938 if self.fullscreen_pushbutton: 939 self.fullscreen_pushbutton.setChecked(False) 940 self.fullscreen_pushbutton.setAttribute(QtCore.Qt.WA_UnderMouse, False) 941 942 self.refreshPanDelayed(100)
Disable fullscreen of MultiViewMainWindow.
Removes interim widget in main window. Returns MDIArea to normal (non-fullscreen) view on main window.
944 def set_fullscreen(self, boolean): 945 """Enable/disable fullscreen of MultiViewMainWindow. 946 947 Args: 948 boolean (bool): True to enable fullscreen; False to disable. 949 """ 950 if boolean: 951 self.set_fullscreen_on() 952 elif not boolean: 953 self.set_fullscreen_off()
Enable/disable fullscreen of MultiViewMainWindow.
Arguments:
- boolean (bool): True to enable fullscreen; False to disable.
955 def update_window_highlight(self, window): 956 """Update highlight of subwindows in MDIArea. 957 958 Input window should be the subwindow which is active. 959 All other subwindow(s) will be shown no highlight. 960 961 Args: 962 window (QMdiSubWindow): The active subwindow to show highlight and indicate as active. 963 """ 964 if window is None: 965 return 966 changed_window = window 967 if self.is_quiet_mode: 968 changed_window.widget().frame_hud.setStyleSheet("QFrame {border: 0px solid transparent}") 969 elif self.activeMdiChild.split_locked: 970 changed_window.widget().frame_hud.setStyleSheet("QFrame {border: 0.2em orange; border-left-style: outset; border-top-style: inset; border-right-style: inset; border-bottom-style: inset}") 971 else: 972 changed_window.widget().frame_hud.setStyleSheet("QFrame {border: 0.2em blue; border-left-style: outset; border-top-style: inset; border-right-style: inset; border-bottom-style: inset}") 973 974 windows = self._mdiArea.subWindowList() 975 for window in windows: 976 if window != changed_window: 977 window.widget().frame_hud.setStyleSheet("QFrame {border: 0px solid transparent}")
Update highlight of subwindows in MDIArea.
Input window should be the subwindow which is active. All other subwindow(s) will be shown no highlight.
Arguments:
- window (QMdiSubWindow): The active subwindow to show highlight and indicate as active.
979 def update_window_labels(self, window): 980 """Update labels of subwindows in MDIArea. 981 982 Input window should be the subwindow which is active. 983 All other subwindow(s) will be shown no labels. 984 985 Args: 986 window (QMdiSubWindow): The active subwindow to show label(s) of image(s) and indicate as active. 987 """ 988 if window is None: 989 return 990 changed_window = window 991 label_visible = True 992 if self.is_quiet_mode: 993 label_visible = False 994 changed_window.widget().label_main_topleft.set_visible_based_on_text(label_visible) 995 changed_window.widget().label_topright.set_visible_based_on_text(label_visible) 996 changed_window.widget().label_bottomright.set_visible_based_on_text(label_visible) 997 changed_window.widget().label_bottomleft.set_visible_based_on_text(label_visible) 998 999 windows = self._mdiArea.subWindowList() 1000 for window in windows: 1001 if window != changed_window: 1002 window.widget().label_main_topleft.set_visible_based_on_text(False) 1003 window.widget().label_topright.set_visible_based_on_text(False) 1004 window.widget().label_bottomright.set_visible_based_on_text(False) 1005 window.widget().label_bottomleft.set_visible_based_on_text(False)
Update labels of subwindows in MDIArea.
Input window should be the subwindow which is active. All other subwindow(s) will be shown no labels.
Arguments:
- window (QMdiSubWindow): The active subwindow to show label(s) of image(s) and indicate as active.
1024 def set_window_mouse_rect_visible(self, window, boolean): 1025 """Enable/disable the visiblilty of the red 1x1 outline at the pointer 1026 1027 Outline shows the relative size of a pixel in the active subwindow. 1028 1029 Args: 1030 window (QMdiSubWindow): The active subwindow. 1031 boolean (bool): True to show 1x1 outline; False to hide. 1032 """ 1033 if window is None: 1034 return 1035 changed_window = window 1036 visible = boolean 1037 changed_window.widget().set_mouse_rect_visible(visible) 1038 windows = self._mdiArea.subWindowList() 1039 for window in windows: 1040 if window != changed_window: 1041 window.widget().set_mouse_rect_visible(visible)
Enable/disable the visiblilty of the red 1x1 outline at the pointer
Outline shows the relative size of a pixel in the active subwindow.
Arguments:
- window (QMdiSubWindow): The active subwindow.
- boolean (bool): True to show 1x1 outline; False to hide.
1043 def auto_tile_subwindows_on_close(self): 1044 """Tile the subwindows of MDIArea using previously used tile method.""" 1045 if self.subwindow_was_just_closed: 1046 self.subwindow_was_just_closed = False 1047 QtCore.QTimer.singleShot(50, self._mdiArea.tile_what_was_done_last_time) 1048 self.refreshPanDelayed(50)
Tile the subwindows of MDIArea using previously used tile method.
1065 def set_single_window_transform_mode_smooth(self, window, boolean): 1066 """Set the transform mode of a given subwindow. 1067 1068 Args: 1069 window (QMdiSubWindow): The subwindow. 1070 boolean (bool): True to smooth (interpolate); False to fast (not interpolate). 1071 """ 1072 if window is None: 1073 return 1074 changed_window = window 1075 changed_window.widget().set_transform_mode_smooth(boolean)
Set the transform mode of a given subwindow.
Arguments:
- window (QMdiSubWindow): The subwindow.
- boolean (bool): True to smooth (interpolate); False to fast (not interpolate).
1078 def set_all_window_transform_mode_smooth(self, boolean): 1079 """Set the transform mode of all subwindows. 1080 1081 Args: 1082 boolean (bool): True to smooth (interpolate); False to fast (not interpolate). 1083 """ 1084 if self._mdiArea.activeSubWindow() is None: 1085 return 1086 windows = self._mdiArea.subWindowList() 1087 for window in windows: 1088 window.widget().set_transform_mode_smooth(boolean)
Set the transform mode of all subwindows.
Arguments:
- boolean (bool): True to smooth (interpolate); False to fast (not interpolate).
1090 def set_all_background_color(self, color): 1091 """Set the background color of all subwindows. 1092 1093 Args: 1094 color (list): Descriptor string and RGB int values. Example: ["White", 255, 255, 255]. 1095 """ 1096 if self._mdiArea.activeSubWindow() is None: 1097 return 1098 windows = self._mdiArea.subWindowList() 1099 for window in windows: 1100 window.widget().set_scene_background_color(color) 1101 self.scene_background_color = color
Set the background color of all subwindows.
Arguments:
- color (list): Descriptor string and RGB int values. Example: ["White", 255, 255, 255].
1108 def show_about(self): 1109 """Show about box.""" 1110 sp = "<br>" 1111 title = "Butterfly Viewer" 1112 text = "Butterfly Viewer" 1113 text = text + sp + "Lars Maxfield" 1114 text = text + sp + "Version: " + __version__ 1115 text = text + sp + "License: <a href='https://www.gnu.org/licenses/gpl-3.0.en.html'>GNU GPL v3</a> or later" 1116 text = text + sp + "Source: <a href='https://github.com/olive-groves/butterfly_viewer'>github.com/olive-groves/butterfly_viewer</a>" 1117 text = text + sp + "Tutorial: <a href='https://olive-groves.github.io/butterfly_viewer'>olive-groves.github.io/butterfly_viewer</a>" 1118 box = QtWidgets.QMessageBox.about(self, title, text)
Show about box.
1122 def loadFile(self, filename_main_topleft, filename_topright=None, filename_bottomleft=None, filename_bottomright=None): 1123 """Load an individual image or sliding overlay into new subwindow. 1124 1125 Args: 1126 filename_main_topleft (str): The image filepath of the main image to be viewed; the basis of the sliding overlay (main; topleft) 1127 filename_topright (str): The image filepath for top-right of the sliding overlay (set None to exclude) 1128 filename_bottomleft (str): The image filepath for bottom-left of the sliding overlay (set None to exclude) 1129 filename_bottomright (str): The image filepath for bottom-right of the sliding overlay (set None to exclude) 1130 """ 1131 1132 self.display_loading_grayout(True, "Loading viewer with main image '" + filename_main_topleft.split("/")[-1] + "'...") 1133 1134 activeMdiChild = self.activeMdiChild 1135 QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) 1136 1137 transform_mode_smooth = self.is_global_transform_mode_smooth 1138 1139 pixmap = QtGui.QPixmap(filename_main_topleft) 1140 pixmap_topright = QtGui.QPixmap(filename_topright) 1141 pixmap_bottomleft = QtGui.QPixmap(filename_bottomleft) 1142 pixmap_bottomright = QtGui.QPixmap(filename_bottomright) 1143 1144 QtWidgets.QApplication.restoreOverrideCursor() 1145 1146 if (not pixmap or 1147 pixmap.width()==0 or pixmap.height==0): 1148 self.display_loading_grayout(True, "Waiting on dialog box...") 1149 QtWidgets.QMessageBox.warning(self, APPNAME, 1150 "Cannot read file %s." % (filename_main_topleft,)) 1151 self.updateRecentFileSettings(filename_main_topleft, delete=True) 1152 self.updateRecentFileActions() 1153 self.display_loading_grayout(False) 1154 return 1155 1156 angle = get_exif_rotation_angle(filename_main_topleft) 1157 if angle: 1158 pixmap = pixmap.transformed(QtGui.QTransform().rotate(angle)) 1159 1160 angle = get_exif_rotation_angle(filename_topright) 1161 if angle: 1162 pixmap_topright = pixmap_topright.transformed(QtGui.QTransform().rotate(angle)) 1163 1164 angle = get_exif_rotation_angle(filename_bottomright) 1165 if angle: 1166 pixmap_bottomright = pixmap_bottomright.transformed(QtGui.QTransform().rotate(angle)) 1167 1168 angle = get_exif_rotation_angle(filename_bottomleft) 1169 if angle: 1170 pixmap_bottomleft = pixmap_bottomleft.transformed(QtGui.QTransform().rotate(angle)) 1171 1172 child = self.createMdiChild(pixmap, filename_main_topleft, pixmap_topright, pixmap_bottomleft, pixmap_bottomright, transform_mode_smooth) 1173 1174 # Show filenames 1175 child.label_main_topleft.setText(filename_main_topleft) 1176 child.label_topright.setText(filename_topright) 1177 child.label_bottomright.setText(filename_bottomright) 1178 child.label_bottomleft.setText(filename_bottomleft) 1179 1180 child.show() 1181 1182 if activeMdiChild: 1183 if self._synchPanAct.isChecked(): 1184 self.synchPan(activeMdiChild) 1185 if self._synchZoomAct.isChecked(): 1186 self.synchZoom(activeMdiChild) 1187 1188 self._mdiArea.tile_what_was_done_last_time() 1189 1190 child.fitToWindow() 1191 child.set_close_pushbutton_always_visible(self.is_interface_showing) 1192 if self.scene_background_color is not None: 1193 child.set_scene_background_color(self.scene_background_color) 1194 1195 self.updateRecentFileSettings(filename_main_topleft) 1196 self.updateRecentFileActions() 1197 1198 self.display_loading_grayout(False) 1199 1200 self.statusBar().showMessage("File loaded", 2000)
Load an individual image or sliding overlay into new subwindow.
Arguments:
- filename_main_topleft (str): The image filepath of the main image to be viewed; the basis of the sliding overlay (main; topleft)
- filename_topright (str): The image filepath for top-right of the sliding overlay (set None to exclude)
- filename_bottomleft (str): The image filepath for bottom-left of the sliding overlay (set None to exclude)
- filename_bottomright (str): The image filepath for bottom-right of the sliding overlay (set None to exclude)
1202 def load_from_dragged_and_dropped_file(self, filename_main_topleft): 1203 """Load an individual image (convenience function — e.g., from a single emitted single filename).""" 1204 self.loadFile(filename_main_topleft)
Load an individual image (convenience function — e.g., from a single emitted single filename).
1206 def createMdiChild(self, pixmap, filename_main_topleft, pixmap_topright, pixmap_bottomleft, pixmap_bottomright, transform_mode_smooth): 1207 """Create new viewing widget for an individual image or sliding overlay to be placed in a new subwindow. 1208 1209 Args: 1210 pixmap (QPixmap): The main image to be viewed; the basis of the sliding overlay (main; topleft) 1211 filename_main_topleft (str): The image filepath of the main image. 1212 pixmap_topright (QPixmap): The top-right image of the sliding overlay (set None to exclude). 1213 pixmap_bottomleft (QPixmap): The bottom-left image of the sliding overlay (set None to exclude). 1214 pixmap_bottomright (QPixmap): The bottom-right image of the sliding overlay (set None to exclude). 1215 1216 Returns: 1217 child (SplitViewMdiChild): The viewing widget instance. 1218 """ 1219 1220 child = SplitViewMdiChild(pixmap, 1221 filename_main_topleft, 1222 "Window %d" % (len(self._mdiArea.subWindowList())+1), 1223 pixmap_topright, pixmap_bottomleft, pixmap_bottomright, 1224 transform_mode_smooth) 1225 1226 child.enableScrollBars(self._showScrollbarsAct.isChecked()) 1227 1228 self._mdiArea.addSubWindow(child, QtCore.Qt.FramelessWindowHint) # LVM: No frame, starts fitted 1229 1230 child.scrollChanged.connect(self.panChanged) 1231 child.transformChanged.connect(self.zoomChanged) 1232 1233 child.positionChanged.connect(self.on_positionChanged) 1234 child.tracker.mouse_leaved.connect(self.on_mouse_leaved) 1235 1236 child.scrollChanged.connect(self.on_scrollChanged) 1237 1238 child.became_closed.connect(self.on_subwindow_closed) 1239 child.was_clicked_close_pushbutton.connect(self._mdiArea.closeActiveSubWindow) 1240 child.shortcut_shift_x_was_activated.connect(self.shortcut_shift_x_was_activated_on_mdichild) 1241 child.signal_display_loading_grayout.connect(self.display_loading_grayout) 1242 child.was_set_global_transform_mode.connect(self.set_all_window_transform_mode_smooth) 1243 child.was_set_scene_background_color.connect(self.set_all_background_color) 1244 1245 return child
Create new viewing widget for an individual image or sliding overlay to be placed in a new subwindow.
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.
- 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).
Returns:
- child (SplitViewMdiChild): The viewing widget instance.
1250 @QtCore.pyqtSlot() 1251 def on_create_splitview(self): 1252 """Load a sliding overlay using the filepaths of the current images in the sliding overlay creator.""" 1253 # Get filenames 1254 file_path_main_topleft = self._splitview_creator.drag_drop_area.app_main_topleft.file_path 1255 file_path_topright = self._splitview_creator.drag_drop_area.app_topright.file_path 1256 file_path_bottomleft = self._splitview_creator.drag_drop_area.app_bottomleft.file_path 1257 file_path_bottomright = self._splitview_creator.drag_drop_area.app_bottomright.file_path 1258 1259 # loadFile with those filenames 1260 self.loadFile(file_path_main_topleft, file_path_topright, file_path_bottomleft, file_path_bottomright)
Load a sliding overlay using the filepaths of the current images in the sliding overlay creator.
1262 def fit_to_window(self): 1263 """Fit the view of the active subwindow (if it exists).""" 1264 if self.activeMdiChild: 1265 self.activeMdiChild.fitToWindow()
Fit the view of the active subwindow (if it exists).
1267 def update_split(self): 1268 """Update the position of the split of the active subwindow (if it exists) relying on the global mouse coordinates.""" 1269 if self.activeMdiChild: 1270 self.activeMdiChild.update_split() # No input = Rely on global mouse position calculation
Update the position of the split of the active subwindow (if it exists) relying on the global mouse coordinates.
1272 def lock_split(self): 1273 """Lock the position of the overlay split of active subwindow and set relevant interface elements.""" 1274 if self.activeMdiChild: 1275 self.activeMdiChild.split_locked = True 1276 self._splitview_manager.lock_split_pushbutton.setChecked(True) 1277 self.update_window_highlight(self._mdiArea.activeSubWindow())
Lock the position of the overlay split of active subwindow and set relevant interface elements.
1279 def unlock_split(self): 1280 """Unlock the position of the overlay split of active subwindow and set relevant interface elements.""" 1281 if self.activeMdiChild: 1282 self.activeMdiChild.split_locked = False 1283 self._splitview_manager.lock_split_pushbutton.setChecked(False) 1284 self.update_window_highlight(self._mdiArea.activeSubWindow())
Unlock the position of the overlay split of active subwindow and set relevant interface elements.
1286 def set_split(self, x_percent=0.5, y_percent=0.5, apply_to_all=True, ignore_lock=False, percent_of_visible=False): 1287 """Set the position of the split of the active subwindow as percent of base image's resolution. 1288 1289 Args: 1290 x_percent (float): The position of the split as a proportion (0-1) of the base image's horizontal resolution. 1291 y_percent (float): The position of the split as a proportion (0-1) of the base image's vertical resolution. 1292 apply_to_all (bool): True to set all subwindow splits; False to set only the active subwindow. 1293 ignore_lock (bool): True to ignore the lock status of the split; False to adhere. 1294 percent_of_visible (bool): True to set split as proportion of visible area; False as proportion of the full image resolution. 1295 """ 1296 if self.activeMdiChild: 1297 self.activeMdiChild.set_split(x_percent, y_percent, ignore_lock=ignore_lock, percent_of_visible=percent_of_visible) 1298 if apply_to_all: 1299 windows = self._mdiArea.subWindowList() 1300 for window in windows: 1301 window.widget().set_split(x_percent, y_percent, ignore_lock=ignore_lock, percent_of_visible=percent_of_visible) 1302 self.update_window_highlight(self._mdiArea.activeSubWindow())
Set the position of the split of the active subwindow as percent of base image's resolution.
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.
- apply_to_all (bool): True to set all subwindow splits; False to set only the active subwindow.
- 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.
1304 def set_split_from_slider(self): 1305 """Set the position of the split of the active subwindow to the center of the visible area of the sliding overlay (convenience function).""" 1306 self.set_split(x_percent=0.5, y_percent=0.5, apply_to_all=False, ignore_lock=False, percent_of_visible=True)
Set the position of the split of the active subwindow to the center of the visible area of the sliding overlay (convenience function).
1308 def set_split_from_manager(self, x_percent, y_percent): 1309 """Set the position of the split of the active subwindow as percent of base image's resolution (convenience function). 1310 1311 Args: 1312 x_percent (float): The position of the split as a proportion of the base image's horizontal resolution (0-1). 1313 y_percent (float): The position of the split as a proportion of the base image's vertical resolution (0-1). 1314 """ 1315 self.set_split(x_percent, y_percent, apply_to_all=False, ignore_lock=False)
Set the position of the split of the active subwindow as percent of base image's resolution (convenience function).
Arguments:
- x_percent (float): The position of the split as a proportion of the base image's horizontal resolution (0-1).
- y_percent (float): The position of the split as a proportion of the base image's vertical resolution (0-1).
1317 def set_and_lock_split_from_manager(self, x_percent, y_percent): 1318 """Set and lock the position of the split of the active subwindow as percent of base image's resolution (convenience function). 1319 1320 Args: 1321 x_percent (float): The position of the split as a proportion of the base image's horizontal resolution (0-1). 1322 y_percent (float): The position of the split as a proportion of the base image's vertical resolution (0-1). 1323 """ 1324 self.set_split(x_percent, y_percent, apply_to_all=False, ignore_lock=True) 1325 self.lock_split()
Set and lock the position of the split of the active subwindow as percent of base image's resolution (convenience function).
Arguments:
- x_percent (float): The position of the split as a proportion of the base image's horizontal resolution (0-1).
- y_percent (float): The position of the split as a proportion of the base image's vertical resolution (0-1).
1327 def shortcut_shift_x_was_activated_on_mdichild(self): 1328 """Update interface button for split lock based on lock status of active subwindow.""" 1329 self._splitview_manager.lock_split_pushbutton.setChecked(self.activeMdiChild.split_locked)
Update interface button for split lock based on lock status of active subwindow.
1331 @QtCore.pyqtSlot() 1332 def on_scrollChanged(self): 1333 """Refresh position of split of all subwindows based on their respective last position.""" 1334 windows = self._mdiArea.subWindowList() 1335 for window in windows: 1336 window.widget().refresh_split_based_on_last_updated_point_of_split_on_scene_main()
Refresh position of split of all subwindows based on their respective last position.
1338 def on_subwindow_closed(self): 1339 """Record that a subwindow was closed upon the closing of a subwindow.""" 1340 self.subwindow_was_just_closed = True
Record that a subwindow was closed upon the closing of a subwindow.
1342 @QtCore.pyqtSlot() 1343 def on_mouse_leaved(self): 1344 """Update displayed coordinates of mouse as N/A upon the mouse leaving the subwindow area.""" 1345 self._label_mouse.setText("View pixel coordinates: ( N/A , N/A )") 1346 self._label_mouse.adjustSize()
Update displayed coordinates of mouse as N/A upon the mouse leaving the subwindow area.
1348 @QtCore.pyqtSlot(QtCore.QPoint) 1349 def on_positionChanged(self, pos): 1350 """Update displayed coordinates of mouse on the active subwindow using global coordinates.""" 1351 1352 point_of_mouse_on_viewport = QtCore.QPointF(pos.x(), pos.y()) 1353 pos_qcursor_global = QtGui.QCursor.pos() 1354 1355 if self.activeMdiChild: 1356 1357 # Use mouse position to grab scene coordinates (activeMdiChild?) 1358 active_view = self.activeMdiChild._view_main_topleft 1359 point_of_mouse_on_scene = active_view.mapToScene(point_of_mouse_on_viewport.x(), point_of_mouse_on_viewport.y()) 1360 1361 if not self._label_mouse.isVisible(): 1362 self._label_mouse.show() 1363 self._label_mouse.setText("View pixel coordinates: ( x = %d , y = %d )" % (point_of_mouse_on_scene.x(), point_of_mouse_on_scene.y())) 1364 1365 pos_qcursor_view = active_view.mapFromGlobal(pos_qcursor_global) 1366 pos_qcursor_scene = active_view.mapToScene(pos_qcursor_view) 1367 # print("Cursor coords scene: ( %d , %d )" % (pos_qcursor_scene.x(), pos_qcursor_scene.y())) 1368 1369 else: 1370 1371 self._label_mouse.setText("View pixel coordinates: ( N/A , N/A )") 1372 1373 self._label_mouse.adjustSize()
Update displayed coordinates of mouse on the active subwindow using global coordinates.
1379 @QtCore.pyqtSlot(int) 1380 def on_slider_opacity_base_changed(self, value): 1381 """Set transparency of base of sliding overlay of active subwindow. 1382 1383 Triggered upon change in interface transparency slider. 1384 Temporarily sets position of split to the center of the visible area to give user a preview of the transparency effect. 1385 1386 Args: 1387 value (float,int): The transparency as percent opacity, where 100 is opaque (not transparent) and 0 is transparent (0-100). 1388 """ 1389 if not self.activeMdiChild: 1390 return 1391 if not self.activeMdiChild.split_locked: 1392 self.set_split_from_slider() 1393 self.activeMdiChild.set_opacity_base(value)
Set transparency of base of sliding overlay of active subwindow.
Triggered upon change in interface transparency slider. Temporarily sets position of split to the center of the visible area to give user a preview of the transparency effect.
Arguments:
- value (float,int): The transparency as percent opacity, where 100 is opaque (not transparent) and 0 is transparent (0-100).
1395 @QtCore.pyqtSlot(int) 1396 def on_slider_opacity_topright_changed(self, value): 1397 """Set transparency of top-right of sliding overlay of active subwindow. 1398 1399 Triggered upon change in interface transparency slider. 1400 Temporarily sets position of split to the center of the visible area to give user a preview of the transparency effect. 1401 1402 Args: 1403 value (float,int): The transparency as percent opacity, where 100 is opaque (not transparent) and 0 is transparent (0-100). 1404 """ 1405 if not self.activeMdiChild: 1406 return 1407 if not self.activeMdiChild.split_locked: 1408 self.set_split_from_slider() 1409 self.activeMdiChild.set_opacity_topright(value)
Set transparency of top-right of sliding overlay of active subwindow.
Triggered upon change in interface transparency slider. Temporarily sets position of split to the center of the visible area to give user a preview of the transparency effect.
Arguments:
- value (float,int): The transparency as percent opacity, where 100 is opaque (not transparent) and 0 is transparent (0-100).
1411 @QtCore.pyqtSlot(int) 1412 def on_slider_opacity_bottomright_changed(self, value): 1413 """Set transparency of bottom-right of sliding overlay of active subwindow. 1414 1415 Triggered upon change in interface transparency slider. 1416 Temporarily sets position of split to the center of the visible area to give user a preview of the transparency effect. 1417 1418 Args: 1419 value (float,int): The transparency as percent opacity, where 100 is opaque (not transparent) and 0 is transparent (0-100). 1420 """ 1421 if not self.activeMdiChild: 1422 return 1423 if not self.activeMdiChild.split_locked: 1424 self.set_split_from_slider() 1425 self.activeMdiChild.set_opacity_bottomright(value)
Set transparency of bottom-right of sliding overlay of active subwindow.
Triggered upon change in interface transparency slider. Temporarily sets position of split to the center of the visible area to give user a preview of the transparency effect.
Arguments:
- value (float,int): The transparency as percent opacity, where 100 is opaque (not transparent) and 0 is transparent (0-100).
1427 @QtCore.pyqtSlot(int) 1428 def on_slider_opacity_bottomleft_changed(self, value): 1429 """Set transparency of bottom-left of sliding overlay of active subwindow. 1430 1431 Triggered upon change in interface transparency slider. 1432 Temporarily sets position of split to the center of the visible area to give user a preview of the transparency effect. 1433 1434 Args: 1435 value (float,int): The transparency as percent opacity, where 100 is opaque (not transparent) and 0 is transparent (0-100). 1436 """ 1437 if not self.activeMdiChild: 1438 return 1439 if not self.activeMdiChild.split_locked: 1440 self.set_split_from_slider() 1441 self.activeMdiChild.set_opacity_bottomleft(value)
Set transparency of bottom-left of sliding overlay of active subwindow.
Triggered upon change in interface transparency slider. Temporarily sets position of split to the center of the visible area to give user a preview of the transparency effect.
Arguments:
- value (float,int): The transparency as percent opacity, where 100 is opaque (not transparent) and 0 is transparent (0-100).
1443 def update_sliders(self, window): 1444 """Update interface transparency sliders upon subwindow activating using the subwindow transparency values. 1445 1446 Args: 1447 window (QMdiSubWindow): The active subwindow. 1448 """ 1449 if window is None: 1450 self._sliders_opacity_splitviews.reset_sliders() 1451 return 1452 1453 child = self.activeMdiChild 1454 1455 self._sliders_opacity_splitviews.set_enabled(True, child.pixmap_topright_exists, child.pixmap_bottomright_exists, child.pixmap_bottomleft_exists) 1456 1457 opacity_base_of_activeMdiChild = child._opacity_base 1458 opacity_topright_of_activeMdiChild = child._opacity_topright 1459 opacity_bottomright_of_activeMdiChild = child._opacity_bottomright 1460 opacity_bottomleft_of_activeMdiChild = child._opacity_bottomleft 1461 1462 self._sliders_opacity_splitviews.update_sliders(opacity_base_of_activeMdiChild, opacity_topright_of_activeMdiChild, opacity_bottomright_of_activeMdiChild, opacity_bottomleft_of_activeMdiChild)
Update interface transparency sliders upon subwindow activating using the subwindow transparency values.
Arguments:
- window (QMdiSubWindow): The active subwindow.
1467 def createMappedAction(self, icon, text, parent, shortcut, methodName): 1468 """Create |QAction| that is mapped via methodName to call. 1469 1470 :param icon: icon associated with |QAction| 1471 :type icon: |QIcon| or None 1472 :param str text: the |QAction| descriptive text 1473 :param QObject parent: the parent |QObject| 1474 :param QKeySequence shortcut: the shortcut |QKeySequence| 1475 :param str methodName: name of method to call when |QAction| is 1476 triggered 1477 :rtype: |QAction|""" 1478 1479 if icon is not None: 1480 action = QtWidgets.QAction(icon, text, parent, 1481 shortcut=shortcut, 1482 triggered=self._actionMapper.map) 1483 else: 1484 action = QtWidgets.QAction(text, parent, 1485 shortcut=shortcut, 1486 triggered=self._actionMapper.map) 1487 self._actionMapper.setMapping(action, methodName) 1488 return action
Create |QAction| that is mapped via methodName to call.
Parameters
- icon: icon associated with |QAction|
- str text: the |QAction| descriptive text
- QObject parent: the parent |QObject|
- QKeySequence shortcut: the shortcut |QKeySequence|
- str methodName: name of method to call when |QAction| is triggered
1490 def createActions(self): 1491 """Create actions used in menus.""" 1492 #File menu actions 1493 self._openAct = QtWidgets.QAction( 1494 "&Open...", self, 1495 shortcut=QtGui.QKeySequence.Open, 1496 statusTip="Open an existing file", 1497 triggered=self.open) 1498 1499 self._switchLayoutDirectionAct = QtWidgets.QAction( 1500 "Switch &layout direction", self, 1501 triggered=self.switchLayoutDirection) 1502 1503 #create dummy recent file actions 1504 for i in range(MultiViewMainWindow.MaxRecentFiles): 1505 self._recentFileActions.append( 1506 QtWidgets.QAction(self, visible=False, 1507 triggered=self._recentFileMapper.map)) 1508 1509 self._exitAct = QtWidgets.QAction( 1510 "E&xit", self, 1511 shortcut=QtGui.QKeySequence.Quit, 1512 statusTip="Exit the application", 1513 triggered=QtWidgets.QApplication.closeAllWindows) 1514 1515 #View menu actions 1516 self._showScrollbarsAct = QtWidgets.QAction( 1517 "&Scrollbars", self, 1518 checkable=True, 1519 statusTip="Toggle display of subwindow scrollbars", 1520 triggered=self.toggleScrollbars) 1521 1522 self._showStatusbarAct = QtWidgets.QAction( 1523 "S&tatusbar", self, 1524 checkable=True, 1525 statusTip="Toggle display of statusbar", 1526 triggered=self.toggleStatusbar) 1527 1528 self._synchZoomAct = QtWidgets.QAction( 1529 "Synch &Zoom", self, 1530 checkable=True, 1531 statusTip="Synch zooming of subwindows", 1532 triggered=self.toggleSynchZoom) 1533 1534 self._synchPanAct = QtWidgets.QAction( 1535 "Synch &Pan", self, 1536 checkable=True, 1537 statusTip="Synch panning of subwindows", 1538 triggered=self.toggleSynchPan) 1539 1540 #Scroll menu actions 1541 self._scrollActions = [ 1542 self.createMappedAction( 1543 None, 1544 "&Top", self, 1545 QtGui.QKeySequence.MoveToStartOfDocument, 1546 "scrollToTop"), 1547 1548 self.createMappedAction( 1549 None, 1550 "&Bottom", self, 1551 QtGui.QKeySequence.MoveToEndOfDocument, 1552 "scrollToBottom"), 1553 1554 self.createMappedAction( 1555 None, 1556 "&Left Edge", self, 1557 QtGui.QKeySequence.MoveToStartOfLine, 1558 "scrollToBegin"), 1559 1560 self.createMappedAction( 1561 None, 1562 "&Right Edge", self, 1563 QtGui.QKeySequence.MoveToEndOfLine, 1564 "scrollToEnd"), 1565 1566 self.createMappedAction( 1567 None, 1568 "&Center", self, 1569 "5", 1570 "centerView"), 1571 ] 1572 1573 #zoom menu actions 1574 separatorAct = QtWidgets.QAction(self) 1575 separatorAct.setSeparator(True) 1576 1577 self._zoomActions = [ 1578 self.createMappedAction( 1579 None, 1580 "Zoo&m In (25%)", self, 1581 QtGui.QKeySequence.ZoomIn, 1582 "zoomIn"), 1583 1584 self.createMappedAction( 1585 None, 1586 "Zoom &Out (25%)", self, 1587 QtGui.QKeySequence.ZoomOut, 1588 "zoomOut"), 1589 1590 #self.createMappedAction( 1591 #None, 1592 #"&Zoom To...", self, 1593 #"Z", 1594 #"zoomTo"), 1595 1596 separatorAct, 1597 1598 self.createMappedAction( 1599 None, 1600 "Actual &Size", self, 1601 "/", 1602 "actualSize"), 1603 1604 self.createMappedAction( 1605 None, 1606 "Fit &Image", self, 1607 "*", 1608 "fitToWindow"), 1609 1610 self.createMappedAction( 1611 None, 1612 "Fit &Width", self, 1613 "Alt+Right", 1614 "fitWidth"), 1615 1616 self.createMappedAction( 1617 None, 1618 "Fit &Height", self, 1619 "Alt+Down", 1620 "fitHeight"), 1621 ] 1622 1623 #Window menu actions 1624 self._activateSubWindowSystemMenuAct = QtWidgets.QAction( 1625 "Activate &System Menu", self, 1626 shortcut="Ctrl+ ", 1627 statusTip="Activate subwindow System Menu", 1628 triggered=self.activateSubwindowSystemMenu) 1629 1630 self._closeAct = QtWidgets.QAction( 1631 "Cl&ose", self, 1632 shortcut=QtGui.QKeySequence.Close, 1633 shortcutContext=QtCore.Qt.WidgetShortcut, 1634 #shortcut="Ctrl+Alt+F4", 1635 statusTip="Close the active window", 1636 triggered=self._mdiArea.closeActiveSubWindow) 1637 1638 self._closeAllAct = QtWidgets.QAction( 1639 "Close &All", self, 1640 statusTip="Close all the windows", 1641 triggered=self._mdiArea.closeAllSubWindows) 1642 1643 self._tileAct = QtWidgets.QAction( 1644 "&Tile", self, 1645 statusTip="Tile the windows", 1646 triggered=self._mdiArea.tileSubWindows) 1647 1648 self._tileAct.triggered.connect(self.tile_and_fit_mdiArea) 1649 1650 self._cascadeAct = QtWidgets.QAction( 1651 "&Cascade", self, 1652 statusTip="Cascade the windows", 1653 triggered=self._mdiArea.cascadeSubWindows) 1654 1655 self._nextAct = QtWidgets.QAction( 1656 "Ne&xt", self, 1657 shortcut=QtGui.QKeySequence.NextChild, 1658 statusTip="Move the focus to the next window", 1659 triggered=self._mdiArea.activateNextSubWindow) 1660 1661 self._previousAct = QtWidgets.QAction( 1662 "Pre&vious", self, 1663 shortcut=QtGui.QKeySequence.PreviousChild, 1664 statusTip="Move the focus to the previous window", 1665 triggered=self._mdiArea.activatePreviousSubWindow) 1666 1667 self._separatorAct = QtWidgets.QAction(self) 1668 self._separatorAct.setSeparator(True) 1669 1670 self._aboutAct = QtWidgets.QAction( 1671 "&About", self, 1672 statusTip="Show the application's About box", 1673 triggered=self.about) 1674 1675 self._aboutQtAct = QtWidgets.QAction( 1676 "About &Qt", self, 1677 statusTip="Show the Qt library's About box", 1678 triggered=QtWidgets.QApplication.aboutQt)
Create actions used in menus.
1680 def createMenus(self): 1681 """Create menus.""" 1682 self._fileMenu = self.menuBar().addMenu("&File") 1683 self._fileMenu.addAction(self._openAct) 1684 self._fileMenu.addAction(self._switchLayoutDirectionAct) 1685 1686 self._fileSeparatorAct = self._fileMenu.addSeparator() 1687 for action in self._recentFileActions: 1688 self._fileMenu.addAction(action) 1689 self.updateRecentFileActions() 1690 self._fileMenu.addSeparator() 1691 self._fileMenu.addAction(self._exitAct) 1692 1693 self._viewMenu = self.menuBar().addMenu("&View") 1694 self._viewMenu.addAction(self._showScrollbarsAct) 1695 self._viewMenu.addAction(self._showStatusbarAct) 1696 self._viewMenu.addSeparator() 1697 self._viewMenu.addAction(self._synchZoomAct) 1698 self._viewMenu.addAction(self._synchPanAct) 1699 1700 self._scrollMenu = self.menuBar().addMenu("&Scroll") 1701 [self._scrollMenu.addAction(action) for action in self._scrollActions] 1702 1703 self._zoomMenu = self.menuBar().addMenu("&Zoom") 1704 [self._zoomMenu.addAction(action) for action in self._zoomActions] 1705 1706 self._windowMenu = self.menuBar().addMenu("&Window") 1707 self.updateWindowMenu() 1708 self._windowMenu.aboutToShow.connect(self.updateWindowMenu) 1709 1710 self.menuBar().addSeparator() 1711 1712 self._helpMenu = self.menuBar().addMenu("&Help") 1713 self._helpMenu.addAction(self._aboutAct) 1714 self._helpMenu.addAction(self._aboutQtAct)
Create menus.
1716 def updateMenus(self): 1717 """Update menus.""" 1718 hasMdiChild = (self.activeMdiChild is not None) 1719 1720 self._scrollMenu.setEnabled(hasMdiChild) 1721 self._zoomMenu.setEnabled(hasMdiChild) 1722 1723 self._closeAct.setEnabled(hasMdiChild) 1724 self._closeAllAct.setEnabled(hasMdiChild) 1725 1726 self._tileAct.setEnabled(hasMdiChild) 1727 self._cascadeAct.setEnabled(hasMdiChild) 1728 self._nextAct.setEnabled(hasMdiChild) 1729 self._previousAct.setEnabled(hasMdiChild) 1730 self._separatorAct.setVisible(hasMdiChild)
Update menus.
1732 def updateRecentFileActions(self): 1733 """Update recent file menu items.""" 1734 settings = QtCore.QSettings() 1735 files = settings.value(SETTING_RECENTFILELIST) 1736 numRecentFiles = min(len(files) if files else 0, 1737 MultiViewMainWindow.MaxRecentFiles) 1738 1739 for i in range(numRecentFiles): 1740 text = "&%d %s" % (i + 1, strippedName(files[i])) 1741 self._recentFileActions[i].setText(text) 1742 self._recentFileActions[i].setData(files[i]) 1743 self._recentFileActions[i].setVisible(True) 1744 self._recentFileMapper.setMapping(self._recentFileActions[i], 1745 files[i]) 1746 1747 for j in range(numRecentFiles, MultiViewMainWindow.MaxRecentFiles): 1748 self._recentFileActions[j].setVisible(False) 1749 1750 self._fileSeparatorAct.setVisible((numRecentFiles > 0))
Update recent file menu items.
1752 def updateWindowMenu(self): 1753 """Update the Window menu.""" 1754 self._windowMenu.clear() 1755 self._windowMenu.addAction(self._closeAct) 1756 self._windowMenu.addAction(self._closeAllAct) 1757 self._windowMenu.addSeparator() 1758 self._windowMenu.addAction(self._tileAct) 1759 self._windowMenu.addAction(self._cascadeAct) 1760 self._windowMenu.addSeparator() 1761 self._windowMenu.addAction(self._nextAct) 1762 self._windowMenu.addAction(self._previousAct) 1763 self._windowMenu.addAction(self._separatorAct) 1764 1765 windows = self._mdiArea.subWindowList() 1766 self._separatorAct.setVisible(len(windows) != 0) 1767 1768 for i, window in enumerate(windows): 1769 child = window.widget() 1770 1771 text = "%d %s" % (i + 1, child.userFriendlyCurrentFile) 1772 if i < 9: 1773 text = '&' + text 1774 1775 action = self._windowMenu.addAction(text) 1776 action.setCheckable(True) 1777 action.setChecked(child == self.activeMdiChild) 1778 action.triggered.connect(self._windowMapper.map) 1779 self._windowMapper.setMapping(action, window)
Update the Window menu.
1781 def createStatusBarLabel(self, stretch=0): 1782 """Create status bar label. 1783 1784 :param int stretch: stretch factor 1785 :rtype: |QLabel|""" 1786 label = QtWidgets.QLabel() 1787 label.setFrameStyle(QtWidgets.QFrame.Panel | QtWidgets.QFrame.Sunken) 1788 label.setLineWidth(2) 1789 self.statusBar().addWidget(label, stretch) 1790 return label
Create status bar label.
Parameters
- int stretch: stretch factor
1792 def createStatusBar(self): 1793 """Create status bar.""" 1794 statusBar = self.statusBar() 1795 1796 self._sbLabelName = self.createStatusBarLabel(1) 1797 self._sbLabelSize = self.createStatusBarLabel() 1798 self._sbLabelDimensions = self.createStatusBarLabel() 1799 self._sbLabelDate = self.createStatusBarLabel() 1800 self._sbLabelZoom = self.createStatusBarLabel() 1801 1802 statusBar.showMessage("Ready")
Create status bar.
1814 def closeEvent(self, event): 1815 """Overrides close event to save application settings. 1816 1817 :param QEvent event: instance of |QEvent|""" 1818 1819 if self.is_fullscreen: # Needed to properly close the image viewer if the main window is closed while the viewer is fullscreen 1820 self.is_fullscreen = False 1821 self.setCentralWidget(self.mdiarea_plus_buttons) 1822 1823 self._mdiArea.closeAllSubWindows() 1824 if self.activeMdiChild: 1825 event.ignore() 1826 else: 1827 self.writeSettings() 1828 event.accept()
Overrides close event to save application settings.
Parameters
- QEvent event: instance of |QEvent|
1837 @QtCore.pyqtSlot(str) 1838 def mappedImageViewerAction(self, methodName): 1839 """Perform action mapped to :class:`aux_splitview.SplitView` 1840 methodName. 1841 1842 :param str methodName: method to call""" 1843 activeViewer = self.activeMdiChild 1844 if hasattr(activeViewer, str(methodName)): 1845 getattr(activeViewer, str(methodName))()
Perform action mapped to aux_splitview.SplitView
methodName.
Parameters
- str methodName: method to call
1847 @QtCore.pyqtSlot() 1848 def toggleSynchPan(self): 1849 """Toggle synchronized subwindow panning.""" 1850 if self._synchPanAct.isChecked(): 1851 self.synchPan(self.activeMdiChild)
Toggle synchronized subwindow panning.
1853 @QtCore.pyqtSlot() 1854 def panChanged(self): 1855 """Synchronize subwindow pans.""" 1856 mdiChild = self.sender() 1857 while mdiChild is not None and type(mdiChild) != SplitViewMdiChild: 1858 mdiChild = mdiChild.parent() 1859 if mdiChild and self._synchPanAct.isChecked(): 1860 self.synchPan(mdiChild)
Synchronize subwindow pans.
1862 @QtCore.pyqtSlot() 1863 def toggleSynchZoom(self): 1864 """Toggle synchronized subwindow zooming.""" 1865 if self._synchZoomAct.isChecked(): 1866 self.synchZoom(self.activeMdiChild)
Toggle synchronized subwindow zooming.
1868 @QtCore.pyqtSlot() 1869 def zoomChanged(self): 1870 """Synchronize subwindow zooms.""" 1871 mdiChild = self.sender() 1872 if self._synchZoomAct.isChecked(): 1873 self.synchZoom(mdiChild) 1874 self.updateStatusBar()
Synchronize subwindow zooms.
1876 def synchPan(self, fromViewer): 1877 """Synch panning of all subwindowws to the same as *fromViewer*. 1878 1879 :param fromViewer: :class:`SplitViewMdiChild` that initiated synching""" 1880 1881 assert isinstance(fromViewer, SplitViewMdiChild) 1882 if not fromViewer: 1883 return 1884 if self._handlingScrollChangedSignal: 1885 return 1886 if fromViewer.parent() != self._mdiArea.activeSubWindow(): # Prevent circular scroll state change signals from propagating 1887 if fromViewer.parent() != self: 1888 return 1889 self._handlingScrollChangedSignal = True 1890 1891 newState = fromViewer.scrollState 1892 changedWindow = fromViewer.parent() 1893 windows = self._mdiArea.subWindowList() 1894 for window in windows: 1895 if window != changedWindow: 1896 window.widget().scrollState = newState 1897 window.widget().resize_scene() 1898 1899 self._handlingScrollChangedSignal = False
Synch panning of all subwindowws to the same as fromViewer.
Parameters
- fromViewer:
SplitViewMdiChild
that initiated synching
1901 def synchZoom(self, fromViewer): 1902 """Synch zoom of all subwindowws to the same as *fromViewer*. 1903 1904 :param fromViewer: :class:`SplitViewMdiChild` that initiated synching""" 1905 if not fromViewer: 1906 return 1907 newZoomFactor = fromViewer.zoomFactor 1908 changedWindow = fromViewer.parent() 1909 windows = self._mdiArea.subWindowList() 1910 for window in windows: 1911 if window != changedWindow: 1912 window.widget().zoomFactor = newZoomFactor 1913 window.widget().resize_scene() 1914 self.refreshPan()
Synch zoom of all subwindowws to the same as fromViewer.
Parameters
- fromViewer:
SplitViewMdiChild
that initiated synching
1926 @QtCore.pyqtSlot() 1927 def activateSubwindowSystemMenu(self): 1928 """Activate current subwindow's System Menu.""" 1929 activeSubWindow = self._mdiArea.activeSubWindow() 1930 if activeSubWindow: 1931 activeSubWindow.showSystemMenu()
Activate current subwindow's System Menu.
1933 @QtCore.pyqtSlot(str) 1934 def openRecentFile(self, filename_main_topleft): 1935 """Open a recent file. 1936 1937 :param str filename_main_topleft: filename_main_topleft to view""" 1938 self.loadFile(filename_main_topleft, None, None, None)
Open a recent file.
Parameters
- str filename_main_topleft: filename_main_topleft to view
1940 @QtCore.pyqtSlot() 1941 def open(self): 1942 """Handle the open action.""" 1943 fileDialog = QtWidgets.QFileDialog(self) 1944 settings = QtCore.QSettings() 1945 fileDialog.setNameFilters(["Image Files (*.jpg *.png *.tif)", 1946 "All Files (*)"]) 1947 if not settings.contains(SETTING_FILEOPEN + "/state"): 1948 fileDialog.setDirectory(".") 1949 else: 1950 self.restoreDialogState(fileDialog, SETTING_FILEOPEN) 1951 fileDialog.setFileMode(QtWidgets.QFileDialog.ExistingFile) 1952 if not fileDialog.exec_(): 1953 return 1954 self.saveDialogState(fileDialog, SETTING_FILEOPEN) 1955 1956 filename_main_topleft = fileDialog.selectedFiles()[0] 1957 self.loadFile(filename_main_topleft, None, None, None)
Handle the open action.
1960 @QtCore.pyqtSlot() 1961 def toggleScrollbars(self): 1962 """Toggle subwindow scrollbar visibility.""" 1963 checked = self._showScrollbarsAct.isChecked() 1964 1965 windows = self._mdiArea.subWindowList() 1966 for window in windows: 1967 child = window.widget() 1968 child.enableScrollBars(checked)
Toggle subwindow scrollbar visibility.
1970 @QtCore.pyqtSlot() 1971 def toggleStatusbar(self): 1972 """Toggle status bar visibility.""" 1973 self.statusBar().setVisible(self._showStatusbarAct.isChecked())
Toggle status bar visibility.
1976 @QtCore.pyqtSlot() 1977 def about(self): 1978 """Display About dialog box.""" 1979 QtWidgets.QMessageBox.about(self, "About MDI", 1980 "<b>MDI Image Viewer</b> demonstrates how to" 1981 "synchronize the panning and zooming of multiple image" 1982 "viewer windows using Qt.")
Display About dialog box.
1983 @QtCore.pyqtSlot(QtWidgets.QMdiSubWindow) 1984 def subWindowActivated(self, window): 1985 """Handle |QMdiSubWindow| activated signal. 1986 1987 :param |QMdiSubWindow| window: |QMdiSubWindow| that was just 1988 activated""" 1989 self.updateStatusBar()
Handle |QMdiSubWindow| activated signal.
Parameters
- |QMdiSubWindow| window: |QMdiSubWindow| that was just activated
1991 @QtCore.pyqtSlot(QtWidgets.QMdiSubWindow) 1992 def setActiveSubWindow(self, window): 1993 """Set active |QMdiSubWindow|. 1994 1995 :param |QMdiSubWindow| window: |QMdiSubWindow| to activate """ 1996 if window: 1997 self._mdiArea.setActiveSubWindow(window)
Set active |QMdiSubWindow|.
Parameters
- |QMdiSubWindow| window: |QMdiSubWindow| to activate
2000 def updateStatusBar(self): 2001 """Update status bar.""" 2002 self.statusBar().setVisible(self._showStatusbarAct.isChecked()) 2003 imageViewer = self.activeMdiChild 2004 if not imageViewer: 2005 self._sbLabelName.setText("") 2006 self._sbLabelSize.setText("") 2007 self._sbLabelDimensions.setText("") 2008 self._sbLabelDate.setText("") 2009 self._sbLabelZoom.setText("") 2010 2011 self._sbLabelSize.hide() 2012 self._sbLabelDimensions.hide() 2013 self._sbLabelDate.hide() 2014 self._sbLabelZoom.hide() 2015 return 2016 2017 filename_main_topleft = imageViewer.currentFile 2018 self._sbLabelName.setText(" %s " % filename_main_topleft) 2019 2020 fi = QtCore.QFileInfo(filename_main_topleft) 2021 size = fi.size() 2022 fmt = " %.1f %s " 2023 if size > 1024*1024*1024: 2024 unit = "MB" 2025 size /= 1024*1024*1024 2026 elif size > 1024*1024: 2027 unit = "MB" 2028 size /= 1024*1024 2029 elif size > 1024: 2030 unit = "KB" 2031 size /= 1024 2032 else: 2033 unit = "Bytes" 2034 fmt = " %d %s " 2035 self._sbLabelSize.setText(fmt % (size, unit)) 2036 2037 pixmap = imageViewer.pixmap_main_topleft 2038 self._sbLabelDimensions.setText(" %dx%dx%d " % 2039 (pixmap.width(), 2040 pixmap.height(), 2041 pixmap.depth())) 2042 2043 self._sbLabelDate.setText( 2044 " %s " % 2045 fi.lastModified().toString(QtCore.Qt.SystemLocaleShortDate)) 2046 self._sbLabelZoom.setText(" %0.f%% " % (imageViewer.zoomFactor*100,)) 2047 2048 self._sbLabelSize.show() 2049 self._sbLabelDimensions.show() 2050 self._sbLabelDate.show() 2051 self._sbLabelZoom.show()
Update status bar.
2053 def switchLayoutDirection(self): 2054 """Switch MDI subwindow layout direction.""" 2055 if self.layoutDirection() == QtCore.Qt.LeftToRight: 2056 QtWidgets.QApplication.setLayoutDirection(QtCore.Qt.RightToLeft) 2057 else: 2058 QtWidgets.QApplication.setLayoutDirection(QtCore.Qt.LeftToRight)
Switch MDI subwindow layout direction.
2060 def saveDialogState(self, dialog, groupName): 2061 """Save dialog state, position & size. 2062 2063 :param |QDialog| dialog: dialog to save state of 2064 :param str groupName: |QSettings| group name""" 2065 assert isinstance(dialog, QtWidgets.QDialog) 2066 2067 settings = QtCore.QSettings() 2068 settings.beginGroup(groupName) 2069 2070 settings.setValue('state', dialog.saveState()) 2071 settings.setValue('geometry', dialog.saveGeometry()) 2072 settings.setValue('filter', dialog.selectedNameFilter()) 2073 2074 settings.endGroup()
Save dialog state, position & size.
Parameters
- |QDialog| dialog: dialog to save state of
- str groupName: |QSettings| group name
2076 def restoreDialogState(self, dialog, groupName): 2077 """Restore dialog state, position & size. 2078 2079 :param str groupName: |QSettings| group name""" 2080 assert isinstance(dialog, QtWidgets.QDialog) 2081 2082 settings = QtCore.QSettings() 2083 settings.beginGroup(groupName) 2084 2085 dialog.restoreState(settings.value('state')) 2086 dialog.restoreGeometry(settings.value('geometry')) 2087 dialog.selectNameFilter(settings.value('filter', "")) 2088 2089 settings.endGroup()
Restore dialog state, position & size.
Parameters
- str groupName: |QSettings| group name
2091 def writeSettings(self): 2092 """Write application settings.""" 2093 settings = QtCore.QSettings() 2094 settings.setValue('pos', self.pos()) 2095 settings.setValue('size', self.size()) 2096 settings.setValue('windowgeometry', self.saveGeometry()) 2097 settings.setValue('windowstate', self.saveState()) 2098 2099 settings.setValue(SETTING_SCROLLBARS, 2100 self._showScrollbarsAct.isChecked()) 2101 settings.setValue(SETTING_STATUSBAR, 2102 self._showStatusbarAct.isChecked()) 2103 settings.setValue(SETTING_SYNCHZOOM, 2104 self._synchZoomAct.isChecked()) 2105 settings.setValue(SETTING_SYNCHPAN, 2106 self._synchPanAct.isChecked())
Write application settings.
2108 def readSettings(self): 2109 """Read application settings.""" 2110 2111 scrollbars_always_checked_off_at_startup = True 2112 statusbar_always_checked_off_at_startup = True 2113 sync_always_checked_on_at_startup = True 2114 2115 settings = QtCore.QSettings() 2116 2117 pos = settings.value('pos', QtCore.QPoint(100, 100)) 2118 size = settings.value('size', QtCore.QSize(1100, 600)) 2119 self.move(pos) 2120 self.resize(size) 2121 2122 if settings.contains('windowgeometry'): 2123 self.restoreGeometry(settings.value('windowgeometry')) 2124 if settings.contains('windowstate'): 2125 self.restoreState(settings.value('windowstate')) 2126 2127 2128 if scrollbars_always_checked_off_at_startup: 2129 self._showScrollbarsAct.setChecked(False) 2130 else: 2131 self._showScrollbarsAct.setChecked( 2132 toBool(settings.value(SETTING_SCROLLBARS, False))) 2133 2134 if statusbar_always_checked_off_at_startup: 2135 self._showStatusbarAct.setChecked(False) 2136 else: 2137 self._showStatusbarAct.setChecked( 2138 toBool(settings.value(SETTING_STATUSBAR, False))) 2139 2140 if sync_always_checked_on_at_startup: 2141 self._synchZoomAct.setChecked(True) 2142 self._synchPanAct.setChecked(True) 2143 else: 2144 self._synchZoomAct.setChecked( 2145 toBool(settings.value(SETTING_SYNCHZOOM, False))) 2146 self._synchPanAct.setChecked( 2147 toBool(settings.value(SETTING_SYNCHPAN, False)))
Read application settings.
2149 def updateRecentFileSettings(self, filename_main_topleft, delete=False): 2150 """Update recent file list setting. 2151 2152 :param str filename_main_topleft: filename_main_topleft to add or remove from recent file 2153 list 2154 :param bool delete: if True then filename_main_topleft removed, otherwise added""" 2155 settings = QtCore.QSettings() 2156 files = list(settings.value(SETTING_RECENTFILELIST, [])) 2157 2158 try: 2159 files.remove(filename_main_topleft) 2160 except ValueError: 2161 pass 2162 2163 if not delete: 2164 files.insert(0, filename_main_topleft) 2165 del files[MultiViewMainWindow.MaxRecentFiles:] 2166 2167 settings.setValue(SETTING_RECENTFILELIST, files)
Update recent file list setting.
Parameters
- str filename_main_topleft: filename_main_topleft to add or remove from recent file list
- bool delete: if True then filename_main_topleft removed, otherwise added
2171def main(): 2172 """Run MultiViewMainWindow as main app. 2173 2174 Attributes: 2175 app (QApplication): Starts and holds the main event loop of application. 2176 mainWin (MultiViewMainWindow): The main window. 2177 """ 2178 import sys 2179 2180 app = QtWidgets.QApplication(sys.argv) 2181 QtCore.QSettings.setDefaultFormat(QtCore.QSettings.IniFormat) 2182 app.setOrganizationName(COMPANY) 2183 app.setOrganizationDomain(DOMAIN) 2184 app.setApplicationName(APPNAME) 2185 app.setWindowIcon(QtGui.QIcon(":/icon.png")) 2186 2187 mainWin = MultiViewMainWindow() 2188 mainWin.setWindowTitle(APPNAME) 2189 2190 mainWin.show() 2191 2192 sys.exit(app.exec_())
Run MultiViewMainWindow as main app.
Attributes:
- app (QApplication): Starts and holds the main event loop of application.
- mainWin (MultiViewMainWindow): The main window.