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

bool: Setting of whether to sync this by zoom (or not).

sync_this_pan

bool: Setting of whether to sync this by pan (or not).

def toggle_lock_split(self):
114    def toggle_lock_split(self):
115        """Toggle the split lock.
116        
117        Toggles the status of the split lock (e.g., if locked, it will become unlocked; vice versa).
118        """
119        self.split_locked = not self.split_locked
120        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):
122    def update_split(self, pos = None, pos_is_global=False, ignore_lock=False):
123        """Update the position of the split while considering the status of the split lock.
124        
125        See parent method for full documentation.
126        """
127        if not self.split_locked or ignore_lock:
128            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):
133    def enterEvent(self, event):
134        """Pass along enter event to parent method."""
135        super().enterEvent(event)

Pass along enter event to parent method.

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

Screenshot MultiViewMainWindow and copy to clipboard as image.

def save_view(self):
470    def save_view(self):
471        """Screenshot MultiViewMainWindow and open Save dialog to save screenshot as image.""" 
472
473        self.display_loading_grayout(True, "Saving viewer screenshot...")
474
475        folderpath = None
476
477        if self.activeMdiChild:
478            folderpath = self.activeMdiChild.currentFile
479            folderpath = os.path.dirname(folderpath)
480            folderpath = folderpath + "\\"
481        else:
482            self.display_loading_grayout(False, pseudo_load_time=0)
483            return
484
485        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)
486        if not interface_was_already_set_hidden:
487            self.show_interface_off()
488
489        pixmap = self._mdiArea.grab()
490
491        date_and_time = datetime.now().strftime('%Y-%m-%d %H%M%S') # Sets the default filename with date and time 
492        filename = "Viewer screenshot " + date_and_time + ".png"
493        name_filters = "PNG (*.png);; JPEG (*.jpeg);; TIFF (*.tiff);; JPG (*.jpg);; TIF (*.tif)" # Allows users to select filetype of screenshot
494
495        self.display_loading_grayout(True, "Selecting folder and name for the viewer screenshot...", pseudo_load_time=0)
496        
497        filepath, _ = QtWidgets.QFileDialog.getSaveFileName(self, "Save a screenshot of the viewer", folderpath+filename, name_filters)
498        _, fileextension = os.path.splitext(filepath)
499        fileextension = fileextension.replace('.','')
500        if filepath:
501            pixmap.save(filepath, fileextension)
502        
503        if not interface_was_already_set_hidden:
504            self.show_interface_on()
505
506        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):
511    def display_loading_grayout(self, boolean, text="Loading...", pseudo_load_time=0.2):
512        """Show/hide grayout screen for loading sequences.
513
514        Args:
515            boolean (bool): True to show grayout; False to hide.
516            text (str): The text to show on the grayout.
517            pseudo_load_time (float): The delay (in seconds) to hide the grayout to give users a feeling of action.
518        """ 
519        if not boolean:
520            text = "Loading..."
521        self.loading_grayout_label.setText(text)
522        self.loading_grayout_label.setVisible(boolean)
523        if boolean:
524            self.loading_grayout_label.repaint()
525        if not boolean:
526            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):
528    def display_dragged_grayout(self, boolean):
529        """Show/hide grayout screen for drag-and-drop sequences.
530
531        Args:
532            boolean (bool): True to show grayout; False to hide.
533        """ 
534        self.dragged_grayout_label.setVisible(boolean)
535        if boolean:
536            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):
538    def on_last_remaining_subwindow_was_closed(self):
539        """Show instructions label of MDIArea."""
540        self.label_mdiarea.setVisible(True)

Show instructions label of MDIArea.

def on_first_subwindow_was_opened(self):
542    def on_first_subwindow_was_opened(self):
543        """Hide instructions label of MDIArea."""
544        self.label_mdiarea.setVisible(False)

Hide instructions label of MDIArea.

def show_interface(self, boolean):
546    def show_interface(self, boolean):
547        """Show/hide interface elements for sliding overlay creator and transparencies.
548
549        Args:
550            boolean (bool): True to show interface; False to hide.
551        """ 
552        if boolean:
553            self.show_interface_on()
554        elif not boolean:
555            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):
557    def show_interface_on(self):
558        """Show interface elements for sliding overlay creator and transparencies.""" 
559        if self.is_interface_showing:
560            return
561        
562        self.is_interface_showing = True
563        self.is_quiet_mode = False
564
565        self.update_window_highlight(self._mdiArea.activeSubWindow())
566        self.update_window_labels(self._mdiArea.activeSubWindow())
567        self.set_window_close_pushbuttons_always_visible(self._mdiArea.activeSubWindow(), True)
568        self.set_window_mouse_rect_visible(self._mdiArea.activeSubWindow(), True)
569        self.interface_mdiarea_topleft.setVisible(True)
570        self.interface_mdiarea_bottomleft.setVisible(True)
571
572        self.interface_toggle_pushbutton.setToolTip("Hide interface (studio mode)")
573
574        if self.interface_toggle_pushbutton:
575            self.interface_toggle_pushbutton.setChecked(True)

Show interface elements for sliding overlay creator and transparencies.

def show_interface_off(self):
577    def show_interface_off(self):
578        """Hide interface elements for sliding overlay creator and transparencies.""" 
579        if not self.is_interface_showing:
580            return
581
582        self.is_interface_showing = False
583        self.is_quiet_mode = True
584
585        self.update_window_highlight(self._mdiArea.activeSubWindow())
586        self.update_window_labels(self._mdiArea.activeSubWindow())
587        self.set_window_close_pushbuttons_always_visible(self._mdiArea.activeSubWindow(), False)
588        self.set_window_mouse_rect_visible(self._mdiArea.activeSubWindow(), False)
589        self.interface_mdiarea_topleft.setVisible(False)
590        self.interface_mdiarea_bottomleft.setVisible(False)
591
592        self.interface_toggle_pushbutton.setToolTip("Show interface (H)")
593
594        if self.interface_toggle_pushbutton:
595            self.interface_toggle_pushbutton.setChecked(False)
596            self.interface_toggle_pushbutton.setAttribute(QtCore.Qt.WA_UnderMouse, False)

Hide interface elements for sliding overlay creator and transparencies.

def toggle_interface(self):
598    def toggle_interface(self):
599        """Toggle visibilty of interface elements for sliding overlay creator and transparencies.""" 
600        if self.is_interface_showing: # If interface is showing, then toggle it off; if not, then toggle it on
601            self.show_interface_off()
602        else:
603            self.show_interface_on()

Toggle visibilty of interface elements for sliding overlay creator and transparencies.

def set_stopsync_pushbutton(self, boolean):
605    def set_stopsync_pushbutton(self, boolean):
606        """Set state of synchronous zoom/pan and appearance of corresponding interface button.
607
608        Args:
609            boolean (bool): True to enable synchronized zoom/pan; False to disable.
610        """ 
611        self._synchZoomAct.setChecked(not boolean)
612        self._synchPanAct.setChecked(not boolean)
613        
614        if self._synchZoomAct.isChecked():
615            if self.activeMdiChild:
616                self.activeMdiChild.fitToWindow()
617
618        if boolean:
619            self.stopsync_toggle_pushbutton.setToolTip("Synchronize zoom and pan (currently unsynced)")
620        else:
621            self.stopsync_toggle_pushbutton.setToolTip("Unsynchronize zoom and pan (currently synced)")

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):
623    def toggle_fullscreen(self):
624        """Toggle fullscreen state of app."""
625        if self.is_fullscreen:
626            self.set_fullscreen_off()
627        else:
628            self.set_fullscreen_on()

Toggle fullscreen state of app.

