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()
class SplitViewMdiChild(aux_splitview.SplitView):
 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.
def toggle_lock_split(self):
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).

def update_split(self, pos=None, pos_is_global=False, ignore_lock=False):
 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.

def enterEvent(self, event):
110    def enterEvent(self, event):
111        """Pass along enter event to parent method."""
112        super().enterEvent(event)

Pass along enter event to parent method.

class MultiViewMainWindow(PyQt5.QtWidgets.QMainWindow):
 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.
def copy_view(self):
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.

def save_view(self):
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.

def display_loading_grayout(self, boolean, text='Loading...', pseudo_load_time=0.2):
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.
def display_dragged_grayout(self, boolean):
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.
def on_last_remaining_subwindow_was_closed(self):
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.

def on_first_subwindow_was_opened(self):
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.

def show_interface(self, boolean):
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.
def show_interface_on(self):
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.

def show_interface_off(self):
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.

def toggle_interface(self):
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.

def set_stopsync_pushbutton(self, boolean):
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()

Set state of synchronous zoom/pan and appearance of corresponding interface button.

Arguments:
  • boolean (bool): True to enable synchronized zoom/pan; False to disable.
def toggle_fullscreen(self):
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.

def set_fullscreen_on(self):
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.

def set_fullscreen_off(self):
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.

def set_fullscreen(self, boolean):
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.
def update_window_highlight(self, window):
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.
def update_window_labels(self, window):
 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.
def set_window_close_pushbuttons_always_visible(self, window, boolean):
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)

Enable/disable the always-on visiblilty of the close X on each subwindow.

Arguments:
  • window (QMdiSubWindow): The active subwindow.
  • boolean (bool): True to show the close X always; False to hide unless mouse hovers over.
def set_window_mouse_rect_visible(self, window, boolean):
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.
def auto_tile_subwindows_on_close(self):
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.

def update_mdi_buttons(self, window):
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)

Update the interface button 'Split Lock' based on the status of the split (locked/unlocked) in the given window.

Arguments:
  • window (QMdiSubWindow): The active subwindow.
def set_single_window_transform_mode_smooth(self, window, boolean):
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).
def set_all_window_transform_mode_smooth(self, boolean):
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).
def set_all_background_color(self, color):
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].
def info_button_clicked(self):
1103    def info_button_clicked(self):
1104        """Trigger when info button is clicked."""
1105        self.show_about()
1106        return

Trigger when info button is clicked.

def show_about(self):
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.

def loadFile( self, filename_main_topleft, filename_topright=None, filename_bottomleft=None, filename_bottomright=None):
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)
def load_from_dragged_and_dropped_file(self, filename_main_topleft):
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).

def createMdiChild( self, pixmap, filename_main_topleft, pixmap_topright, pixmap_bottomleft, pixmap_bottomright, transform_mode_smooth):
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.
@QtCore.pyqtSlot()
def on_create_splitview(self):
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.

def fit_to_window(self):
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).

def update_split(self):
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.

def lock_split(self):
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.

def unlock_split(self):
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.

def set_split( self, x_percent=0.5, y_percent=0.5, apply_to_all=True, ignore_lock=False, percent_of_visible=False):
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.
def set_split_from_slider(self):
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).

def set_split_from_manager(self, x_percent, y_percent):
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).
def set_and_lock_split_from_manager(self, x_percent, y_percent):
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).
def shortcut_shift_x_was_activated_on_mdichild(self):
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.

@QtCore.pyqtSlot()
def on_scrollChanged(self):
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.

def on_subwindow_closed(self):
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.

@QtCore.pyqtSlot()
def on_mouse_leaved(self):
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.

@QtCore.pyqtSlot(QtCore.QPoint)
def on_positionChanged(self, pos):
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.

@QtCore.pyqtSlot(int)
def on_slider_opacity_base_changed(self, value):
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).
@QtCore.pyqtSlot(int)
def on_slider_opacity_topright_changed(self, value):
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).
@QtCore.pyqtSlot(int)
def on_slider_opacity_bottomright_changed(self, value):
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).
@QtCore.pyqtSlot(int)
def on_slider_opacity_bottomleft_changed(self, value):
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).
def update_sliders(self, window):
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.
def createMappedAction(self, icon, text, parent, shortcut, methodName):
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
def createActions(self):
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.

def createMenus(self):
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.

def updateMenus(self):
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.

def updateRecentFileActions(self):
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.

def updateWindowMenu(self):
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.

def createStatusBarLabel(self, stretch=0):
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
def createStatusBar(self):
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.

activeMdiChild

Get active MDI child (SplitViewMdiChild or None).

def closeEvent(self, event):
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|
@QtCore.pyqtSlot(str)
def mappedImageViewerAction(self, methodName):
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
@QtCore.pyqtSlot()
def toggleSynchPan(self):
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.

@QtCore.pyqtSlot()
def panChanged(self):
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.

@QtCore.pyqtSlot()
def toggleSynchZoom(self):
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.

@QtCore.pyqtSlot()
def zoomChanged(self):
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.

def synchPan(self, fromViewer):
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
def synchZoom(self, fromViewer):
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
@QtCore.pyqtSlot()
def activateSubwindowSystemMenu(self):
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.

@QtCore.pyqtSlot(str)
def openRecentFile(self, filename_main_topleft):
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
@QtCore.pyqtSlot()
def open(self):
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.

@QtCore.pyqtSlot()
def toggleScrollbars(self):
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.

@QtCore.pyqtSlot()
def toggleStatusbar(self):
1970    @QtCore.pyqtSlot()
1971    def toggleStatusbar(self):
1972        """Toggle status bar visibility."""
1973        self.statusBar().setVisible(self._showStatusbarAct.isChecked())

Toggle status bar visibility.

@QtCore.pyqtSlot()
def about(self):
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.

@QtCore.pyqtSlot(QtWidgets.QMdiSubWindow)
def subWindowActivated(self, window):
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
@QtCore.pyqtSlot(QtWidgets.QMdiSubWindow)
def setActiveSubWindow(self, window):
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
def updateStatusBar(self):
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.

def switchLayoutDirection(self):
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.

def saveDialogState(self, dialog, groupName):
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
def restoreDialogState(self, dialog, groupName):
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
def writeSettings(self):
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.

def readSettings(self):
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.

def updateRecentFileSettings(self, filename_main_topleft, delete=False):
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
def main():
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.