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()
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.
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
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).
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].
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.
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.
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)
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).
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.
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.
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).
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.
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.
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.
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.
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).
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).
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).
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.
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.
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.
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.
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.
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).
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).
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).
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).
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.
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
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.
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.
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.
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.
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.
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
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.
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|
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
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.
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.
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.
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.
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
- fromViewer:
SplitViewMdiChild
that initiated synching
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
- fromViewer:
SplitViewMdiChild
that initiated synching
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.
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
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.
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.
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.
1778 @QtCore.pyqtSlot() 1779 def toggleStatusbar(self): 1780 """Toggle status bar visibility.""" 1781 self.statusBar().setVisible(self._showStatusbarAct.isChecked())
Toggle status bar visibility.
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.
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
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
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.
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.
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
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
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.
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.
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
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.