def set_fullscreen_on(self):
630    def set_fullscreen_on(self):
631        """Enable fullscreen of MultiViewMainWindow.
632        
633        Moves MDIArea to secondary window and makes it fullscreen.
634        Shows interim widget in main window.  
635        """
636        if self.is_fullscreen:
637            return
638
639        position_of_window = self.pos()
640
641        centralwidget_to_be_made_fullscreen = self.mdiarea_plus_buttons
642        widget_to_replace_central = self.centralwidget_during_fullscreen
643
644        centralwidget_to_be_made_fullscreen.setParent(None)
645
646        # 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
647        # The solution is to move the widget to the position of the app window and then make the widget fullscreen
648        # 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)
649        centralwidget_to_be_made_fullscreen.move(position_of_window)
650        QtCore.QTimer.singleShot(50, centralwidget_to_be_made_fullscreen.showFullScreen)
651
652        self.showMinimized()
653
654        self.setCentralWidget(widget_to_replace_central)
655        widget_to_replace_central.show()
656        
657        self._mdiArea.tile_what_was_done_last_time()
658        self._mdiArea.activateWindow()
659
660        self.is_fullscreen = True
661        if self.fullscreen_pushbutton:
662            self.fullscreen_pushbutton.setChecked(True)
663
664        if self.activeMdiChild:
665            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):
667    def set_fullscreen_off(self):
668        """Disable fullscreen of MultiViewMainWindow.
669        
670        Removes interim widget in main window. 
671        Returns MDIArea to normal (non-fullscreen) view on main window. 
672        """
673        if not self.is_fullscreen:
674            return
675        
676        self.showNormal()
677
678        fullscreenwidget_to_be_made_central = self.mdiarea_plus_buttons
679        centralwidget_to_be_hidden = self.centralwidget_during_fullscreen
680
681        centralwidget_to_be_hidden.setParent(None)
682        centralwidget_to_be_hidden.hide()
683
684        self.setCentralWidget(fullscreenwidget_to_be_made_central)
685
686        self._mdiArea.tile_what_was_done_last_time()
687        self._mdiArea.activateWindow()
688
689        self.is_fullscreen = False
690        if self.fullscreen_pushbutton:
691            self.fullscreen_pushbutton.setChecked(False)
692            self.fullscreen_pushbutton.setAttribute(QtCore.Qt.WA_UnderMouse, False)
693
694        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):
696    def set_fullscreen(self, boolean):
697        """Enable/disable fullscreen of MultiViewMainWindow.
698        
699        Args:
700            boolean (bool): True to enable fullscreen; False to disable.
701        """
702        if boolean:
703            self.set_fullscreen_on()
704        elif not boolean:
705            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):
707    def update_window_highlight(self, window):
708        """Update highlight of subwindows in MDIArea.
709
710        Input window should be the subwindow which is active.
711        All other subwindow(s) will be shown no highlight.
712        
713        Args:
714            window (QMdiSubWindow): The active subwindow to show highlight and indicate as active.
715        """
716        if window is None:
717            return
718        changed_window = window
719        if self.is_quiet_mode:
720            changed_window.widget().frame_hud.setStyleSheet("QFrame {border: 0px solid transparent}")
721        elif self.activeMdiChild.split_locked:
722            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}")
723        else:
724            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}")
725
726        windows = self._mdiArea.subWindowList()
727        for window in windows:
728            if window != changed_window:
729                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):
731    def update_window_labels(self, window):
732        """Update labels of subwindows in MDIArea.
733
734        Input window should be the subwindow which is active.
735        All other subwindow(s) will be shown no labels.
736        
737        Args:
738            window (QMdiSubWindow): The active subwindow to show label(s) of image(s) and indicate as active.
739        """
740        if window is None:
741            return
742        changed_window = window
743        label_visible = True
744        if self.is_quiet_mode:
745            label_visible = False
746        changed_window.widget().label_main_topleft.set_visible_based_on_text(label_visible)
747        changed_window.widget().label_topright.set_visible_based_on_text(label_visible)
748        changed_window.widget().label_bottomright.set_visible_based_on_text(label_visible)
749        changed_window.widget().label_bottomleft.set_visible_based_on_text(label_visible)
750
751        windows = self._mdiArea.subWindowList()
752        for window in windows:
753            if window != changed_window:
754                window.widget().label_main_topleft.set_visible_based_on_text(False)
755                window.widget().label_topright.set_visible_based_on_text(False)
756                window.widget().label_bottomright.set_visible_based_on_text(False)
757                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):
759    def set_window_close_pushbuttons_always_visible(self, window, boolean):
760        """Enable/disable the always-on visiblilty of the close X on each subwindow.
761        
762        Args:
763            window (QMdiSubWindow): The active subwindow.
764            boolean (bool): True to show the close X always; False to hide unless mouse hovers over.
765        """
766        if window is None:
767            return
768        changed_window = window
769        always_visible = boolean
770        changed_window.widget().set_close_pushbutton_always_visible(always_visible)
771        windows = self._mdiArea.subWindowList()
772        for window in windows:
773            if window != changed_window:
774                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):
776    def set_window_mouse_rect_visible(self, window, boolean):
777        """Enable/disable the visiblilty of the red 1x1 outline at the pointer
778        
779        Outline shows the relative size of a pixel in the active subwindow.
780        
781        Args:
782            window (QMdiSubWindow): The active subwindow.
783            boolean (bool): True to show 1x1 outline; False to hide.
784        """
785        if window is None:
786            return
787        changed_window = window
788        visible = boolean
789        changed_window.widget().set_mouse_rect_visible(visible)
790        windows = self._mdiArea.subWindowList()
791        for window in windows:
792            if window != changed_window:
793                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):
795    def auto_tile_subwindows_on_close(self):
796        """Tile the subwindows of MDIArea using previously used tile method."""
797        if self.subwindow_was_just_closed:
798            self.subwindow_was_just_closed = False
799            QtCore.QTimer.singleShot(50, self._mdiArea.tile_what_was_done_last_time)
800            self.refreshPanDelayed(50)

Tile the subwindows of MDIArea using previously used tile method.

def update_mdi_buttons(self, window):
802    def update_mdi_buttons(self, window):
803        """Update the interface button 'Split Lock' based on the status of the split (locked/unlocked) in the given window.
804        
805        Args:
806            window (QMdiSubWindow): The active subwindow.
807        """
808        if window is None:
809            self._splitview_manager.lock_split_pushbutton.setChecked(False)
810            return
811        
812        child = self.activeMdiChild
813
814        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):
817    def set_single_window_transform_mode_smooth(self, window, boolean):
818        """Set the transform mode of a given subwindow.
819        
820        Args:
821            window (QMdiSubWindow): The subwindow.
822            boolean (bool): True to smooth (interpolate); False to fast (not interpolate).
823        """
824        if window is None:
825            return
826        changed_window = window
827        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):
830    def set_all_window_transform_mode_smooth(self, boolean):
831        """Set the transform mode of all subwindows. 
832        
833        Args:
834            boolean (bool): True to smooth (interpolate); False to fast (not interpolate).
835        """
836        if self._mdiArea.activeSubWindow() is None:
837            return
838        windows = self._mdiArea.subWindowList()
839        for window in windows:
840            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):
842    def set_all_background_color(self, color):
843        """Set the background color of all subwindows. 
844        
845        Args:
846            color (list): Descriptor string and RGB int values. Example: ["White", 255, 255, 255].
847        """
848        if self._mdiArea.activeSubWindow() is None:
849            return
850        windows = self._mdiArea.subWindowList()
851        for window in windows:
852            window.widget().set_scene_background_color(color)
853        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 set_all_sync_zoom_by(self, by: str):
855    def set_all_sync_zoom_by(self, by: str):
856        """[str] Set the method by which to sync zoom all windows."""
857        if self._mdiArea.activeSubWindow() is None:
858            return
859        windows = self._mdiArea.subWindowList()
860        for window in windows:
861            window.widget().update_sync_zoom_by(by)
862        self.sync_zoom_by = by
863        self.refreshZoom()

[str] Set the method by which to sync zoom all windows.

def info_button_clicked(self):
865    def info_button_clicked(self):
866        """Trigger when info button is clicked."""
867        self.show_about()
868        return

Trigger when info button is clicked.

