butterfly_viewer.aux_dragdrop
Drag-and-drop interface widgets and their supporting subwidgets for Butterfly Viewer.
Not intended as a script.
Interface widgets are:
DragDropImageLabel, for users to drag and drop an image from local storage and show a preview in the drop area. FourDragDropImageLabel, a 2x2 panel of DragDropImageLabel designed for users to arrange images for a SplitView.
1#!/usr/bin/env python3 2 3"""Drag-and-drop interface widgets and their supporting subwidgets for Butterfly Viewer. 4 5Not intended as a script. 6 7Interface widgets are: 8 DragDropImageLabel, for users to drag and drop an image from local storage and show a preview in the drop area. 9 FourDragDropImageLabel, a 2x2 panel of DragDropImageLabel designed for users to arrange images for a SplitView. 10""" 11# SPDX-License-Identifier: GPL-3.0-or-later 12 13 14 15import sys 16import time 17 18from PyQt5 import QtCore, QtGui, QtWidgets 19 20from aux_labels import FilenameLabel 21from aux_exif import get_exif_rotation_angle 22 23 24class ImageLabel(QtWidgets.QLabel): 25 """Custom QLabel as a drag-and-drop zone for images. 26 27 Instantiate without input. 28 """ 29 30 became_occupied = QtCore.pyqtSignal(bool) 31 32 def __init__(self, text=None, is_addable=True): 33 super().__init__() 34 35 self.IS_ADDABLE = is_addable 36 self.IS_OCCUPIED = False 37 38 # self.setMargin(4) 39 self.setAlignment(QtCore.Qt.AlignCenter) 40 41 if text: 42 self.setText(text) 43 44 self.set_stylesheet_occupied(self.IS_OCCUPIED) 45 46 47 def setPixmap(self, image): 48 """QPixmap: Extend setPixmap() to also set style and size, and execute supporting functions.""" 49 size = self.size() 50 self.IS_OCCUPIED = True 51 self.original_pixmap = image 52 self.set_pixmap_to_label_size() 53 self.set_stylesheet_occupied(self.IS_OCCUPIED) 54 self.became_occupied.emit(True) 55 self.setFixedSize(size) 56 57 def set_pixmap_to_label_size(self): 58 """Resize and set pixmap to the label's size, thus maintaining the label's size and shape.""" 59 w = self.width_contents() 60 h = self.height_contents() 61 p = self.original_pixmap 62 p = p.scaled(w, h, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation) 63 super().setPixmap(p) 64 65 def clear(self): 66 """Extend clear() to also set style and size, reduce memory.""" 67 self.IS_OCCUPIED = False 68 self.set_stylesheet_occupied(self.IS_OCCUPIED) 69 70 if self.original_pixmap: 71 del self.original_pixmap # Remove from memory 72 73 super().clear() 74 75 self.became_occupied.emit(False) 76 77 self.setMinimumSize(QtCore.QSize(0,0)) 78 self.setMinimumSize(QtCore.QSize(QtWidgets.QWIDGETSIZE_MAX,QtWidgets.QWIDGETSIZE_MAX)) 79 80 81 def width_contents(self): 82 """float: Width of the label's contents excluding the frame width.""" 83 return self.width() - 2*self.frameWidth() 84 85 def height_contents(self): 86 """float: Height of the label's contents excluding the frame width.""" 87 return self.height() - 2*self.frameWidth() 88 89 # Styles 90 91 def stylesheet_unoccupied_notaddable(self): 92 """Set label stylesheet to unoccupied and unaddable state.""" 93 self.setStyleSheet("QLabel {font-size: 9pt; border: 0.13em dashed lightGray; border-radius: 0.5em; background-color: transparent;}") 94 95 def stylesheet_unoccupied_addable(self): 96 """Set label stylesheet to unoccupied and addable state.""" 97 self.setStyleSheet("QLabel {font-size: 9pt; border: 0.13em dashed gray; border-radius: 0.5em; background-color: transparent;}") 98 99 def stylesheet_occupied_notaddable(self): 100 """Set label stylesheet to occupied and unaddable state.""" 101 self.setStyleSheet("QLabel font-size: 9pt; border: 0.13em dashed gray; border-radius: 0.5em; background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #6F6F6F, stop: 1 #3F3F3F);}") 102 103 def stylesheet_occupied_addable(self): 104 """Set label stylesheet to occupied and addable state.""" 105 self.setStyleSheet("QLabel {font-size: 9pt; border: 0.13em dashed gray; border-radius: 0.5em; background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #3F3F3F, stop: 1 #0F0F0F);}") 106 107 def stylesheet_hovered_unoccupied(self): 108 """Set label stylesheet to hovered and unoccupied.""" 109 self.setStyleSheet("QLabel {font-size: 9pt; border: 0.13em dashed black; border-radius: 0.5em; background-color: rgba(0,0,0,63);}") 110 111 def stylesheet_hovered_occupied(self): 112 """Set label stylesheet to hovered and occupied.""" 113 self.setStyleSheet("QLabel {font-size: 9pt; border: 0.13em dashed black; border-radius: 0.5em; background-color: rgba(0,0,0,63);}") 114 115 def set_stylesheet_addable(self, boolean): 116 """bool: Set addable state of label stylesheet, considering current occupied state.""" 117 if boolean: 118 if self.IS_OCCUPIED: 119 self.stylesheet_occupied_addable() 120 else: 121 self.stylesheet_unoccupied_addable() 122 else: 123 if self.IS_OCCUPIED: 124 self.stylesheet_occupied_notaddable() 125 else: 126 self.stylesheet_unoccupied_notaddable() 127 128 def set_stylesheet_occupied(self, boolean): 129 """bool: Set occupied state of label stylesheet, considering current addable state.""" 130 if boolean: 131 if self.IS_ADDABLE: 132 self.stylesheet_occupied_addable() 133 else: 134 self.stylesheet_occupied_notaddable() 135 else: 136 if self.IS_ADDABLE: 137 self.stylesheet_unoccupied_addable() 138 else: 139 self.stylesheet_unoccupied_notaddable() 140 141 def set_stylesheet_hovered(self, boolean): 142 """bool: Set hovered state of label stylesheet, considering current occupied and addable states.""" 143 if boolean: 144 if self.IS_OCCUPIED: 145 self.stylesheet_hovered_occupied() 146 else: 147 self.stylesheet_hovered_unoccupied() 148 else: 149 if self.IS_ADDABLE: 150 if self.IS_OCCUPIED: 151 self.stylesheet_occupied_addable() 152 else: 153 self.stylesheet_unoccupied_addable() 154 else: 155 if self.IS_OCCUPIED: 156 self.stylesheet_occupied_notaddable() 157 else: 158 self.stylesheet_unoccupied_notaddable() 159 160 161 162class ImageLabelMain(ImageLabel): 163 """Extend ImageLabel as 'main' drag-and-drop zone for SplitViewCreator. 164 165 Instantiate without input. 166 """ 167 def __init__(self, text=None): 168 super().__init__(text) 169 170 171 172class DragDropImageLabel(QtWidgets.QWidget): 173 """Drag-and-drop widget to preview an image from local storage and hold its filepath. 174 175 Includes: 176 Button to select an image from a dialog window. 177 Button to clear the current image. 178 179 Args: 180 show_filename (bool): True to show label with filename over image preview; False to hide. 181 show_pushbuttons (bool): True to show button for selecting file from dialog and button to clear image; False to hide. 182 is_main (bool): True if the label is the drag zone for the main image of SplitView; False if not. 183 text_default (str): Text to show when no image preview is showing. 184 """ 185 186 became_occupied = QtCore.pyqtSignal(bool) 187 188 def __init__(self, show_filename=False, show_pushbuttons=True, is_main=False, text_default="Drag image"): 189 super().__init__() 190 191 self.file_path = None 192 self.show_filepath_while_loading = False 193 194 self.text_default = text_default 195 196 self.MAX_DIMENSION_FOR_IMAGE_IN_LABEL = 400 197 198 self.show_filename = show_filename 199 self.show_pushbuttons = show_pushbuttons 200 201 self.image_filetypes = [ 202 ".jpeg", ".jpg", ".jpe", ".jif", ".jfif", ".jfi", ".pjpeg", ".pjp", 203 ".png", ".apng", 204 ".tiff", ".tif", 205 ".bmp", 206 ".gif", 207 ".webp", 208 ".svg", 209 ".ico", ".cur"] 210 211 self.setAcceptDrops(True) 212 213 main_layout = QtWidgets.QGridLayout() 214 215 if is_main: 216 self.image_label_child = ImageLabelMain() 217 else: 218 self.image_label_child = ImageLabel() 219 220 self.image_label_child.became_occupied.connect(self.became_occupied) 221 222 self.set_text(self.text_default) 223 224 main_layout.addWidget(self.image_label_child, 0, 0) 225 226 self.filename_label = FilenameLabel("No filename available", remove_path=True) 227 main_layout.addWidget(self.filename_label, 0, 0, QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft) 228 self.filename_label.setVisible(False) 229 230 if self.show_pushbuttons is True: 231 self.buttons_layout = QtWidgets.QGridLayout() 232 self.clear_layout = QtWidgets.QGridLayout() 233 234 self.open_pushbutton = QtWidgets.QToolButton() 235 self.open_pushbutton.setText("Select image...") 236 self.open_pushbutton.setToolTip("Select image from file and add here in sliding overlay creator...") 237 self.open_pushbutton.setStyleSheet(""" 238 QToolButton { 239 font-size: 9pt; 240 } 241 """) 242 243 self.clear_pushbutton = QtWidgets.QToolButton() 244 self.clear_pushbutton.setText("×") 245 self.clear_pushbutton.setToolTip("Clear image") 246 self.clear_pushbutton.setStyleSheet(""" 247 QToolButton { 248 font-size: 9pt; 249 } 250 """) 251 252 self.open_pushbutton.clicked.connect(self.was_clicked_open_pushbutton) 253 self.clear_pushbutton.clicked.connect(self.was_clicked_clear_pushbutton) 254 255 w = 8 256 257 self.buttons_layout.addWidget(self.open_pushbutton, 0, 0) 258 self.buttons_layout.setContentsMargins(w,w,w,w) 259 self.buttons_layout.setSpacing(w) 260 261 self.clear_layout.addWidget(self.clear_pushbutton, 0, 0) 262 self.clear_layout.setContentsMargins(w,w,w,w) 263 self.clear_layout.setSpacing(w) 264 265 main_layout.addLayout(self.buttons_layout, 0, 0, QtCore.Qt.AlignBottom) 266 main_layout.addLayout(self.clear_layout, 0, 0, QtCore.Qt.AlignTop|QtCore.Qt.AlignRight) 267 268 self.clear_pushbutton.setEnabled(False) 269 self.clear_pushbutton.setVisible(False) 270 271 272 self.loading_grayout_label = QtWidgets.QLabel("Loading...") 273 self.loading_grayout_label.setAlignment(QtCore.Qt.AlignCenter | QtCore.Qt.AlignVCenter) 274 self.loading_grayout_label.setVisible(False) 275 self.loading_grayout_label.setStyleSheet(""" 276 QLabel { 277 color: white; 278 font-size: 7.5pt; 279 background-color: rgba(0,0,0,223); 280 } 281 """) 282 283 main_layout.addWidget(self.loading_grayout_label, 0, 0) 284 285 main_layout.setContentsMargins(2, 2, 2, 2) 286 main_layout.setSpacing(0) 287 288 self.setLayout(main_layout) 289 290 def set_addable(self, boolean): 291 """bool: Set whether an imaged may be added (dragged and dropped) into the widget.""" 292 self.image_label_child.IS_ADDABLE = boolean 293 self.image_label_child.setEnabled(boolean) 294 self.open_pushbutton.setEnabled(boolean) 295 self.setAcceptDrops(boolean) 296 self.filename_label.setEnabled(boolean) 297 self.image_label_child.set_stylesheet_addable(boolean) 298 299 300 def dragEnterEvent(self, event): 301 """event: Override dragEnterEvent() to set stylesheet as hovered and read filepath from a dragged image, but reject multiple files.""" 302 if len(event.mimeData().urls()) is 1 and self.grab_image_urls_from_mimedata(event.mimeData()): 303 self.image_label_child.set_stylesheet_hovered(True) 304 event.accept() 305 else: 306 event.ignore() 307 308 def dragMoveEvent(self, event): 309 """event: Override dragMoveEvent() to reject multiple files.""" 310 if len(event.mimeData().urls()) is 1 and self.grab_image_urls_from_mimedata(event.mimeData()): 311 event.accept() 312 else: 313 event.ignore() 314 315 def dragLeaveEvent(self, event): 316 """event: Override dragLeaveEvent() to set stylesheet as not hovered.""" 317 self.image_label_child.set_stylesheet_hovered(False) 318 319 def dropEvent(self, event): 320 """event: Override dropEvent() to read filepath from a dragged image and load image preview.""" 321 urls = self.grab_image_urls_from_mimedata(event.mimeData()) 322 if len(urls) is 1 and urls: 323 event.setDropAction(QtCore.Qt.CopyAction) 324 file_path = urls[0].toLocalFile() 325 loaded = self.load_image(file_path) 326 if loaded: 327 event.accept() 328 else: 329 event.ignore() 330 self.image_label_child.set_stylesheet_hovered(False) 331 else: 332 event.ignore() 333 334 def grab_image_urls_from_mimedata(self, mimedata): 335 """mimeData: Get urls (filepaths) from drag event.""" 336 urls = list() 337 for url in mimedata.urls(): 338 if any([filetype in url.toLocalFile().lower() for filetype in self.image_filetypes]): 339 urls.append(url) 340 return urls 341 342 def mouseDoubleClickEvent(self, event): 343 """event: Override mouseDoubleClickEvent() to trigger dialog window to open image.""" 344 self.open_image_via_dialog() 345 346 def was_clicked_open_pushbutton(self): 347 """Trigger dialog window to open image when button to select image is clicked.""" 348 self.open_image_via_dialog() 349 350 def was_clicked_clear_pushbutton(self): 351 """Clear image preview when clear button is clicked.""" 352 self.clear_image() 353 354 def set_image(self, pixmap): 355 """QPixmap: Scale and set preview of image; set status of clear button.""" 356 self.image_label_child.setPixmap(pixmap.scaled(self.MAX_DIMENSION_FOR_IMAGE_IN_LABEL, self.MAX_DIMENSION_FOR_IMAGE_IN_LABEL, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)) 357 self.clear_pushbutton.setEnabled(True) 358 self.clear_pushbutton.setVisible(True) 359 360 def load_image(self, file_path): 361 """str: Load image from filepath with loading grayout; set filename text. 362 363 Returns: 364 loaded (bool): True if image successfully loaded; False if not.""" 365 loading_text = "Loading..." 366 if self.show_filepath_while_loading: 367 loading_text = loading_text.replace("...", " '" + file_path.split("/")[-1] + "'...") 368 self.display_loading_grayout(True, loading_text) 369 pixmap = QtGui.QPixmap(file_path) 370 if pixmap.depth() is 0: 371 self.display_loading_grayout(False) 372 return False 373 374 angle = get_exif_rotation_angle(file_path) 375 if angle: 376 pixmap = pixmap.transformed(QtGui.QTransform().rotate(angle)) 377 378 self.set_image(pixmap) 379 self.set_filename_label(file_path) 380 self.file_path = file_path 381 self.display_loading_grayout(False) 382 return True 383 384 def open_image_via_dialog(self): 385 """Open dialog window to select and load image from file.""" 386 file_dialog = QtWidgets.QFileDialog(self) 387 388 file_dialog.setNameFilters([ 389 "Common image files (*.jpeg *.jpg *.png *.tiff *.tif *.bmp *.gif *.webp *.svg)", 390 "All files (*)", 391 "JPEG image files (*.jpeg *.jpg)", 392 "PNG image files (*.png)", 393 "TIFF image files (*.tiff *.tif)", 394 "BMP (*.bmp)"]) 395 file_dialog.setFileMode(QtWidgets.QFileDialog.ExistingFile) 396 397 if not file_dialog.exec_(): 398 return 399 400 file_path = file_dialog.selectedFiles()[0] 401 402 self.load_image(file_path) 403 404 def clear_image(self): 405 """Clear image preview and filepath; set status of clear button; set text of drag zone.""" 406 if self.image_label_child.pixmap(): 407 self.image_label_child.clear() 408 409 self.set_text(self.text_default) 410 self.file_path = None 411 self.clear_pushbutton.setEnabled(False) 412 self.clear_pushbutton.setVisible(False) 413 self.filename_label.setText("No filename available") 414 self.filename_label.setVisible(False) 415 416 def set_text(self, text): 417 """str: Set text of drag zone when there is no image preview.""" 418 text_margin_vertical = "\n\n\n" 419 self.image_label_child.setText(text_margin_vertical+text+text_margin_vertical) 420 421 def set_filename_label(self, text): 422 """str: Set text of filename label on image preview.""" 423 self.filename_label.setText(text) 424 self.filename_label.setVisible(self.show_filename) 425 426 def display_loading_grayout(self, boolean, text="Loading...", pseudo_load_time=0.2): 427 """Show/hide grayout overlay on label for loading sequences. 428 429 Args: 430 boolean (bool): True to show grayout; False to hide. 431 text (str): The text to show on the grayout. 432 pseudo_load_time (float): The delay (in seconds) to hide the grayout to give users a feeling of action. 433 """ 434 if not boolean: 435 text = "Loading..." 436 self.loading_grayout_label.setText(text) 437 self.loading_grayout_label.setVisible(boolean) 438 if boolean: 439 self.loading_grayout_label.repaint() 440 if not boolean: 441 time.sleep(pseudo_load_time) 442 443 444 445class FourDragDropImageLabel(QtWidgets.QFrame): 446 """2x2 panel of drag-and-drop zones for users to arrange images for a SplitView. 447 448 Instantiate without input. 449 450 Allows dragging multiple files (1–4) at once. 451 """ 452 453 will_start_loading = QtCore.pyqtSignal(bool, str) 454 has_stopped_loading = QtCore.pyqtSignal(bool) 455 456 def __init__(self): 457 super().__init__() 458 459 self.image_filetypes = [ 460 ".jpeg", ".jpg", ".jpe", ".jif", ".jfif", ".jfi", ".pjpeg", ".pjp", 461 ".png", ".apng", 462 ".tiff", ".tif", 463 ".bmp", 464 ".gif", 465 ".webp", 466 ".svg", 467 ".ico", ".cur"] 468 469 self.setAcceptDrops(True) 470 471 main_layout = QtWidgets.QGridLayout() 472 473 self.app_main_topleft = DragDropImageLabel(show_filename=True, show_pushbuttons=True, is_main=True, text_default="Drag image(s)") 474 self.app_topright = DragDropImageLabel(show_filename=True, show_pushbuttons=True) 475 self.app_bottomleft = DragDropImageLabel(show_filename=True, show_pushbuttons=True) 476 self.app_bottomright = DragDropImageLabel(show_filename=True, show_pushbuttons=True) 477 478 main_layout.addWidget(self.app_main_topleft, 0, 0) 479 main_layout.addWidget(self.app_topright, 0, 1) 480 main_layout.addWidget(self.app_bottomleft, 1, 0) 481 main_layout.addWidget(self.app_bottomright, 1, 1) 482 483 main_layout.setColumnStretch(0,1) 484 main_layout.setColumnStretch(1,1) 485 main_layout.setRowStretch(0,1) 486 main_layout.setRowStretch(1,1) 487 488 contents_margins_w = 0 489 main_layout.setContentsMargins(contents_margins_w, contents_margins_w, contents_margins_w, contents_margins_w) 490 main_layout.setSpacing(4) 491 492 self.setLayout(main_layout) 493 494 def dragEnterEvent(self, event): 495 """Override dragEnterEvent() to accept multiple (1-4) image files and set stylesheet(s) as hovered.""" 496 urls = self.grab_image_urls_from_mimedata(event.mimeData()) 497 if len(event.mimeData().urls()) >= 1 and len(event.mimeData().urls()) <= 4 and self.grab_image_urls_from_mimedata(event.mimeData()): 498 self.app_main_topleft.image_label_child.set_stylesheet_hovered(True) 499 i = 0 500 if len(urls) >= 2: 501 i += 1 502 self.app_topright.image_label_child.set_stylesheet_hovered(True) 503 504 if len(urls) >= 3: 505 i += 1 506 self.app_bottomright.image_label_child.set_stylesheet_hovered(True) 507 508 if len(urls) >= 4: 509 i += 1 510 self.app_bottomleft.image_label_child.set_stylesheet_hovered(True) 511 event.accept() 512 else: 513 event.ignore() 514 515 def dragMoveEvent(self, event): 516 """Override dragMoveEvent() to accept multiple (1-4) image files.""" 517 if len(event.mimeData().urls()) >= 1 and len(event.mimeData().urls()) <= 4 and self.grab_image_urls_from_mimedata(event.mimeData()): 518 event.accept() 519 else: 520 event.ignore() 521 522 def dragLeaveEvent(self, event): 523 """Override dragLeaveEvent() to set stylesheet(s) as no longer hovered.""" 524 self.app_main_topleft.image_label_child.set_stylesheet_hovered(False) 525 self.app_topright.image_label_child.set_stylesheet_hovered(False) 526 self.app_bottomright.image_label_child.set_stylesheet_hovered(False) 527 self.app_bottomleft.image_label_child.set_stylesheet_hovered(False) 528 529 def dropEvent(self, event): 530 """event: Override dropEvent() to read filepath(s) from 1-4 dragged images and load the preview(s).""" 531 urls = self.grab_image_urls_from_mimedata(event.mimeData()) 532 n = len(urls) 533 n_str = str(n) 534 if n >= 1 and n <= 4 and urls: 535 event.setDropAction(QtCore.Qt.CopyAction) 536 i = 0 537 file_path = urls[i].toLocalFile() 538 539 self.will_start_loading.emit(True, "Loading to creator " + str(i+1) + "/" + n_str + "...") 540 541 loaded = self.app_main_topleft.load_image(file_path) 542 if not loaded: 543 self.app_main_topleft.image_label_child.set_stylesheet_hovered(False) 544 545 if n >= 2: 546 i += 1 547 file_path = urls[i].toLocalFile() 548 self.will_start_loading.emit(True, "Loading to creator " + str(i+1) + "/" + n_str + "...") 549 loaded = self.app_topright.load_image(file_path) 550 if not loaded: 551 self.app_topright.image_label_child.set_stylesheet_hovered(False) 552 553 if n >= 3: 554 i += 1 555 file_path = urls[i].toLocalFile() 556 self.will_start_loading.emit(True, "Loading to creator " + str(i+1) + "/" + n_str + "...") 557 loaded = self.app_bottomright.load_image(file_path) 558 if not loaded: 559 self.app_bottomright.image_label_child.set_stylesheet_hovered(False) 560 561 if n >= 4: 562 i += 1 563 file_path = urls[i].toLocalFile() 564 self.will_start_loading.emit(True, "Loading to creator " + str(i+1) + "/" + n_str + "...") 565 loaded = self.app_bottomleft.load_image(file_path) 566 if not loaded: 567 self.app_bottomleft.image_label_child.set_stylesheet_hovered(False) 568 569 self.has_stopped_loading.emit(False) 570 571 event.accept() 572 else: 573 event.ignore() 574 575 def grab_image_urls_from_mimedata(self, mimedata): 576 """mimeData: Get urls (filepaths) from drag event.""" 577 urls = list() 578 for url in mimedata.urls(): 579 if any([filetype in url.toLocalFile().lower() for filetype in self.image_filetypes]): 580 urls.append(url) 581 return urls 582 583 584 585def main(): 586 """Demo the drag-and-drop function in the 2x2 panel.""" 587 588 app = QtWidgets.QApplication(sys.argv) 589 590 demo = FourDragDropImageLabel() 591 demo.show() 592 593 sys.exit(app.exec_()) 594 595 596 597if __name__ == '__main__': 598 main()
25class ImageLabel(QtWidgets.QLabel): 26 """Custom QLabel as a drag-and-drop zone for images. 27 28 Instantiate without input. 29 """ 30 31 became_occupied = QtCore.pyqtSignal(bool) 32 33 def __init__(self, text=None, is_addable=True): 34 super().__init__() 35 36 self.IS_ADDABLE = is_addable 37 self.IS_OCCUPIED = False 38 39 # self.setMargin(4) 40 self.setAlignment(QtCore.Qt.AlignCenter) 41 42 if text: 43 self.setText(text) 44 45 self.set_stylesheet_occupied(self.IS_OCCUPIED) 46 47 48 def setPixmap(self, image): 49 """QPixmap: Extend setPixmap() to also set style and size, and execute supporting functions.""" 50 size = self.size() 51 self.IS_OCCUPIED = True 52 self.original_pixmap = image 53 self.set_pixmap_to_label_size() 54 self.set_stylesheet_occupied(self.IS_OCCUPIED) 55 self.became_occupied.emit(True) 56 self.setFixedSize(size) 57 58 def set_pixmap_to_label_size(self): 59 """Resize and set pixmap to the label's size, thus maintaining the label's size and shape.""" 60 w = self.width_contents() 61 h = self.height_contents() 62 p = self.original_pixmap 63 p = p.scaled(w, h, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation) 64 super().setPixmap(p) 65 66 def clear(self): 67 """Extend clear() to also set style and size, reduce memory.""" 68 self.IS_OCCUPIED = False 69 self.set_stylesheet_occupied(self.IS_OCCUPIED) 70 71 if self.original_pixmap: 72 del self.original_pixmap # Remove from memory 73 74 super().clear() 75 76 self.became_occupied.emit(False) 77 78 self.setMinimumSize(QtCore.QSize(0,0)) 79 self.setMinimumSize(QtCore.QSize(QtWidgets.QWIDGETSIZE_MAX,QtWidgets.QWIDGETSIZE_MAX)) 80 81 82 def width_contents(self): 83 """float: Width of the label's contents excluding the frame width.""" 84 return self.width() - 2*self.frameWidth() 85 86 def height_contents(self): 87 """float: Height of the label's contents excluding the frame width.""" 88 return self.height() - 2*self.frameWidth() 89 90 # Styles 91 92 def stylesheet_unoccupied_notaddable(self): 93 """Set label stylesheet to unoccupied and unaddable state.""" 94 self.setStyleSheet("QLabel {font-size: 9pt; border: 0.13em dashed lightGray; border-radius: 0.5em; background-color: transparent;}") 95 96 def stylesheet_unoccupied_addable(self): 97 """Set label stylesheet to unoccupied and addable state.""" 98 self.setStyleSheet("QLabel {font-size: 9pt; border: 0.13em dashed gray; border-radius: 0.5em; background-color: transparent;}") 99 100 def stylesheet_occupied_notaddable(self): 101 """Set label stylesheet to occupied and unaddable state.""" 102 self.setStyleSheet("QLabel font-size: 9pt; border: 0.13em dashed gray; border-radius: 0.5em; background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #6F6F6F, stop: 1 #3F3F3F);}") 103 104 def stylesheet_occupied_addable(self): 105 """Set label stylesheet to occupied and addable state.""" 106 self.setStyleSheet("QLabel {font-size: 9pt; border: 0.13em dashed gray; border-radius: 0.5em; background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #3F3F3F, stop: 1 #0F0F0F);}") 107 108 def stylesheet_hovered_unoccupied(self): 109 """Set label stylesheet to hovered and unoccupied.""" 110 self.setStyleSheet("QLabel {font-size: 9pt; border: 0.13em dashed black; border-radius: 0.5em; background-color: rgba(0,0,0,63);}") 111 112 def stylesheet_hovered_occupied(self): 113 """Set label stylesheet to hovered and occupied.""" 114 self.setStyleSheet("QLabel {font-size: 9pt; border: 0.13em dashed black; border-radius: 0.5em; background-color: rgba(0,0,0,63);}") 115 116 def set_stylesheet_addable(self, boolean): 117 """bool: Set addable state of label stylesheet, considering current occupied state.""" 118 if boolean: 119 if self.IS_OCCUPIED: 120 self.stylesheet_occupied_addable() 121 else: 122 self.stylesheet_unoccupied_addable() 123 else: 124 if self.IS_OCCUPIED: 125 self.stylesheet_occupied_notaddable() 126 else: 127 self.stylesheet_unoccupied_notaddable() 128 129 def set_stylesheet_occupied(self, boolean): 130 """bool: Set occupied state of label stylesheet, considering current addable state.""" 131 if boolean: 132 if self.IS_ADDABLE: 133 self.stylesheet_occupied_addable() 134 else: 135 self.stylesheet_occupied_notaddable() 136 else: 137 if self.IS_ADDABLE: 138 self.stylesheet_unoccupied_addable() 139 else: 140 self.stylesheet_unoccupied_notaddable() 141 142 def set_stylesheet_hovered(self, boolean): 143 """bool: Set hovered state of label stylesheet, considering current occupied and addable states.""" 144 if boolean: 145 if self.IS_OCCUPIED: 146 self.stylesheet_hovered_occupied() 147 else: 148 self.stylesheet_hovered_unoccupied() 149 else: 150 if self.IS_ADDABLE: 151 if self.IS_OCCUPIED: 152 self.stylesheet_occupied_addable() 153 else: 154 self.stylesheet_unoccupied_addable() 155 else: 156 if self.IS_OCCUPIED: 157 self.stylesheet_occupied_notaddable() 158 else: 159 self.stylesheet_unoccupied_notaddable()
Custom QLabel as a drag-and-drop zone for images.
Instantiate without input.
48 def setPixmap(self, image): 49 """QPixmap: Extend setPixmap() to also set style and size, and execute supporting functions.""" 50 size = self.size() 51 self.IS_OCCUPIED = True 52 self.original_pixmap = image 53 self.set_pixmap_to_label_size() 54 self.set_stylesheet_occupied(self.IS_OCCUPIED) 55 self.became_occupied.emit(True) 56 self.setFixedSize(size)
QPixmap: Extend setPixmap() to also set style and size, and execute supporting functions.
58 def set_pixmap_to_label_size(self): 59 """Resize and set pixmap to the label's size, thus maintaining the label's size and shape.""" 60 w = self.width_contents() 61 h = self.height_contents() 62 p = self.original_pixmap 63 p = p.scaled(w, h, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation) 64 super().setPixmap(p)
Resize and set pixmap to the label's size, thus maintaining the label's size and shape.
66 def clear(self): 67 """Extend clear() to also set style and size, reduce memory.""" 68 self.IS_OCCUPIED = False 69 self.set_stylesheet_occupied(self.IS_OCCUPIED) 70 71 if self.original_pixmap: 72 del self.original_pixmap # Remove from memory 73 74 super().clear() 75 76 self.became_occupied.emit(False) 77 78 self.setMinimumSize(QtCore.QSize(0,0)) 79 self.setMinimumSize(QtCore.QSize(QtWidgets.QWIDGETSIZE_MAX,QtWidgets.QWIDGETSIZE_MAX))
Extend clear() to also set style and size, reduce memory.
82 def width_contents(self): 83 """float: Width of the label's contents excluding the frame width.""" 84 return self.width() - 2*self.frameWidth()
float: Width of the label's contents excluding the frame width.
86 def height_contents(self): 87 """float: Height of the label's contents excluding the frame width.""" 88 return self.height() - 2*self.frameWidth()
float: Height of the label's contents excluding the frame width.
92 def stylesheet_unoccupied_notaddable(self): 93 """Set label stylesheet to unoccupied and unaddable state.""" 94 self.setStyleSheet("QLabel {font-size: 9pt; border: 0.13em dashed lightGray; border-radius: 0.5em; background-color: transparent;}")
Set label stylesheet to unoccupied and unaddable state.
96 def stylesheet_unoccupied_addable(self): 97 """Set label stylesheet to unoccupied and addable state.""" 98 self.setStyleSheet("QLabel {font-size: 9pt; border: 0.13em dashed gray; border-radius: 0.5em; background-color: transparent;}")
Set label stylesheet to unoccupied and addable state.
100 def stylesheet_occupied_notaddable(self): 101 """Set label stylesheet to occupied and unaddable state.""" 102 self.setStyleSheet("QLabel font-size: 9pt; border: 0.13em dashed gray; border-radius: 0.5em; background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #6F6F6F, stop: 1 #3F3F3F);}")
Set label stylesheet to occupied and unaddable state.
104 def stylesheet_occupied_addable(self): 105 """Set label stylesheet to occupied and addable state.""" 106 self.setStyleSheet("QLabel {font-size: 9pt; border: 0.13em dashed gray; border-radius: 0.5em; background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #3F3F3F, stop: 1 #0F0F0F);}")
Set label stylesheet to occupied and addable state.
108 def stylesheet_hovered_unoccupied(self): 109 """Set label stylesheet to hovered and unoccupied.""" 110 self.setStyleSheet("QLabel {font-size: 9pt; border: 0.13em dashed black; border-radius: 0.5em; background-color: rgba(0,0,0,63);}")
Set label stylesheet to hovered and unoccupied.
112 def stylesheet_hovered_occupied(self): 113 """Set label stylesheet to hovered and occupied.""" 114 self.setStyleSheet("QLabel {font-size: 9pt; border: 0.13em dashed black; border-radius: 0.5em; background-color: rgba(0,0,0,63);}")
Set label stylesheet to hovered and occupied.
116 def set_stylesheet_addable(self, boolean): 117 """bool: Set addable state of label stylesheet, considering current occupied state.""" 118 if boolean: 119 if self.IS_OCCUPIED: 120 self.stylesheet_occupied_addable() 121 else: 122 self.stylesheet_unoccupied_addable() 123 else: 124 if self.IS_OCCUPIED: 125 self.stylesheet_occupied_notaddable() 126 else: 127 self.stylesheet_unoccupied_notaddable()
bool: Set addable state of label stylesheet, considering current occupied state.
129 def set_stylesheet_occupied(self, boolean): 130 """bool: Set occupied state of label stylesheet, considering current addable state.""" 131 if boolean: 132 if self.IS_ADDABLE: 133 self.stylesheet_occupied_addable() 134 else: 135 self.stylesheet_occupied_notaddable() 136 else: 137 if self.IS_ADDABLE: 138 self.stylesheet_unoccupied_addable() 139 else: 140 self.stylesheet_unoccupied_notaddable()
bool: Set occupied state of label stylesheet, considering current addable state.
142 def set_stylesheet_hovered(self, boolean): 143 """bool: Set hovered state of label stylesheet, considering current occupied and addable states.""" 144 if boolean: 145 if self.IS_OCCUPIED: 146 self.stylesheet_hovered_occupied() 147 else: 148 self.stylesheet_hovered_unoccupied() 149 else: 150 if self.IS_ADDABLE: 151 if self.IS_OCCUPIED: 152 self.stylesheet_occupied_addable() 153 else: 154 self.stylesheet_unoccupied_addable() 155 else: 156 if self.IS_OCCUPIED: 157 self.stylesheet_occupied_notaddable() 158 else: 159 self.stylesheet_unoccupied_notaddable()
bool: Set hovered state of label stylesheet, considering current occupied and addable states.
163class ImageLabelMain(ImageLabel): 164 """Extend ImageLabel as 'main' drag-and-drop zone for SplitViewCreator. 165 166 Instantiate without input. 167 """ 168 def __init__(self, text=None): 169 super().__init__(text)
Extend ImageLabel as 'main' drag-and-drop zone for SplitViewCreator.
Instantiate without input.
173class DragDropImageLabel(QtWidgets.QWidget): 174 """Drag-and-drop widget to preview an image from local storage and hold its filepath. 175 176 Includes: 177 Button to select an image from a dialog window. 178 Button to clear the current image. 179 180 Args: 181 show_filename (bool): True to show label with filename over image preview; False to hide. 182 show_pushbuttons (bool): True to show button for selecting file from dialog and button to clear image; False to hide. 183 is_main (bool): True if the label is the drag zone for the main image of SplitView; False if not. 184 text_default (str): Text to show when no image preview is showing. 185 """ 186 187 became_occupied = QtCore.pyqtSignal(bool) 188 189 def __init__(self, show_filename=False, show_pushbuttons=True, is_main=False, text_default="Drag image"): 190 super().__init__() 191 192 self.file_path = None 193 self.show_filepath_while_loading = False 194 195 self.text_default = text_default 196 197 self.MAX_DIMENSION_FOR_IMAGE_IN_LABEL = 400 198 199 self.show_filename = show_filename 200 self.show_pushbuttons = show_pushbuttons 201 202 self.image_filetypes = [ 203 ".jpeg", ".jpg", ".jpe", ".jif", ".jfif", ".jfi", ".pjpeg", ".pjp", 204 ".png", ".apng", 205 ".tiff", ".tif", 206 ".bmp", 207 ".gif", 208 ".webp", 209 ".svg", 210 ".ico", ".cur"] 211 212 self.setAcceptDrops(True) 213 214 main_layout = QtWidgets.QGridLayout() 215 216 if is_main: 217 self.image_label_child = ImageLabelMain() 218 else: 219 self.image_label_child = ImageLabel() 220 221 self.image_label_child.became_occupied.connect(self.became_occupied) 222 223 self.set_text(self.text_default) 224 225 main_layout.addWidget(self.image_label_child, 0, 0) 226 227 self.filename_label = FilenameLabel("No filename available", remove_path=True) 228 main_layout.addWidget(self.filename_label, 0, 0, QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft) 229 self.filename_label.setVisible(False) 230 231 if self.show_pushbuttons is True: 232 self.buttons_layout = QtWidgets.QGridLayout() 233 self.clear_layout = QtWidgets.QGridLayout() 234 235 self.open_pushbutton = QtWidgets.QToolButton() 236 self.open_pushbutton.setText("Select image...") 237 self.open_pushbutton.setToolTip("Select image from file and add here in sliding overlay creator...") 238 self.open_pushbutton.setStyleSheet(""" 239 QToolButton { 240 font-size: 9pt; 241 } 242 """) 243 244 self.clear_pushbutton = QtWidgets.QToolButton() 245 self.clear_pushbutton.setText("×") 246 self.clear_pushbutton.setToolTip("Clear image") 247 self.clear_pushbutton.setStyleSheet(""" 248 QToolButton { 249 font-size: 9pt; 250 } 251 """) 252 253 self.open_pushbutton.clicked.connect(self.was_clicked_open_pushbutton) 254 self.clear_pushbutton.clicked.connect(self.was_clicked_clear_pushbutton) 255 256 w = 8 257 258 self.buttons_layout.addWidget(self.open_pushbutton, 0, 0) 259 self.buttons_layout.setContentsMargins(w,w,w,w) 260 self.buttons_layout.setSpacing(w) 261 262 self.clear_layout.addWidget(self.clear_pushbutton, 0, 0) 263 self.clear_layout.setContentsMargins(w,w,w,w) 264 self.clear_layout.setSpacing(w) 265 266 main_layout.addLayout(self.buttons_layout, 0, 0, QtCore.Qt.AlignBottom) 267 main_layout.addLayout(self.clear_layout, 0, 0, QtCore.Qt.AlignTop|QtCore.Qt.AlignRight) 268 269 self.clear_pushbutton.setEnabled(False) 270 self.clear_pushbutton.setVisible(False) 271 272 273 self.loading_grayout_label = QtWidgets.QLabel("Loading...") 274 self.loading_grayout_label.setAlignment(QtCore.Qt.AlignCenter | QtCore.Qt.AlignVCenter) 275 self.loading_grayout_label.setVisible(False) 276 self.loading_grayout_label.setStyleSheet(""" 277 QLabel { 278 color: white; 279 font-size: 7.5pt; 280 background-color: rgba(0,0,0,223); 281 } 282 """) 283 284 main_layout.addWidget(self.loading_grayout_label, 0, 0) 285 286 main_layout.setContentsMargins(2, 2, 2, 2) 287 main_layout.setSpacing(0) 288 289 self.setLayout(main_layout) 290 291 def set_addable(self, boolean): 292 """bool: Set whether an imaged may be added (dragged and dropped) into the widget.""" 293 self.image_label_child.IS_ADDABLE = boolean 294 self.image_label_child.setEnabled(boolean) 295 self.open_pushbutton.setEnabled(boolean) 296 self.setAcceptDrops(boolean) 297 self.filename_label.setEnabled(boolean) 298 self.image_label_child.set_stylesheet_addable(boolean) 299 300 301 def dragEnterEvent(self, event): 302 """event: Override dragEnterEvent() to set stylesheet as hovered and read filepath from a dragged image, but reject multiple files.""" 303 if len(event.mimeData().urls()) is 1 and self.grab_image_urls_from_mimedata(event.mimeData()): 304 self.image_label_child.set_stylesheet_hovered(True) 305 event.accept() 306 else: 307 event.ignore() 308 309 def dragMoveEvent(self, event): 310 """event: Override dragMoveEvent() to reject multiple files.""" 311 if len(event.mimeData().urls()) is 1 and self.grab_image_urls_from_mimedata(event.mimeData()): 312 event.accept() 313 else: 314 event.ignore() 315 316 def dragLeaveEvent(self, event): 317 """event: Override dragLeaveEvent() to set stylesheet as not hovered.""" 318 self.image_label_child.set_stylesheet_hovered(False) 319 320 def dropEvent(self, event): 321 """event: Override dropEvent() to read filepath from a dragged image and load image preview.""" 322 urls = self.grab_image_urls_from_mimedata(event.mimeData()) 323 if len(urls) is 1 and urls: 324 event.setDropAction(QtCore.Qt.CopyAction) 325 file_path = urls[0].toLocalFile() 326 loaded = self.load_image(file_path) 327 if loaded: 328 event.accept() 329 else: 330 event.ignore() 331 self.image_label_child.set_stylesheet_hovered(False) 332 else: 333 event.ignore() 334 335 def grab_image_urls_from_mimedata(self, mimedata): 336 """mimeData: Get urls (filepaths) from drag event.""" 337 urls = list() 338 for url in mimedata.urls(): 339 if any([filetype in url.toLocalFile().lower() for filetype in self.image_filetypes]): 340 urls.append(url) 341 return urls 342 343 def mouseDoubleClickEvent(self, event): 344 """event: Override mouseDoubleClickEvent() to trigger dialog window to open image.""" 345 self.open_image_via_dialog() 346 347 def was_clicked_open_pushbutton(self): 348 """Trigger dialog window to open image when button to select image is clicked.""" 349 self.open_image_via_dialog() 350 351 def was_clicked_clear_pushbutton(self): 352 """Clear image preview when clear button is clicked.""" 353 self.clear_image() 354 355 def set_image(self, pixmap): 356 """QPixmap: Scale and set preview of image; set status of clear button.""" 357 self.image_label_child.setPixmap(pixmap.scaled(self.MAX_DIMENSION_FOR_IMAGE_IN_LABEL, self.MAX_DIMENSION_FOR_IMAGE_IN_LABEL, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)) 358 self.clear_pushbutton.setEnabled(True) 359 self.clear_pushbutton.setVisible(True) 360 361 def load_image(self, file_path): 362 """str: Load image from filepath with loading grayout; set filename text. 363 364 Returns: 365 loaded (bool): True if image successfully loaded; False if not.""" 366 loading_text = "Loading..." 367 if self.show_filepath_while_loading: 368 loading_text = loading_text.replace("...", " '" + file_path.split("/")[-1] + "'...") 369 self.display_loading_grayout(True, loading_text) 370 pixmap = QtGui.QPixmap(file_path) 371 if pixmap.depth() is 0: 372 self.display_loading_grayout(False) 373 return False 374 375 angle = get_exif_rotation_angle(file_path) 376 if angle: 377 pixmap = pixmap.transformed(QtGui.QTransform().rotate(angle)) 378 379 self.set_image(pixmap) 380 self.set_filename_label(file_path) 381 self.file_path = file_path 382 self.display_loading_grayout(False) 383 return True 384 385 def open_image_via_dialog(self): 386 """Open dialog window to select and load image from file.""" 387 file_dialog = QtWidgets.QFileDialog(self) 388 389 file_dialog.setNameFilters([ 390 "Common image files (*.jpeg *.jpg *.png *.tiff *.tif *.bmp *.gif *.webp *.svg)", 391 "All files (*)", 392 "JPEG image files (*.jpeg *.jpg)", 393 "PNG image files (*.png)", 394 "TIFF image files (*.tiff *.tif)", 395 "BMP (*.bmp)"]) 396 file_dialog.setFileMode(QtWidgets.QFileDialog.ExistingFile) 397 398 if not file_dialog.exec_(): 399 return 400 401 file_path = file_dialog.selectedFiles()[0] 402 403 self.load_image(file_path) 404 405 def clear_image(self): 406 """Clear image preview and filepath; set status of clear button; set text of drag zone.""" 407 if self.image_label_child.pixmap(): 408 self.image_label_child.clear() 409 410 self.set_text(self.text_default) 411 self.file_path = None 412 self.clear_pushbutton.setEnabled(False) 413 self.clear_pushbutton.setVisible(False) 414 self.filename_label.setText("No filename available") 415 self.filename_label.setVisible(False) 416 417 def set_text(self, text): 418 """str: Set text of drag zone when there is no image preview.""" 419 text_margin_vertical = "\n\n\n" 420 self.image_label_child.setText(text_margin_vertical+text+text_margin_vertical) 421 422 def set_filename_label(self, text): 423 """str: Set text of filename label on image preview.""" 424 self.filename_label.setText(text) 425 self.filename_label.setVisible(self.show_filename) 426 427 def display_loading_grayout(self, boolean, text="Loading...", pseudo_load_time=0.2): 428 """Show/hide grayout overlay on label for loading sequences. 429 430 Args: 431 boolean (bool): True to show grayout; False to hide. 432 text (str): The text to show on the grayout. 433 pseudo_load_time (float): The delay (in seconds) to hide the grayout to give users a feeling of action. 434 """ 435 if not boolean: 436 text = "Loading..." 437 self.loading_grayout_label.setText(text) 438 self.loading_grayout_label.setVisible(boolean) 439 if boolean: 440 self.loading_grayout_label.repaint() 441 if not boolean: 442 time.sleep(pseudo_load_time)
Drag-and-drop widget to preview an image from local storage and hold its filepath.
Includes:
Button to select an image from a dialog window. Button to clear the current image.
Arguments:
- show_filename (bool): True to show label with filename over image preview; False to hide.
- show_pushbuttons (bool): True to show button for selecting file from dialog and button to clear image; False to hide.
- is_main (bool): True if the label is the drag zone for the main image of SplitView; False if not.
- text_default (str): Text to show when no image preview is showing.
291 def set_addable(self, boolean): 292 """bool: Set whether an imaged may be added (dragged and dropped) into the widget.""" 293 self.image_label_child.IS_ADDABLE = boolean 294 self.image_label_child.setEnabled(boolean) 295 self.open_pushbutton.setEnabled(boolean) 296 self.setAcceptDrops(boolean) 297 self.filename_label.setEnabled(boolean) 298 self.image_label_child.set_stylesheet_addable(boolean)
bool: Set whether an imaged may be added (dragged and dropped) into the widget.
301 def dragEnterEvent(self, event): 302 """event: Override dragEnterEvent() to set stylesheet as hovered and read filepath from a dragged image, but reject multiple files.""" 303 if len(event.mimeData().urls()) is 1 and self.grab_image_urls_from_mimedata(event.mimeData()): 304 self.image_label_child.set_stylesheet_hovered(True) 305 event.accept() 306 else: 307 event.ignore()
event: Override dragEnterEvent() to set stylesheet as hovered and read filepath from a dragged image, but reject multiple files.
309 def dragMoveEvent(self, event): 310 """event: Override dragMoveEvent() to reject multiple files.""" 311 if len(event.mimeData().urls()) is 1 and self.grab_image_urls_from_mimedata(event.mimeData()): 312 event.accept() 313 else: 314 event.ignore()
event: Override dragMoveEvent() to reject multiple files.
316 def dragLeaveEvent(self, event): 317 """event: Override dragLeaveEvent() to set stylesheet as not hovered.""" 318 self.image_label_child.set_stylesheet_hovered(False)
event: Override dragLeaveEvent() to set stylesheet as not hovered.
320 def dropEvent(self, event): 321 """event: Override dropEvent() to read filepath from a dragged image and load image preview.""" 322 urls = self.grab_image_urls_from_mimedata(event.mimeData()) 323 if len(urls) is 1 and urls: 324 event.setDropAction(QtCore.Qt.CopyAction) 325 file_path = urls[0].toLocalFile() 326 loaded = self.load_image(file_path) 327 if loaded: 328 event.accept() 329 else: 330 event.ignore() 331 self.image_label_child.set_stylesheet_hovered(False) 332 else: 333 event.ignore()
event: Override dropEvent() to read filepath from a dragged image and load image preview.
335 def grab_image_urls_from_mimedata(self, mimedata): 336 """mimeData: Get urls (filepaths) from drag event.""" 337 urls = list() 338 for url in mimedata.urls(): 339 if any([filetype in url.toLocalFile().lower() for filetype in self.image_filetypes]): 340 urls.append(url) 341 return urls
mimeData: Get urls (filepaths) from drag event.
343 def mouseDoubleClickEvent(self, event): 344 """event: Override mouseDoubleClickEvent() to trigger dialog window to open image.""" 345 self.open_image_via_dialog()
event: Override mouseDoubleClickEvent() to trigger dialog window to open image.
355 def set_image(self, pixmap): 356 """QPixmap: Scale and set preview of image; set status of clear button.""" 357 self.image_label_child.setPixmap(pixmap.scaled(self.MAX_DIMENSION_FOR_IMAGE_IN_LABEL, self.MAX_DIMENSION_FOR_IMAGE_IN_LABEL, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)) 358 self.clear_pushbutton.setEnabled(True) 359 self.clear_pushbutton.setVisible(True)
QPixmap: Scale and set preview of image; set status of clear button.
361 def load_image(self, file_path): 362 """str: Load image from filepath with loading grayout; set filename text. 363 364 Returns: 365 loaded (bool): True if image successfully loaded; False if not.""" 366 loading_text = "Loading..." 367 if self.show_filepath_while_loading: 368 loading_text = loading_text.replace("...", " '" + file_path.split("/")[-1] + "'...") 369 self.display_loading_grayout(True, loading_text) 370 pixmap = QtGui.QPixmap(file_path) 371 if pixmap.depth() is 0: 372 self.display_loading_grayout(False) 373 return False 374 375 angle = get_exif_rotation_angle(file_path) 376 if angle: 377 pixmap = pixmap.transformed(QtGui.QTransform().rotate(angle)) 378 379 self.set_image(pixmap) 380 self.set_filename_label(file_path) 381 self.file_path = file_path 382 self.display_loading_grayout(False) 383 return True
str: Load image from filepath with loading grayout; set filename text.
Returns:
- loaded (bool): True if image successfully loaded; False if not.
385 def open_image_via_dialog(self): 386 """Open dialog window to select and load image from file.""" 387 file_dialog = QtWidgets.QFileDialog(self) 388 389 file_dialog.setNameFilters([ 390 "Common image files (*.jpeg *.jpg *.png *.tiff *.tif *.bmp *.gif *.webp *.svg)", 391 "All files (*)", 392 "JPEG image files (*.jpeg *.jpg)", 393 "PNG image files (*.png)", 394 "TIFF image files (*.tiff *.tif)", 395 "BMP (*.bmp)"]) 396 file_dialog.setFileMode(QtWidgets.QFileDialog.ExistingFile) 397 398 if not file_dialog.exec_(): 399 return 400 401 file_path = file_dialog.selectedFiles()[0] 402 403 self.load_image(file_path)
Open dialog window to select and load image from file.
405 def clear_image(self): 406 """Clear image preview and filepath; set status of clear button; set text of drag zone.""" 407 if self.image_label_child.pixmap(): 408 self.image_label_child.clear() 409 410 self.set_text(self.text_default) 411 self.file_path = None 412 self.clear_pushbutton.setEnabled(False) 413 self.clear_pushbutton.setVisible(False) 414 self.filename_label.setText("No filename available") 415 self.filename_label.setVisible(False)
Clear image preview and filepath; set status of clear button; set text of drag zone.
417 def set_text(self, text): 418 """str: Set text of drag zone when there is no image preview.""" 419 text_margin_vertical = "\n\n\n" 420 self.image_label_child.setText(text_margin_vertical+text+text_margin_vertical)
str: Set text of drag zone when there is no image preview.
422 def set_filename_label(self, text): 423 """str: Set text of filename label on image preview.""" 424 self.filename_label.setText(text) 425 self.filename_label.setVisible(self.show_filename)
str: Set text of filename label on image preview.
427 def display_loading_grayout(self, boolean, text="Loading...", pseudo_load_time=0.2): 428 """Show/hide grayout overlay on label for loading sequences. 429 430 Args: 431 boolean (bool): True to show grayout; False to hide. 432 text (str): The text to show on the grayout. 433 pseudo_load_time (float): The delay (in seconds) to hide the grayout to give users a feeling of action. 434 """ 435 if not boolean: 436 text = "Loading..." 437 self.loading_grayout_label.setText(text) 438 self.loading_grayout_label.setVisible(boolean) 439 if boolean: 440 self.loading_grayout_label.repaint() 441 if not boolean: 442 time.sleep(pseudo_load_time)
Show/hide grayout overlay on label 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.
446class FourDragDropImageLabel(QtWidgets.QFrame): 447 """2x2 panel of drag-and-drop zones for users to arrange images for a SplitView. 448 449 Instantiate without input. 450 451 Allows dragging multiple files (1–4) at once. 452 """ 453 454 will_start_loading = QtCore.pyqtSignal(bool, str) 455 has_stopped_loading = QtCore.pyqtSignal(bool) 456 457 def __init__(self): 458 super().__init__() 459 460 self.image_filetypes = [ 461 ".jpeg", ".jpg", ".jpe", ".jif", ".jfif", ".jfi", ".pjpeg", ".pjp", 462 ".png", ".apng", 463 ".tiff", ".tif", 464 ".bmp", 465 ".gif", 466 ".webp", 467 ".svg", 468 ".ico", ".cur"] 469 470 self.setAcceptDrops(True) 471 472 main_layout = QtWidgets.QGridLayout() 473 474 self.app_main_topleft = DragDropImageLabel(show_filename=True, show_pushbuttons=True, is_main=True, text_default="Drag image(s)") 475 self.app_topright = DragDropImageLabel(show_filename=True, show_pushbuttons=True) 476 self.app_bottomleft = DragDropImageLabel(show_filename=True, show_pushbuttons=True) 477 self.app_bottomright = DragDropImageLabel(show_filename=True, show_pushbuttons=True) 478 479 main_layout.addWidget(self.app_main_topleft, 0, 0) 480 main_layout.addWidget(self.app_topright, 0, 1) 481 main_layout.addWidget(self.app_bottomleft, 1, 0) 482 main_layout.addWidget(self.app_bottomright, 1, 1) 483 484 main_layout.setColumnStretch(0,1) 485 main_layout.setColumnStretch(1,1) 486 main_layout.setRowStretch(0,1) 487 main_layout.setRowStretch(1,1) 488 489 contents_margins_w = 0 490 main_layout.setContentsMargins(contents_margins_w, contents_margins_w, contents_margins_w, contents_margins_w) 491 main_layout.setSpacing(4) 492 493 self.setLayout(main_layout) 494 495 def dragEnterEvent(self, event): 496 """Override dragEnterEvent() to accept multiple (1-4) image files and set stylesheet(s) as hovered.""" 497 urls = self.grab_image_urls_from_mimedata(event.mimeData()) 498 if len(event.mimeData().urls()) >= 1 and len(event.mimeData().urls()) <= 4 and self.grab_image_urls_from_mimedata(event.mimeData()): 499 self.app_main_topleft.image_label_child.set_stylesheet_hovered(True) 500 i = 0 501 if len(urls) >= 2: 502 i += 1 503 self.app_topright.image_label_child.set_stylesheet_hovered(True) 504 505 if len(urls) >= 3: 506 i += 1 507 self.app_bottomright.image_label_child.set_stylesheet_hovered(True) 508 509 if len(urls) >= 4: 510 i += 1 511 self.app_bottomleft.image_label_child.set_stylesheet_hovered(True) 512 event.accept() 513 else: 514 event.ignore() 515 516 def dragMoveEvent(self, event): 517 """Override dragMoveEvent() to accept multiple (1-4) image files.""" 518 if len(event.mimeData().urls()) >= 1 and len(event.mimeData().urls()) <= 4 and self.grab_image_urls_from_mimedata(event.mimeData()): 519 event.accept() 520 else: 521 event.ignore() 522 523 def dragLeaveEvent(self, event): 524 """Override dragLeaveEvent() to set stylesheet(s) as no longer hovered.""" 525 self.app_main_topleft.image_label_child.set_stylesheet_hovered(False) 526 self.app_topright.image_label_child.set_stylesheet_hovered(False) 527 self.app_bottomright.image_label_child.set_stylesheet_hovered(False) 528 self.app_bottomleft.image_label_child.set_stylesheet_hovered(False) 529 530 def dropEvent(self, event): 531 """event: Override dropEvent() to read filepath(s) from 1-4 dragged images and load the preview(s).""" 532 urls = self.grab_image_urls_from_mimedata(event.mimeData()) 533 n = len(urls) 534 n_str = str(n) 535 if n >= 1 and n <= 4 and urls: 536 event.setDropAction(QtCore.Qt.CopyAction) 537 i = 0 538 file_path = urls[i].toLocalFile() 539 540 self.will_start_loading.emit(True, "Loading to creator " + str(i+1) + "/" + n_str + "...") 541 542 loaded = self.app_main_topleft.load_image(file_path) 543 if not loaded: 544 self.app_main_topleft.image_label_child.set_stylesheet_hovered(False) 545 546 if n >= 2: 547 i += 1 548 file_path = urls[i].toLocalFile() 549 self.will_start_loading.emit(True, "Loading to creator " + str(i+1) + "/" + n_str + "...") 550 loaded = self.app_topright.load_image(file_path) 551 if not loaded: 552 self.app_topright.image_label_child.set_stylesheet_hovered(False) 553 554 if n >= 3: 555 i += 1 556 file_path = urls[i].toLocalFile() 557 self.will_start_loading.emit(True, "Loading to creator " + str(i+1) + "/" + n_str + "...") 558 loaded = self.app_bottomright.load_image(file_path) 559 if not loaded: 560 self.app_bottomright.image_label_child.set_stylesheet_hovered(False) 561 562 if n >= 4: 563 i += 1 564 file_path = urls[i].toLocalFile() 565 self.will_start_loading.emit(True, "Loading to creator " + str(i+1) + "/" + n_str + "...") 566 loaded = self.app_bottomleft.load_image(file_path) 567 if not loaded: 568 self.app_bottomleft.image_label_child.set_stylesheet_hovered(False) 569 570 self.has_stopped_loading.emit(False) 571 572 event.accept() 573 else: 574 event.ignore() 575 576 def grab_image_urls_from_mimedata(self, mimedata): 577 """mimeData: Get urls (filepaths) from drag event.""" 578 urls = list() 579 for url in mimedata.urls(): 580 if any([filetype in url.toLocalFile().lower() for filetype in self.image_filetypes]): 581 urls.append(url) 582 return urls
2x2 panel of drag-and-drop zones for users to arrange images for a SplitView.
Instantiate without input.
Allows dragging multiple files (1–4) at once.
495 def dragEnterEvent(self, event): 496 """Override dragEnterEvent() to accept multiple (1-4) image files and set stylesheet(s) as hovered.""" 497 urls = self.grab_image_urls_from_mimedata(event.mimeData()) 498 if len(event.mimeData().urls()) >= 1 and len(event.mimeData().urls()) <= 4 and self.grab_image_urls_from_mimedata(event.mimeData()): 499 self.app_main_topleft.image_label_child.set_stylesheet_hovered(True) 500 i = 0 501 if len(urls) >= 2: 502 i += 1 503 self.app_topright.image_label_child.set_stylesheet_hovered(True) 504 505 if len(urls) >= 3: 506 i += 1 507 self.app_bottomright.image_label_child.set_stylesheet_hovered(True) 508 509 if len(urls) >= 4: 510 i += 1 511 self.app_bottomleft.image_label_child.set_stylesheet_hovered(True) 512 event.accept() 513 else: 514 event.ignore()
Override dragEnterEvent() to accept multiple (1-4) image files and set stylesheet(s) as hovered.
516 def dragMoveEvent(self, event): 517 """Override dragMoveEvent() to accept multiple (1-4) image files.""" 518 if len(event.mimeData().urls()) >= 1 and len(event.mimeData().urls()) <= 4 and self.grab_image_urls_from_mimedata(event.mimeData()): 519 event.accept() 520 else: 521 event.ignore()
Override dragMoveEvent() to accept multiple (1-4) image files.
523 def dragLeaveEvent(self, event): 524 """Override dragLeaveEvent() to set stylesheet(s) as no longer hovered.""" 525 self.app_main_topleft.image_label_child.set_stylesheet_hovered(False) 526 self.app_topright.image_label_child.set_stylesheet_hovered(False) 527 self.app_bottomright.image_label_child.set_stylesheet_hovered(False) 528 self.app_bottomleft.image_label_child.set_stylesheet_hovered(False)
Override dragLeaveEvent() to set stylesheet(s) as no longer hovered.
530 def dropEvent(self, event): 531 """event: Override dropEvent() to read filepath(s) from 1-4 dragged images and load the preview(s).""" 532 urls = self.grab_image_urls_from_mimedata(event.mimeData()) 533 n = len(urls) 534 n_str = str(n) 535 if n >= 1 and n <= 4 and urls: 536 event.setDropAction(QtCore.Qt.CopyAction) 537 i = 0 538 file_path = urls[i].toLocalFile() 539 540 self.will_start_loading.emit(True, "Loading to creator " + str(i+1) + "/" + n_str + "...") 541 542 loaded = self.app_main_topleft.load_image(file_path) 543 if not loaded: 544 self.app_main_topleft.image_label_child.set_stylesheet_hovered(False) 545 546 if n >= 2: 547 i += 1 548 file_path = urls[i].toLocalFile() 549 self.will_start_loading.emit(True, "Loading to creator " + str(i+1) + "/" + n_str + "...") 550 loaded = self.app_topright.load_image(file_path) 551 if not loaded: 552 self.app_topright.image_label_child.set_stylesheet_hovered(False) 553 554 if n >= 3: 555 i += 1 556 file_path = urls[i].toLocalFile() 557 self.will_start_loading.emit(True, "Loading to creator " + str(i+1) + "/" + n_str + "...") 558 loaded = self.app_bottomright.load_image(file_path) 559 if not loaded: 560 self.app_bottomright.image_label_child.set_stylesheet_hovered(False) 561 562 if n >= 4: 563 i += 1 564 file_path = urls[i].toLocalFile() 565 self.will_start_loading.emit(True, "Loading to creator " + str(i+1) + "/" + n_str + "...") 566 loaded = self.app_bottomleft.load_image(file_path) 567 if not loaded: 568 self.app_bottomleft.image_label_child.set_stylesheet_hovered(False) 569 570 self.has_stopped_loading.emit(False) 571 572 event.accept() 573 else: 574 event.ignore()
event: Override dropEvent() to read filepath(s) from 1-4 dragged images and load the preview(s).
576 def grab_image_urls_from_mimedata(self, mimedata): 577 """mimeData: Get urls (filepaths) from drag event.""" 578 urls = list() 579 for url in mimedata.urls(): 580 if any([filetype in url.toLocalFile().lower() for filetype in self.image_filetypes]): 581 urls.append(url) 582 return urls
mimeData: Get urls (filepaths) from drag event.
586def main(): 587 """Demo the drag-and-drop function in the 2x2 panel.""" 588 589 app = QtWidgets.QApplication(sys.argv) 590 591 demo = FourDragDropImageLabel() 592 demo.show() 593 594 sys.exit(app.exec_())
Demo the drag-and-drop function in the 2x2 panel.