def show_about(self):
870    def show_about(self):
871        """Show about box."""
872        sp = "<br>"
873        title = "Butterfly Viewer"
874        text = "Butterfly Viewer"
875        text = text + sp + "Lars Maxfield"
876        text = text + sp + "Version: " + VERSION
877        text = text + sp + "License: <a href='https://www.gnu.org/licenses/gpl-3.0.en.html'>GNU GPL v3</a> or later"
878        text = text + sp + "Source: <a href='https://github.com/olive-groves/butterfly_viewer'>github.com/olive-groves/butterfly_viewer</a>"
879        text = text + sp + "Tutorial: <a href='https://olive-groves.github.io/butterfly_viewer'>olive-groves.github.io/butterfly_viewer</a>"
880        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):
884    def loadFile(self, filename_main_topleft, filename_topright=None, filename_bottomleft=None, filename_bottomright=None):
885        """Load an individual image or sliding overlay into new subwindow.
886
887        Args:
888            filename_main_topleft (str): The image filepath of the main image to be viewed; the basis of the sliding overlay (main; topleft)
889            filename_topright (str): The image filepath for top-right of the sliding overlay (set None to exclude)
890            filename_bottomleft (str): The image filepath for bottom-left of the sliding overlay (set None to exclude)
891            filename_bottomright (str): The image filepath for bottom-right of the sliding overlay (set None to exclude)
892        """
893
894        self.display_loading_grayout(True, "Loading viewer with main image '" + filename_main_topleft.split("/")[-1] + "'...")
895
896        activeMdiChild = self.activeMdiChild
897        QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor)
898
899        transform_mode_smooth = self.is_global_transform_mode_smooth
900        
901        pixmap = QtGui.QPixmap(filename_main_topleft)
902        pixmap_topright = QtGui.QPixmap(filename_topright)
903        pixmap_bottomleft = QtGui.QPixmap(filename_bottomleft)
904        pixmap_bottomright = QtGui.QPixmap(filename_bottomright)
905        
906        QtWidgets.QApplication.restoreOverrideCursor()
907        
908        if (not pixmap or
909            pixmap.width()==0 or pixmap.height==0):
910            self.display_loading_grayout(True, "Waiting on dialog box...")
911            QtWidgets.QMessageBox.warning(self, APPNAME,
912                                      "Cannot read file %s." % (filename_main_topleft,))
913            self.updateRecentFileSettings(filename_main_topleft, delete=True)
914            self.updateRecentFileActions()
915            self.display_loading_grayout(False)
916            return
917        
918        angle = get_exif_rotation_angle(filename_main_topleft)
919        if angle:
920            pixmap = pixmap.transformed(QtGui.QTransform().rotate(angle))
921        
922        angle = get_exif_rotation_angle(filename_topright)
923        if angle:
924            pixmap_topright = pixmap_topright.transformed(QtGui.QTransform().rotate(angle))
925
926        angle = get_exif_rotation_angle(filename_bottomright)
927        if angle:
928            pixmap_bottomright = pixmap_bottomright.transformed(QtGui.QTransform().rotate(angle))
929
930        angle = get_exif_rotation_angle(filename_bottomleft)
931        if angle:
932            pixmap_bottomleft = pixmap_bottomleft.transformed(QtGui.QTransform().rotate(angle))
933
934        child = self.createMdiChild(pixmap, filename_main_topleft, pixmap_topright, pixmap_bottomleft, pixmap_bottomright, transform_mode_smooth)
935
936        # Show filenames
937        child.label_main_topleft.setText(filename_main_topleft)
938        child.label_topright.setText(filename_topright)
939        child.label_bottomright.setText(filename_bottomright)
940        child.label_bottomleft.setText(filename_bottomleft)
941        
942        child.show()
943
944        if activeMdiChild:
945            if self._synchPanAct.isChecked():
946                self.synchPan(activeMdiChild)
947            if self._synchZoomAct.isChecked():
948                self.synchZoom(activeMdiChild)
949                
950        self._mdiArea.tile_what_was_done_last_time()
951        
952        child.fitToWindow()
953        child.set_close_pushbutton_always_visible(self.is_interface_showing)
954        if self.scene_background_color is not None:
955            child.set_scene_background_color(self.scene_background_color)
956
957        self.updateRecentFileSettings(filename_main_topleft)
958        self.updateRecentFileActions()
959        
960        self._last_accessed_fullpath = filename_main_topleft
961
962        self.display_loading_grayout(False)
963
964        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):
966    def load_from_dragged_and_dropped_file(self, filename_main_topleft):
967        """Load an individual image (convenience function — e.g., from a single emitted single filename)."""
968        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):
 970    def createMdiChild(self, pixmap, filename_main_topleft, pixmap_topright, pixmap_bottomleft, pixmap_bottomright, transform_mode_smooth):
 971        """Create new viewing widget for an individual image or sliding overlay to be placed in a new subwindow.
 972
 973        Args:
 974            pixmap (QPixmap): The main image to be viewed; the basis of the sliding overlay (main; topleft)
 975            filename_main_topleft (str): The image filepath of the main image.
 976            pixmap_topright (QPixmap): The top-right image of the sliding overlay (set None to exclude).
 977            pixmap_bottomleft (QPixmap): The bottom-left image of the sliding overlay (set None to exclude).
 978            pixmap_bottomright (QPixmap): The bottom-right image of the sliding overlay (set None to exclude).
 979
 980        Returns:
 981            child (SplitViewMdiChild): The viewing widget instance.
 982        """
 983        
 984        child = SplitViewMdiChild(pixmap,
 985                         filename_main_topleft,
 986                         "Window %d" % (len(self._mdiArea.subWindowList())+1),
 987                         pixmap_topright, pixmap_bottomleft, pixmap_bottomright, 
 988                         transform_mode_smooth)
 989
 990        child.enableScrollBars(self._showScrollbarsAct.isChecked())
 991
 992        child.sync_this_zoom = True
 993        child.sync_this_pan = True
 994        
 995        self._mdiArea.addSubWindow(child, QtCore.Qt.FramelessWindowHint) # LVM: No frame, starts fitted
 996
 997        child.scrollChanged.connect(self.panChanged)
 998        child.transformChanged.connect(self.zoomChanged)
 999        
1000        child.positionChanged.connect(self.on_positionChanged)
1001        child.tracker.mouse_leaved.connect(self.on_mouse_leaved)
1002        
1003        child.scrollChanged.connect(self.on_scrollChanged)
1004
1005        child.became_closed.connect(self.on_subwindow_closed)
1006        child.was_clicked_close_pushbutton.connect(self._mdiArea.closeActiveSubWindow)
1007        child.shortcut_shift_x_was_activated.connect(self.shortcut_shift_x_was_activated_on_mdichild)
1008        child.signal_display_loading_grayout.connect(self.display_loading_grayout)
1009        child.was_set_global_transform_mode.connect(self.set_all_window_transform_mode_smooth)
1010        child.was_set_scene_background_color.connect(self.set_all_background_color)
1011        child.was_set_sync_zoom_by.connect(self.set_all_sync_zoom_by)
1012
1013        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):
1018    @QtCore.pyqtSlot()
1019    def on_create_splitview(self):
1020        """Load a sliding overlay using the filepaths of the current images in the sliding overlay creator."""
1021        # Get filenames
1022        file_path_main_topleft = self._splitview_creator.drag_drop_area.app_main_topleft.file_path
1023        file_path_topright = self._splitview_creator.drag_drop_area.app_topright.file_path
1024        file_path_bottomleft = self._splitview_creator.drag_drop_area.app_bottomleft.file_path
1025        file_path_bottomright = self._splitview_creator.drag_drop_area.app_bottomright.file_path
1026
1027        # loadFile with those filenames
1028        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):
1030    def fit_to_window(self):
1031        """Fit the view of the active subwindow (if it exists)."""
1032        if self.activeMdiChild:
1033            self.activeMdiChild.fitToWindow()

Fit the view of the active subwindow (if it exists).

def update_split(self):
1035    def update_split(self):
1036        """Update the position of the split of the active subwindow (if it exists) relying on the global mouse coordinates."""
1037        if self.activeMdiChild:
1038            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):
1040    def lock_split(self):
1041        """Lock the position of the overlay split of active subwindow and set relevant interface elements."""
1042        if self.activeMdiChild:
1043            self.activeMdiChild.split_locked = True
1044        self._splitview_manager.lock_split_pushbutton.setChecked(True)
1045        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):
1047    def unlock_split(self):
1048        """Unlock the position of the overlay split of active subwindow and set relevant interface elements."""
1049        if self.activeMdiChild:
1050            self.activeMdiChild.split_locked = False
1051        self._splitview_manager.lock_split_pushbutton.setChecked(False)
1052        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):
1054    def set_split(self, x_percent=0.5, y_percent=0.5, apply_to_all=True, ignore_lock=False, percent_of_visible=False):
1055        """Set the position of the split of the active subwindow as percent of base image's resolution.
1056        
1057        Args:
1058            x_percent (float): The position of the split as a proportion (0-1) of the base image's horizontal resolution.
1059            y_percent (float): The position of the split as a proportion (0-1) of the base image's vertical resolution.
1060            apply_to_all (bool): True to set all subwindow splits; False to set only the active subwindow.
1061            ignore_lock (bool): True to ignore the lock status of the split; False to adhere.
1062            percent_of_visible (bool): True to set split as proportion of visible area; False as proportion of the full image resolution.
1063        """
1064        if self.activeMdiChild:
1065            self.activeMdiChild.set_split(x_percent, y_percent, ignore_lock=ignore_lock, percent_of_visible=percent_of_visible)
1066        if apply_to_all:
1067            windows = self._mdiArea.subWindowList()
1068            for window in windows:
1069                window.widget().set_split(x_percent, y_percent, ignore_lock=ignore_lock, percent_of_visible=percent_of_visible)
1070        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):
1072    def set_split_from_slider(self):
1073        """Set the position of the split of the active subwindow to the center of the visible area of the sliding overlay (convenience function)."""
1074        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):
1076    def set_split_from_manager(self, x_percent, y_percent):
1077        """Set the position of the split of the active subwindow as percent of base image's resolution (convenience function).
1078        
1079        Args:
1080            x_percent (float): The position of the split as a proportion of the base image's horizontal resolution (0-1).
1081            y_percent (float): The position of the split as a proportion of the base image's vertical resolution (0-1).
1082        """
1083        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):
1085    def set_and_lock_split_from_manager(self, x_percent, y_percent):
1086        """Set and lock the position of the split of the active subwindow as percent of base image's resolution (convenience function).
1087        
1088        Args:
1089            x_percent (float): The position of the split as a proportion of the base image's horizontal resolution (0-1).
1090            y_percent (float): The position of the split as a proportion of the base image's vertical resolution (0-1).
1091        """
1092        self.set_split(x_percent, y_percent, apply_to_all=False, ignore_lock=True)
1093        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):
1095    def shortcut_shift_x_was_activated_on_mdichild(self):
1096        """Update interface button for split lock based on lock status of active subwindow."""
1097        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):
1099    @QtCore.pyqtSlot()
1100    def on_scrollChanged(self):
1101        """Refresh position of split of all subwindows based on their respective last position."""
1102        windows = self._mdiArea.subWindowList()
1103        for window in windows:
1104            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):
1106    def on_subwindow_closed(self):
1107        """Record that a subwindow was closed upon the closing of a subwindow."""
1108        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):
1110    @QtCore.pyqtSlot()
1111    def on_mouse_leaved(self):
1112        """Update displayed coordinates of mouse as N/A upon the mouse leaving the subwindow area."""
1113        self._label_mouse.setText("View pixel coordinates: ( N/A , N/A )")
1114        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):
1116    @QtCore.pyqtSlot(QtCore.QPoint)
1117    def on_positionChanged(self, pos):
1118        """Update displayed coordinates of mouse on the active subwindow using global coordinates."""
1119    
1120        point_of_mouse_on_viewport = QtCore.QPointF(pos.x(), pos.y())
1121        pos_qcursor_global = QtGui.QCursor.pos()
1122        
1123        if self.activeMdiChild:
1124        
1125            # Use mouse position to grab scene coordinates (activeMdiChild?)
1126            active_view = self.activeMdiChild._view_main_topleft
1127            point_of_mouse_on_scene = active_view.mapToScene(point_of_mouse_on_viewport.x(), point_of_mouse_on_viewport.y())
1128
1129            if not self._label_mouse.isVisible():
1130                self._label_mouse.show()
1131            self._label_mouse.setText("View pixel coordinates: ( x = %d , y = %d )" % (point_of_mouse_on_scene.x(), point_of_mouse_on_scene.y()))
1132            
1133            pos_qcursor_view = active_view.mapFromGlobal(pos_qcursor_global)
1134            pos_qcursor_scene = active_view.mapToScene(pos_qcursor_view)
1135            # print("Cursor coords scene: ( %d , %d )" % (pos_qcursor_scene.x(), pos_qcursor_scene.y()))
1136            
1137        else:
1138            
1139            self._label_mouse.setText("View pixel coordinates: ( N/A , N/A )")
1140            
1141        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):
1147    @QtCore.pyqtSlot(int)
1148    def on_slider_opacity_base_changed(self, value):
1149        """Set transparency of base of sliding overlay of active subwindow.
1150        
1151        Triggered upon change in interface transparency slider.
1152        Temporarily sets position of split to the center of the visible area to give user a preview of the transparency effect.
1153
1154        Args:
1155            value (float,int): The transparency as percent opacity, where 100 is opaque (not transparent) and 0 is transparent (0-100).
1156        """
1157        if not self.activeMdiChild:
1158            return
1159        if not self.activeMdiChild.split_locked:
1160            self.set_split_from_slider()
1161        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):
1163    @QtCore.pyqtSlot(int)
1164    def on_slider_opacity_topright_changed(self, value):
1165        """Set transparency of top-right of sliding overlay of active subwindow.
1166        
1167        Triggered upon change in interface transparency slider.
1168        Temporarily sets position of split to the center of the visible area to give user a preview of the transparency effect.
1169
1170        Args:
1171            value (float,int): The transparency as percent opacity, where 100 is opaque (not transparent) and 0 is transparent (0-100).
1172        """
1173        if not self.activeMdiChild:
1174            return
1175        if not self.activeMdiChild.split_locked:
1176            self.set_split_from_slider()
1177        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):
1179    @QtCore.pyqtSlot(int)
1180    def on_slider_opacity_bottomright_changed(self, value):
1181        """Set transparency of bottom-right of sliding overlay of active subwindow.
1182        
1183        Triggered upon change in interface transparency slider.
1184        Temporarily sets position of split to the center of the visible area to give user a preview of the transparency effect.
1185
1186        Args:
1187            value (float,int): The transparency as percent opacity, where 100 is opaque (not transparent) and 0 is transparent (0-100).
1188        """
1189        if not self.activeMdiChild:
1190            return
1191        if not self.activeMdiChild.split_locked:
1192            self.set_split_from_slider()    
1193        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):
1195    @QtCore.pyqtSlot(int)
1196    def on_slider_opacity_bottomleft_changed(self, value):
1197        """Set transparency of bottom-left of sliding overlay of active subwindow.
1198        
1199        Triggered upon change in interface transparency slider.
1200        Temporarily sets position of split to the center of the visible area to give user a preview of the transparency effect.
1201
1202        Args:
1203            value (float,int): The transparency as percent opacity, where 100 is opaque (not transparent) and 0 is transparent (0-100).
1204        """
1205        if not self.activeMdiChild:
1206            return
1207        if not self.activeMdiChild.split_locked:
1208            self.set_split_from_slider()
1209        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):
1211    def update_sliders(self, window):
1212        """Update interface transparency sliders upon subwindow activating using the subwindow transparency values.
1213        
1214        Args:
1215            window (QMdiSubWindow): The active subwindow.
1216        """
1217        if window is None:
1218            self._sliders_opacity_splitviews.reset_sliders()
1219            return
1220
1221        child = self.activeMdiChild
1222        
1223        self._sliders_opacity_splitviews.set_enabled(True, child.pixmap_topright_exists, child.pixmap_bottomright_exists, child.pixmap_bottomleft_exists)
1224
1225        opacity_base_of_activeMdiChild = child._opacity_base
1226        opacity_topright_of_activeMdiChild = child._opacity_topright
1227        opacity_bottomright_of_activeMdiChild = child._opacity_bottomright
1228        opacity_bottomleft_of_activeMdiChild = child._opacity_bottomleft
1229
1230        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):
1235    def createMappedAction(self, icon, text, parent, shortcut, methodName):
1236        """Create |QAction| that is mapped via methodName to call.
1237
1238        :param icon: icon associated with |QAction|
1239        :type icon: |QIcon| or None
1240        :param str text: the |QAction| descriptive text
1241        :param QObject parent: the parent |QObject|
1242        :param QKeySequence shortcut: the shortcut |QKeySequence|
1243        :param str methodName: name of method to call when |QAction| is
1244                               triggered
1245        :rtype: |QAction|"""
1246
1247        if icon is not None:
1248            action = QtWidgets.QAction(icon, text, parent,
1249                                   shortcut=shortcut,
1250                                   triggered=self._actionMapper.map)
1251        else:
1252            action = QtWidgets.QAction(text, parent,
1253                                   shortcut=shortcut,
1254                                   triggered=self._actionMapper.map)
1255        self._actionMapper.setMapping(action, methodName)
1256        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):
1258    def createActions(self):
1259        """Create actions used in menus."""
1260        #File menu actions
1261        self._openAct = QtWidgets.QAction(
1262            "&Open...", self,
1263            shortcut=QtGui.QKeySequence.Open,
1264            statusTip="Open an existing file",
1265            triggered=self.open)
1266
1267        self._switchLayoutDirectionAct = QtWidgets.QAction(
1268            "Switch &layout direction", self,
1269            triggered=self.switchLayoutDirection)
1270
1271        #create dummy recent file actions
1272        for i in range(MultiViewMainWindow.MaxRecentFiles):
1273            self._recentFileActions.append(
1274                QtWidgets.QAction(self, visible=False,
1275                              triggered=self._recentFileMapper.map))
1276
1277        self._exitAct = QtWidgets.QAction(
1278            "E&xit", self,
1279            shortcut=QtGui.QKeySequence.Quit,
1280            statusTip="Exit the application",
1281            triggered=QtWidgets.QApplication.closeAllWindows)
1282
1283        #View menu actions
1284        self._showScrollbarsAct = QtWidgets.QAction(
1285            "&Scrollbars", self,
1286            checkable=True,
1287            statusTip="Toggle display of subwindow scrollbars",
1288            triggered=self.toggleScrollbars)
1289
1290        self._showStatusbarAct = QtWidgets.QAction(
1291            "S&tatusbar", self,
1292            checkable=True,
1293            statusTip="Toggle display of statusbar",
1294            triggered=self.toggleStatusbar)
1295
1296        self._synchZoomAct = QtWidgets.QAction(
1297            "Synch &Zoom", self,
1298            checkable=True,
1299            statusTip="Synch zooming of subwindows",
1300            triggered=self.toggleSynchZoom)
1301
1302        self._synchPanAct = QtWidgets.QAction(
1303            "Synch &Pan", self,
1304            checkable=True,
1305            statusTip="Synch panning of subwindows",
1306            triggered=self.toggleSynchPan)
1307
1308        #Scroll menu actions
1309        self._scrollActions = [
1310            self.createMappedAction(
1311                None,
1312                "&Top", self,
1313                QtGui.QKeySequence.MoveToStartOfDocument,
1314                "scrollToTop"),
1315
1316            self.createMappedAction(
1317                None,
1318                "&Bottom", self,
1319                QtGui.QKeySequence.MoveToEndOfDocument,
1320                "scrollToBottom"),
1321
1322            self.createMappedAction(
1323                None,
1324                "&Left Edge", self,
1325                QtGui.QKeySequence.MoveToStartOfLine,
1326                "scrollToBegin"),
1327
1328            self.createMappedAction(
1329                None,
1330                "&Right Edge", self,
1331                QtGui.QKeySequence.MoveToEndOfLine,
1332                "scrollToEnd"),
1333
1334            self.createMappedAction(
1335                None,
1336                "&Center", self,
1337                "5",
1338                "centerView"),
1339            ]
1340
1341        #zoom menu actions
1342        separatorAct = QtWidgets.QAction(self)
1343        separatorAct.setSeparator(True)
1344
1345        self._zoomActions = [
1346            self.createMappedAction(
1347                None,
1348                "Zoo&m In (25%)", self,
1349                QtGui.QKeySequence.ZoomIn,
1350                "zoomIn"),
1351
1352            self.createMappedAction(
1353                None,
1354                "Zoom &Out (25%)", self,
1355                QtGui.QKeySequence.ZoomOut,
1356                "zoomOut"),
1357
1358            #self.createMappedAction(
1359                #None,
1360                #"&Zoom To...", self,
1361                #"Z",
1362                #"zoomTo"),
1363
1364            separatorAct,
1365
1366            self.createMappedAction(
1367                None,
1368                "Actual &Size", self,
1369                "/",
1370                "actualSize"),
1371
1372            self.createMappedAction(
1373                None,
1374                "Fit &Image", self,
1375                "*",
1376                "fitToWindow"),
1377
1378            self.createMappedAction(
1379                None,
1380                "Fit &Width", self,
1381                "Alt+Right",
1382                "fitWidth"),
1383
1384            self.createMappedAction(
1385                None,
1386                "Fit &Height", self,
1387                "Alt+Down",
1388                "fitHeight"),
1389           ]
1390
1391        #Window menu actions
1392        self._activateSubWindowSystemMenuAct = QtWidgets.QAction(
1393            "Activate &System Menu", self,
1394            shortcut="Ctrl+ ",
1395            statusTip="Activate subwindow System Menu",
1396            triggered=self.activateSubwindowSystemMenu)
1397
1398        self._closeAct = QtWidgets.QAction(
1399            "Cl&ose", self,
1400            shortcut=QtGui.QKeySequence.Close,
1401            shortcutContext=QtCore.Qt.WidgetShortcut,
1402            #shortcut="Ctrl+Alt+F4",
1403            statusTip="Close the active window",
1404            triggered=self._mdiArea.closeActiveSubWindow)
1405
1406        self._closeAllAct = QtWidgets.QAction(
1407            "Close &All", self,
1408            statusTip="Close all the windows",
1409            triggered=self._mdiArea.closeAllSubWindows)
1410
1411        self._tileAct = QtWidgets.QAction(
1412            "&Tile", self,
1413            statusTip="Tile the windows",
1414            triggered=self._mdiArea.tileSubWindows)
1415
1416        self._tileAct.triggered.connect(self.tile_and_fit_mdiArea)
1417
1418        self._cascadeAct = QtWidgets.QAction(
1419            "&Cascade", self,
1420            statusTip="Cascade the windows",
1421            triggered=self._mdiArea.cascadeSubWindows)
1422
1423        self._nextAct = QtWidgets.QAction(
1424            "Ne&xt", self,
1425            shortcut=QtGui.QKeySequence.NextChild,
1426            statusTip="Move the focus to the next window",
1427            triggered=self._mdiArea.activateNextSubWindow)
1428
1429        self._previousAct = QtWidgets.QAction(
1430            "Pre&vious", self,
1431            shortcut=QtGui.QKeySequence.PreviousChild,
1432            statusTip="Move the focus to the previous window",
1433            triggered=self._mdiArea.activatePreviousSubWindow)
1434
1435        self._separatorAct = QtWidgets.QAction(self)
1436        self._separatorAct.setSeparator(True)
1437
1438        self._aboutAct = QtWidgets.QAction(
1439            "&About", self,
1440            statusTip="Show the application's About box",
1441            triggered=self.about)
1442
1443        self._aboutQtAct = QtWidgets.QAction(
1444            "About &Qt", self,
1445            statusTip="Show the Qt library's About box",
1446            triggered=QtWidgets.QApplication.aboutQt)

Create actions used in menus.

def createMenus(self):
1448    def createMenus(self):
1449        """Create menus."""
1450        self._fileMenu = self.menuBar().addMenu("&File")
1451        self._fileMenu.addAction(self._openAct)
1452        self._fileMenu.addAction(self._switchLayoutDirectionAct)
1453
1454        self._fileSeparatorAct = self._fileMenu.addSeparator()
1455        for action in self._recentFileActions:
1456            self._fileMenu.addAction(action)
1457        self.updateRecentFileActions()
1458        self._fileMenu.addSeparator()
1459        self._fileMenu.addAction(self._exitAct)
1460
1461        self._viewMenu = self.menuBar().addMenu("&View")
1462        self._viewMenu.addAction(self._showScrollbarsAct)
1463        self._viewMenu.addAction(self._showStatusbarAct)
1464        self._viewMenu.addSeparator()
1465        self._viewMenu.addAction(self._synchZoomAct)
1466        self._viewMenu.addAction(self._synchPanAct)
1467
1468        self._scrollMenu = self.menuBar().addMenu("&Scroll")
1469        [self._scrollMenu.addAction(action) for action in self._scrollActions]
1470
1471        self._zoomMenu = self.menuBar().addMenu("&Zoom")
1472        [self._zoomMenu.addAction(action) for action in self._zoomActions]
1473
1474        self._windowMenu = self.menuBar().addMenu("&Window")
1475        self.updateWindowMenu()
1476        self._windowMenu.aboutToShow.connect(self.updateWindowMenu)
1477
1478        self.menuBar().addSeparator()
1479
1480        self._helpMenu = self.menuBar().addMenu("&Help")
1481        self._helpMenu.addAction(self._aboutAct)
1482        self._helpMenu.addAction(self._aboutQtAct)

Create menus.

def updateMenus(self):
1484    def updateMenus(self):
1485        """Update menus."""
1486        hasMdiChild = (self.activeMdiChild is not None)
1487
1488        self._scrollMenu.setEnabled(hasMdiChild)
1489        self._zoomMenu.setEnabled(hasMdiChild)
1490
1491        self._closeAct.setEnabled(hasMdiChild)
1492        self._closeAllAct.setEnabled(hasMdiChild)
1493
1494        self._tileAct.setEnabled(hasMdiChild)
1495        self._cascadeAct.setEnabled(hasMdiChild)
1496        self._nextAct.setEnabled(hasMdiChild)
1497        self._previousAct.setEnabled(hasMdiChild)
1498        self._separatorAct.setVisible(hasMdiChild)

Update menus.

def updateRecentFileActions(self):
1500    def updateRecentFileActions(self):
1501        """Update recent file menu items."""
1502        settings = QtCore.QSettings()
1503        files = settings.value(SETTING_RECENTFILELIST)
1504        numRecentFiles = min(len(files) if files else 0,
1505                             MultiViewMainWindow.MaxRecentFiles)
1506
1507        for i in range(numRecentFiles):
1508            text = "&%d %s" % (i + 1, strippedName(files[i]))
1509            self._recentFileActions[i].setText(text)
1510            self._recentFileActions[i].setData(files[i])
1511            self._recentFileActions[i].setVisible(True)
1512            self._recentFileMapper.setMapping(self._recentFileActions[i],
1513                                              files[i])
1514
1515        for j in range(numRecentFiles, MultiViewMainWindow.MaxRecentFiles):
1516            self._recentFileActions[j].setVisible(False)
1517
1518        self._fileSeparatorAct.setVisible((numRecentFiles > 0))

Update recent file menu items.

def updateWindowMenu(self):
1520    def updateWindowMenu(self):
1521        """Update the Window menu."""
1522        self._windowMenu.clear()
1523        self._windowMenu.addAction(self._closeAct)
1524        self._windowMenu.addAction(self._closeAllAct)
1525        self._windowMenu.addSeparator()
1526        self._windowMenu.addAction(self._tileAct)
1527        self._windowMenu.addAction(self._cascadeAct)
1528        self._windowMenu.addSeparator()
1529        self._windowMenu.addAction(self._nextAct)
1530        self._windowMenu.addAction(self._previousAct)
1531        self._windowMenu.addAction(self._separatorAct)
1532
1533        windows = self._mdiArea.subWindowList()
1534        self._separatorAct.setVisible(len(windows) != 0)
1535
1536        for i, window in enumerate(windows):
1537            child = window.widget()
1538
1539            text = "%d %s" % (i + 1, child.userFriendlyCurrentFile)
1540            if i < 9:
1541                text = '&' + text
1542
1543            action = self._windowMenu.addAction(text)
1544            action.setCheckable(True)
1545            action.setChecked(child == self.activeMdiChild)
1546            action.triggered.connect(self._windowMapper.map)
1547            self._windowMapper.setMapping(action, window)

Update the Window menu.

def createStatusBarLabel(self, stretch=0):
1549    def createStatusBarLabel(self, stretch=0):
1550        """Create status bar label.
1551
1552        :param int stretch: stretch factor
1553        :rtype: |QLabel|"""
1554        label = QtWidgets.QLabel()
1555        label.setFrameStyle(QtWidgets.QFrame.Panel | QtWidgets.QFrame.Sunken)
1556        label.setLineWidth(2)
1557        self.statusBar().addWidget(label, stretch)
1558        return label

Create status bar label.

Parameters
  • int stretch: stretch factor
def createStatusBar(self):
1560    def createStatusBar(self):
1561        """Create status bar."""
1562        statusBar = self.statusBar()
1563
1564        self._sbLabelName = self.createStatusBarLabel(1)
1565        self._sbLabelSize = self.createStatusBarLabel()
1566        self._sbLabelDimensions = self.createStatusBarLabel()
1567        self._sbLabelDate = self.createStatusBarLabel()
1568        self._sbLabelZoom = self.createStatusBarLabel()
1569
1570        statusBar.showMessage("Ready")

Create status bar.

activeMdiChild

Get active MDI child (SplitViewMdiChild or None).

def closeEvent(self, event):
1582    def closeEvent(self, event):
1583        """Overrides close event to save application settings.
1584
1585        :param QEvent event: instance of |QEvent|"""
1586
1587        if self.is_fullscreen: # Needed to properly close the image viewer if the main window is closed while the viewer is fullscreen
1588            self.is_fullscreen = False
1589            self.setCentralWidget(self.mdiarea_plus_buttons)
1590
1591        self._mdiArea.closeAllSubWindows()
1592        if self.activeMdiChild:
1593            event.ignore()
1594        else:
1595            self.writeSettings()
1596            event.accept()

Overrides close event to save application settings.

Parameters
  • QEvent event: instance of |QEvent|
@QtCore.pyqtSlot(str)
def mappedImageViewerAction(self, methodName):
1605    @QtCore.pyqtSlot(str)
1606    def mappedImageViewerAction(self, methodName):
1607        """Perform action mapped to :class:`aux_splitview.SplitView`
1608        methodName.
1609
1610        :param str methodName: method to call"""
1611        activeViewer = self.activeMdiChild
1612        if hasattr(activeViewer, str(methodName)):
1613            getattr(activeViewer, str(methodName))()

Perform action mapped to aux_splitview.SplitView methodName.

Parameters
  • str methodName: method to call
@QtCore.pyqtSlot()
def toggleSynchPan(self):
1615    @QtCore.pyqtSlot()
1616    def toggleSynchPan(self):
1617        """Toggle synchronized subwindow panning."""
1618        if self._synchPanAct.isChecked():
1619            self.synchPan(self.activeMdiChild)

Toggle synchronized subwindow panning.

@QtCore.pyqtSlot()
def panChanged(self):
1621    @QtCore.pyqtSlot()
1622    def panChanged(self):
1623        """Synchronize subwindow pans."""
1624        mdiChild = self.sender()
1625        while mdiChild is not None and type(mdiChild) != SplitViewMdiChild:
1626            mdiChild = mdiChild.parent()
1627        if mdiChild and self._synchPanAct.isChecked():
1628            self.synchPan(mdiChild)

Synchronize subwindow pans.

@QtCore.pyqtSlot()
def toggleSynchZoom(self):
1630    @QtCore.pyqtSlot()
1631    def toggleSynchZoom(self):
1632        """Toggle synchronized subwindow zooming."""
1633        if self._synchZoomAct.isChecked():
1634            self.synchZoom(self.activeMdiChild)

Toggle synchronized subwindow zooming.

@QtCore.pyqtSlot()
def zoomChanged(self):
1636    @QtCore.pyqtSlot()
1637    def zoomChanged(self):
1638        """Synchronize subwindow zooms."""
1639        mdiChild = self.sender()
1640        if self._synchZoomAct.isChecked():
1641            self.synchZoom(mdiChild)
1642        self.updateStatusBar()

Synchronize subwindow zooms.

def synchPan(self, fromViewer):
1644    def synchPan(self, fromViewer):
1645        """Synch panning of all subwindowws to the same as *fromViewer*.
1646
1647        :param fromViewer: :class:`SplitViewMdiChild` that initiated synching"""
1648
1649        assert isinstance(fromViewer, SplitViewMdiChild)
1650        if not fromViewer:
1651            return
1652        if self._handlingScrollChangedSignal:
1653            return
1654        if fromViewer.parent() != self._mdiArea.activeSubWindow(): # Prevent circular scroll state change signals from propagating
1655            if fromViewer.parent() != self:
1656                return
1657        self._handlingScrollChangedSignal = True
1658
1659        newState = fromViewer.scrollState
1660        changedWindow = fromViewer.parent()
1661        windows = self._mdiArea.subWindowList()
1662        for window in windows:
1663            if window != changedWindow:
1664                if window.widget().sync_this_pan:
1665                    window.widget().scrollState = newState
1666                    window.widget().resize_scene()
1667
1668        self._handlingScrollChangedSignal = False

Synch panning of all subwindowws to the same as fromViewer.

Parameters
def synchZoom(self, fromViewer):
1670    def synchZoom(self, fromViewer):
1671        """Synch zoom of all subwindowws to the same as *fromViewer*.
1672
1673        :param fromViewer: :class:`SplitViewMdiChild` that initiated synching"""
1674        if not fromViewer:
1675            return
1676        newZoomFactor = fromViewer.zoomFactor
1677
1678        sync_by = self.sync_zoom_by
1679
1680        sender_dimension = determineSyncSenderDimension(fromViewer.imageWidth,
1681                                                        fromViewer.imageHeight,
1682                                                        sync_by)
1683
1684        changedWindow = fromViewer.parent()
1685        windows = self._mdiArea.subWindowList()
1686        for window in windows:
1687            if window != changedWindow:
1688                receiver = window.widget()
1689                if receiver.sync_this_zoom:
1690                    adjustment_factor = determineSyncAdjustmentFactor(sync_by,
1691                                                                      sender_dimension,
1692                                                                      receiver.imageWidth,
1693                                                                      receiver.imageHeight)
1694
1695                    receiver.zoomFactor = newZoomFactor*adjustment_factor
1696                    receiver.resize_scene()
1697        self.refreshPan()

Synch zoom of all subwindowws to the same as fromViewer.

Parameters
@QtCore.pyqtSlot()
def activateSubwindowSystemMenu(self):
1713    @QtCore.pyqtSlot()
1714    def activateSubwindowSystemMenu(self):
1715        """Activate current subwindow's System Menu."""
1716        activeSubWindow = self._mdiArea.activeSubWindow()
1717        if activeSubWindow:
1718            activeSubWindow.showSystemMenu()

Activate current subwindow's System Menu.

@QtCore.pyqtSlot(str)
def openRecentFile(self, filename_main_topleft):
1720    @QtCore.pyqtSlot(str)
1721    def openRecentFile(self, filename_main_topleft):
1722        """Open a recent file.
1723
1724        :param str filename_main_topleft: filename_main_topleft to view"""
1725        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):
1727    @QtCore.pyqtSlot()
1728    def open(self):
1729        """Handle the open action."""
1730        fileDialog = QtWidgets.QFileDialog(self)
1731        settings = QtCore.QSettings()
1732        fileDialog.setNameFilters([
1733            "Common image files (*.jpeg *.jpg  *.png *.tiff *.tif *.bmp *.gif *.webp *.svg)",
1734            "JPEG image files (*.jpeg *.jpg)", 
1735            "PNG image files (*.png)", 
1736            "TIFF image files (*.tiff *.tif)",
1737            "BMP (*.bmp)",
1738            "All files (*)",])
1739        if not settings.contains(SETTING_FILEOPEN + "/state"):
1740            fileDialog.setDirectory(".")
1741        else:
1742            self.restoreDialogState(fileDialog, SETTING_FILEOPEN)
1743        fileDialog.setFileMode(QtWidgets.QFileDialog.ExistingFile)
1744        if not fileDialog.exec_():
1745            return
1746        self.saveDialogState(fileDialog, SETTING_FILEOPEN)
1747
1748        filename_main_topleft = fileDialog.selectedFiles()[0]
1749        self.loadFile(filename_main_topleft, None, None, None)

Handle the open action.

def open_multiple(self):
1751    def open_multiple(self):
1752        """Handle the open multiple action."""
1753        last_accessed_fullpath = self._last_accessed_fullpath
1754        filters = "\
1755            Common image files (*.jpeg *.jpg  *.png *.tiff *.tif *.bmp *.gif *.webp *.svg);;\
1756            JPEG image files (*.jpeg *.jpg);;\
1757            PNG image files (*.png);;\
1758            TIFF image files (*.tiff *.tif);;\
1759            BMP (*.bmp);;\
1760            All files (*)"
1761        fullpaths, _ = QtWidgets.QFileDialog.getOpenFileNames(self, "Select image(s) to open", last_accessed_fullpath, filters)
1762
1763        for fullpath in fullpaths:
1764            self.loadFile(fullpath, None, None, None)

Handle the open multiple action.

@QtCore.pyqtSlot()
def toggleScrollbars(self):
1768    @QtCore.pyqtSlot()
1769    def toggleScrollbars(self):
1770        """Toggle subwindow scrollbar visibility."""
1771        checked = self._showScrollbarsAct.isChecked()
1772
1773        windows = self._mdiArea.subWindowList()
1774        for window in windows:
1775            child = window.widget()
1776            child.enableScrollBars(checked)

Toggle subwindow scrollbar visibility.

@QtCore.pyqtSlot()
def toggleStatusbar(self):
1778    @QtCore.pyqtSlot()
1779    def toggleStatusbar(self):
1780        """Toggle status bar visibility."""
1781        self.statusBar().setVisible(self._showStatusbarAct.isChecked())

Toggle status bar visibility.

@QtCore.pyqtSlot()
def about(self):
1784    @QtCore.pyqtSlot()
1785    def about(self):
1786        """Display About dialog box."""
1787        QtWidgets.QMessageBox.about(self, "About MDI",
1788                "<b>MDI Image Viewer</b> demonstrates how to"
1789                "synchronize the panning and zooming of multiple image"
1790                "viewer windows using Qt.")

Display About dialog box.

@QtCore.pyqtSlot(QtWidgets.QMdiSubWindow)
def subWindowActivated(self, window):
1791    @QtCore.pyqtSlot(QtWidgets.QMdiSubWindow)
1792    def subWindowActivated(self, window):
1793        """Handle |QMdiSubWindow| activated signal.
1794
1795        :param |QMdiSubWindow| window: |QMdiSubWindow| that was just
1796                                       activated"""
1797        self.updateStatusBar()

Handle |QMdiSubWindow| activated signal.

Parameters
  • |QMdiSubWindow| window: |QMdiSubWindow| that was just activated
@QtCore.pyqtSlot(QtWidgets.QMdiSubWindow)
def setActiveSubWindow(self, window):
1799    @QtCore.pyqtSlot(QtWidgets.QMdiSubWindow)
1800    def setActiveSubWindow(self, window):
1801        """Set active |QMdiSubWindow|.
1802
1803        :param |QMdiSubWindow| window: |QMdiSubWindow| to activate """
1804        if window:
1805            self._mdiArea.setActiveSubWindow(window)

Set active |QMdiSubWindow|.

Parameters
  • |QMdiSubWindow| window: |QMdiSubWindow| to activate
def updateStatusBar(self):
1808    def updateStatusBar(self):
1809        """Update status bar."""
1810        self.statusBar().setVisible(self._showStatusbarAct.isChecked())
1811        imageViewer = self.activeMdiChild
1812        if not imageViewer:
1813            self._sbLabelName.setText("")
1814            self._sbLabelSize.setText("")
1815            self._sbLabelDimensions.setText("")
1816            self._sbLabelDate.setText("")
1817            self._sbLabelZoom.setText("")
1818
1819            self._sbLabelSize.hide()
1820            self._sbLabelDimensions.hide()
1821            self._sbLabelDate.hide()
1822            self._sbLabelZoom.hide()
1823            return
1824
1825        filename_main_topleft = imageViewer.currentFile
1826        self._sbLabelName.setText(" %s " % filename_main_topleft)
1827
1828        fi = QtCore.QFileInfo(filename_main_topleft)
1829        size = fi.size()
1830        fmt = " %.1f %s "
1831        if size > 1024*1024*1024:
1832            unit = "MB"
1833            size /= 1024*1024*1024
1834        elif size > 1024*1024:
1835            unit = "MB"
1836            size /= 1024*1024
1837        elif size > 1024:
1838            unit = "KB"
1839            size /= 1024
1840        else:
1841            unit = "Bytes"
1842            fmt = " %d %s "
1843        self._sbLabelSize.setText(fmt % (size, unit))
1844
1845        pixmap = imageViewer.pixmap_main_topleft
1846        self._sbLabelDimensions.setText(" %dx%dx%d " %
1847                                        (pixmap.width(),
1848                                         pixmap.height(),
1849                                         pixmap.depth()))
1850
1851        self._sbLabelDate.setText(
1852            " %s " %
1853            fi.lastModified().toString(QtCore.Qt.SystemLocaleShortDate))
1854        self._sbLabelZoom.setText(" %0.f%% " % (imageViewer.zoomFactor*100,))
1855
1856        self._sbLabelSize.show()
1857        self._sbLabelDimensions.show()
1858        self._sbLabelDate.show()
1859        self._sbLabelZoom.show()

Update status bar.

def switchLayoutDirection(self):
1861    def switchLayoutDirection(self):
1862        """Switch MDI subwindow layout direction."""
1863        if self.layoutDirection() == QtCore.Qt.LeftToRight:
1864            QtWidgets.QApplication.setLayoutDirection(QtCore.Qt.RightToLeft)
1865        else:
1866            QtWidgets.QApplication.setLayoutDirection(QtCore.Qt.LeftToRight)

Switch MDI subwindow layout direction.

def saveDialogState(self, dialog, groupName):
1868    def saveDialogState(self, dialog, groupName):
1869        """Save dialog state, position & size.
1870
1871        :param |QDialog| dialog: dialog to save state of
1872        :param str groupName: |QSettings| group name"""
1873        assert isinstance(dialog, QtWidgets.QDialog)
1874
1875        settings = QtCore.QSettings()
1876        settings.beginGroup(groupName)
1877
1878        settings.setValue('state', dialog.saveState())
1879        settings.setValue('geometry', dialog.saveGeometry())
1880        settings.setValue('filter', dialog.selectedNameFilter())
1881
1882        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):
1884    def restoreDialogState(self, dialog, groupName):
1885        """Restore dialog state, position & size.
1886
1887        :param str groupName: |QSettings| group name"""
1888        assert isinstance(dialog, QtWidgets.QDialog)
1889
1890        settings = QtCore.QSettings()
1891        settings.beginGroup(groupName)
1892
1893        dialog.restoreState(settings.value('state'))
1894        dialog.restoreGeometry(settings.value('geometry'))
1895        dialog.selectNameFilter(settings.value('filter', ""))
1896
1897        settings.endGroup()

Restore dialog state, position & size.

Parameters
  • str groupName: |QSettings| group name
def writeSettings(self):
1899    def writeSettings(self):
1900        """Write application settings."""
1901        settings = QtCore.QSettings()
1902        settings.setValue('pos', self.pos())
1903        settings.setValue('size', self.size())
1904        settings.setValue('windowgeometry', self.saveGeometry())
1905        settings.setValue('windowstate', self.saveState())
1906
1907        settings.setValue(SETTING_SCROLLBARS,
1908                          self._showScrollbarsAct.isChecked())
1909        settings.setValue(SETTING_STATUSBAR,
1910                          self._showStatusbarAct.isChecked())
1911        settings.setValue(SETTING_SYNCHZOOM,
1912                          self._synchZoomAct.isChecked())
1913        settings.setValue(SETTING_SYNCHPAN,
1914                          self._synchPanAct.isChecked())

Write application settings.

def readSettings(self):
1916    def readSettings(self):
1917        """Read application settings."""
1918        
1919        scrollbars_always_checked_off_at_startup = True
1920        statusbar_always_checked_off_at_startup = True
1921        sync_always_checked_on_at_startup = True
1922
1923        settings = QtCore.QSettings()
1924
1925        pos = settings.value('pos', QtCore.QPoint(100, 100))
1926        size = settings.value('size', QtCore.QSize(1100, 600))
1927        self.move(pos)
1928        self.resize(size)
1929
1930        if settings.contains('windowgeometry'):
1931            self.restoreGeometry(settings.value('windowgeometry'))
1932        if settings.contains('windowstate'):
1933            self.restoreState(settings.value('windowstate'))
1934
1935        
1936        if scrollbars_always_checked_off_at_startup:
1937            self._showScrollbarsAct.setChecked(False)
1938        else:
1939            self._showScrollbarsAct.setChecked(
1940                toBool(settings.value(SETTING_SCROLLBARS, False)))
1941
1942        if statusbar_always_checked_off_at_startup:
1943            self._showStatusbarAct.setChecked(False)
1944        else:
1945            self._showStatusbarAct.setChecked(
1946                toBool(settings.value(SETTING_STATUSBAR, False)))
1947
1948        if sync_always_checked_on_at_startup:
1949            self._synchZoomAct.setChecked(True)
1950            self._synchPanAct.setChecked(True)
1951        else:
1952            self._synchZoomAct.setChecked(
1953                toBool(settings.value(SETTING_SYNCHZOOM, False)))
1954            self._synchPanAct.setChecked(
1955                toBool(settings.value(SETTING_SYNCHPAN, False)))

Read application settings.

def updateRecentFileSettings(self, filename_main_topleft, delete=False):
1957    def updateRecentFileSettings(self, filename_main_topleft, delete=False):
1958        """Update recent file list setting.
1959
1960        :param str filename_main_topleft: filename_main_topleft to add or remove from recent file
1961                             list
1962        :param bool delete: if True then filename_main_topleft removed, otherwise added"""
1963        settings = QtCore.QSettings()
1964        
1965        try:
1966            files = list(settings.value(SETTING_RECENTFILELIST, []))
1967        except TypeError:
1968            files = []
1969
1970        try:
1971            files.remove(filename_main_topleft)
1972        except ValueError:
1973            pass
1974
1975        if not delete:
1976            files.insert(0, filename_main_topleft)
1977        del files[MultiViewMainWindow.MaxRecentFiles:]
1978
1979        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():
1983def main():
1984    """Run MultiViewMainWindow as main app.
1985    
1986    Attributes:
1987        app (QApplication): Starts and holds the main event loop of application.
1988        mainWin (MultiViewMainWindow): The main window.
1989    """
1990    import sys
1991
1992    app = QtWidgets.QApplication(sys.argv)
1993    QtCore.QSettings.setDefaultFormat(QtCore.QSettings.IniFormat)
1994    app.setOrganizationName(COMPANY)
1995    app.setOrganizationDomain(DOMAIN)
1996    app.setApplicationName(APPNAME)
1997    app.setApplicationVersion(VERSION)
1998    app.setWindowIcon(QtGui.QIcon(":/icons/icon.png"))
1999
2000    mainWin = MultiViewMainWindow()
2001    mainWin.setWindowTitle(APPNAME)
2002
2003    mainWin.show()
2004
2005    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.