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", 204 ".tiff", ".tif", 205 ".bmp", 206 ".webp", 207 ".ico", ".cur"] 208 209 self.setAcceptDrops(True) 210 211 main_layout = QtWidgets.QGridLayout() 212 213 if is_main: 214 self.image_label_child = ImageLabelMain() 215 else: 216 self.image_label_child = ImageLabel() 217 218 self.image_label_child.became_occupied.connect(self.became_occupied) 219 220 self.set_text(self.text_default) 221 222 main_layout.addWidget(self.image_label_child, 0, 0) 223 224 self.filename_label = FilenameLabel("No filename available", remove_path=True) 225 main_layout.addWidget(self.filename_label, 0, 0, QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft) 226 self.filename_label.setVisible(False) 227 228 if self.show_pushbuttons is True: 229 self.buttons_layout = QtWidgets.QGridLayout() 230 self.clear_layout = QtWidgets.QGridLayout() 231 232 self.open_pushbutton = QtWidgets.QToolButton() 233 self.open_pushbutton.setText("Select image...") 234 self.open_pushbutton.setToolTip("Select image from file and add here in sliding overlay creator...") 235 self.open_pushbutton.setStyleSheet(""" 236 QToolButton { 237 font-size: 9pt; 238 } 239 """) 240 241 self.clear_pushbutton = QtWidgets.QToolButton() 242 self.clear_pushbutton.setText("×") 243 self.clear_pushbutton.setToolTip("Clear image") 244 self.clear_pushbutton.setStyleSheet(""" 245 QToolButton { 246 font-size: 9pt; 247 } 248 """) 249 250 self.open_pushbutton.clicked.connect(self.was_clicked_open_pushbutton) 251 self.clear_pushbutton.clicked.connect(self.was_clicked_clear_pushbutton) 252 253 w = 8 254 255 self.buttons_layout.addWidget(self.open_pushbutton, 0, 0) 256 self.buttons_layout.setContentsMargins(w,w,w,w) 257 self.buttons_layout.setSpacing(w) 258 259 self.clear_layout.addWidget(self.clear_pushbutton, 0, 0) 260 self.clear_layout.setContentsMargins(w,w,w,w) 261 self.clear_layout.setSpacing(w) 262 263 main_layout.addLayout(self.buttons_layout, 0, 0, QtCore.Qt.AlignBottom) 264 main_layout.addLayout(self.clear_layout, 0, 0, QtCore.Qt.AlignTop|QtCore.Qt.AlignRight) 265 266 self.clear_pushbutton.setEnabled(False) 267 self.clear_pushbutton.setVisible(False) 268 269 270 self.loading_grayout_label = QtWidgets.QLabel("Loading...") 271 self.loading_grayout_label.setAlignment(QtCore.Qt.AlignCenter | QtCore.Qt.AlignVCenter) 272 self.loading_grayout_label.setVisible(False) 273 self.loading_grayout_label.setStyleSheet(""" 274 QLabel { 275 color: white; 276 font-size: 7.5pt; 277 background-color: rgba(0,0,0,223); 278 } 279 """) 280 281 main_layout.addWidget(self.loading_grayout_label, 0, 0) 282 283 main_layout.setContentsMargins(2, 2, 2, 2) 284 main_layout.setSpacing(0) 285 286 self.setLayout(main_layout) 287 288 def set_addable(self, boolean): 289 """bool: Set whether an imaged may be added (dragged and dropped) into the widget.""" 290 self.image_label_child.IS_ADDABLE = boolean 291 self.image_label_child.setEnabled(boolean) 292 self.open_pushbutton.setEnabled(boolean) 293 self.setAcceptDrops(boolean) 294 self.filename_label.setEnabled(boolean) 295 self.image_label_child.set_stylesheet_addable(boolean) 296 297 298 def dragEnterEvent(self, event): 299 """event: Override dragEnterEvent() to set stylesheet as hovered and read filepath from a dragged image, but reject multiple files.""" 300 if len(event.mimeData().urls()) is 1 and self.grab_image_urls_from_mimedata(event.mimeData()): 301 self.image_label_child.set_stylesheet_hovered(True) 302 event.accept() 303 else: 304 event.ignore() 305 306 def dragMoveEvent(self, event): 307 """event: Override dragMoveEvent() to reject multiple files.""" 308 if len(event.mimeData().urls()) is 1 and self.grab_image_urls_from_mimedata(event.mimeData()): 309 event.accept() 310 else: 311 event.ignore() 312 313 def dragLeaveEvent(self, event): 314 """event: Override dragLeaveEvent() to set stylesheet as not hovered.""" 315 self.image_label_child.set_stylesheet_hovered(False) 316 317 def dropEvent(self, event): 318 """event: Override dropEvent() to read filepath from a dragged image and load image preview.""" 319 urls = self.grab_image_urls_from_mimedata(event.mimeData()) 320 if len(urls) is 1 and urls: 321 event.setDropAction(QtCore.Qt.CopyAction) 322 file_path = urls[0].toLocalFile() 323 loaded = self.load_image(file_path) 324 if loaded: 325 event.accept() 326 else: 327 event.ignore() 328 self.image_label_child.set_stylesheet_hovered(False) 329 else: 330 event.ignore() 331 332 def grab_image_urls_from_mimedata(self, mimedata): 333 """mimeData: Get urls (filepaths) from drag event.""" 334 urls = list() 335 for url in mimedata.urls(): 336 if any([filetype in url.toLocalFile().lower() for filetype in self.image_filetypes]): 337 urls.append(url) 338 return urls 339 340 def mouseDoubleClickEvent(self, event): 341 """event: Override mouseDoubleClickEvent() to trigger dialog window to open image.""" 342 self.open_image_via_dialog() 343 344 def was_clicked_open_pushbutton(self): 345 """Trigger dialog window to open image when button to select image is clicked.""" 346 self.open_image_via_dialog() 347 348 def was_clicked_clear_pushbutton(self): 349 """Clear image preview when clear button is clicked.""" 350 self.clear_image() 351 352 def set_image(self, pixmap): 353 """QPixmap: Scale and set preview of image; set status of clear button.""" 354 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)) 355 self.clear_pushbutton.setEnabled(True) 356 self.clear_pushbutton.setVisible(True) 357 358 def load_image(self, file_path): 359 """str: Load image from filepath with loading grayout; set filename text. 360 361 Returns: 362 loaded (bool): True if image successfully loaded; False if not.""" 363 loading_text = "Loading..." 364 if self.show_filepath_while_loading: 365 loading_text = loading_text.replace("...", " '" + file_path.split("/")[-1] + "'...") 366 self.display_loading_grayout(True, loading_text) 367 pixmap = QtGui.QPixmap(file_path) 368 if pixmap.depth() is 0: 369 self.display_loading_grayout(False) 370 return False 371 372 angle = get_exif_rotation_angle(file_path) 373 if angle: 374 pixmap = pixmap.transformed(QtGui.QTransform().rotate(angle)) 375 376 self.set_image(pixmap) 377 self.set_filename_label(file_path) 378 self.file_path = file_path 379 self.display_loading_grayout(False) 380 return True 381 382 def open_image_via_dialog(self): 383 """Open dialog window to select and load image from file.""" 384 file_dialog = QtWidgets.QFileDialog(self) 385 386 file_dialog.setNameFilters([ 387 "All supported image files (*.jpeg *.jpg *.png *.tiff *.tif *.gif *.bmp)", 388 "All files (*)", 389 "JPEG image files (*.jpeg *.jpg)", 390 "PNG image files (*.png)", 391 "TIFF image files (*.tiff *.tif)", 392 "BMP (*.bmp)"]) 393 file_dialog.setFileMode(QtWidgets.QFileDialog.ExistingFile) 394 395 if not file_dialog.exec_(): 396 return 397 398 file_path = file_dialog.selectedFiles()[0] 399 400 self.load_image(file_path) 401 402 def clear_image(self): 403 """Clear image preview and filepath; set status of clear button; set text of drag zone.""" 404 if self.image_label_child.pixmap(): 405 self.image_label_child.clear() 406 407 self.set_text(self.text_default) 408 self.file_path = None 409 self.clear_pushbutton.setEnabled(False) 410 self.clear_pushbutton.setVisible(False) 411 self.filename_label.setText("No filename available") 412 self.filename_label.setVisible(False) 413 414 def set_text(self, text): 415 """str: Set text of drag zone when there is no image preview.""" 416 text_margin_vertical = "\n\n\n" 417 self.image_label_child.setText(text_margin_vertical+text+text_margin_vertical) 418 419 def set_filename_label(self, text): 420 """str: Set text of filename label on image preview.""" 421 self.filename_label.setText(text) 422 self.filename_label.setVisible(self.show_filename) 423 424 def display_loading_grayout(self, boolean, text="Loading...", pseudo_load_time=0.2): 425 """Show/hide grayout overlay on label for loading sequences. 426 427 Args: 428 boolean (bool): True to show grayout; False to hide. 429 text (str): The text to show on the grayout. 430 pseudo_load_time (float): The delay (in seconds) to hide the grayout to give users a feeling of action. 431 """ 432 if not boolean: 433 text = "Loading..." 434 self.loading_grayout_label.setText(text) 435 self.loading_grayout_label.setVisible(boolean) 436 if boolean: 437 self.loading_grayout_label.repaint() 438 if not boolean: 439 time.sleep(pseudo_load_time) 440 441 442 443class FourDragDropImageLabel(QtWidgets.QFrame): 444 """2x2 panel of drag-and-drop zones for users to arrange images for a SplitView. 445 446 Instantiate without input. 447 448 Allows dragging multiple files (1–4) at once. 449 """ 450 451 will_start_loading = QtCore.pyqtSignal(bool, str) 452 has_stopped_loading = QtCore.pyqtSignal(bool) 453 454 def __init__(self): 455 super().__init__() 456 457 self.image_filetypes = [ 458 ".jpeg", ".jpg", ".jpe", ".jif", ".jfif", ".jfi", ".pjpeg", ".pjp", 459 ".png", 460 ".tiff", ".tif", 461 ".bmp", 462 ".webp", 463 ".ico", ".cur"] 464 465 self.setAcceptDrops(True) 466 467 main_layout = QtWidgets.QGridLayout() 468 469 self.app_main_topleft = DragDropImageLabel(show_filename=True, show_pushbuttons=True, is_main=True, text_default="Drag image(s)") 470 self.app_topright = DragDropImageLabel(show_filename=True, show_pushbuttons=True) 471 self.app_bottomleft = DragDropImageLabel(show_filename=True, show_pushbuttons=True) 472 self.app_bottomright = DragDropImageLabel(show_filename=True, show_pushbuttons=True) 473 474 main_layout.addWidget(self.app_main_topleft, 0, 0) 475 main_layout.addWidget(self.app_topright, 0, 1) 476 main_layout.addWidget(self.app_bottomleft, 1, 0) 477 main_layout.addWidget(self.app_bottomright, 1, 1) 478 479 main_layout.setColumnStretch(0,1) 480 main_layout.setColumnStretch(1,1) 481 main_layout.setRowStretch(0,1) 482 main_layout.setRowStretch(1,1) 483 484 contents_margins_w = 0 485 main_layout.setContentsMargins(contents_margins_w, contents_margins_w, contents_margins_w, contents_margins_w) 486 main_layout.setSpacing(4) 487 488 self.setLayout(main_layout) 489 490 def dragEnterEvent(self, event): 491 """Override dragEnterEvent() to accept multiple (1-4) image files and set stylesheet(s) as hovered.""" 492 urls = self.grab_image_urls_from_mimedata(event.mimeData()) 493 if len(event.mimeData().urls()) >= 1 and len(event.mimeData().urls()) <= 4 and self.grab_image_urls_from_mimedata(event.mimeData()): 494 self.app_main_topleft.image_label_child.set_stylesheet_hovered(True) 495 i = 0 496 if len(urls) >= 2: 497 i += 1 498 self.app_topright.image_label_child.set_stylesheet_hovered(True) 499 500 if len(urls) >= 3: 501 i += 1 502 self.app_bottomright.image_label_child.set_stylesheet_hovered(True) 503 504 if len(urls) >= 4: 505 i += 1 506 self.app_bottomleft.image_label_child.set_stylesheet_hovered(True) 507 event.accept() 508 else: 509 event.ignore() 510 511 def dragMoveEvent(self, event): 512 """Override dragMoveEvent() to accept multiple (1-4) image files.""" 513 if len(event.mimeData().urls()) >= 1 and len(event.mimeData().urls()) <= 4 and self.grab_image_urls_from_mimedata(event.mimeData()): 514 event.accept() 515 else: 516 event.ignore() 517 518 def dragLeaveEvent(self, event): 519 """Override dragLeaveEvent() to set stylesheet(s) as no longer hovered.""" 520 self.app_main_topleft.image_label_child.set_stylesheet_hovered(False) 521 self.app_topright.image_label_child.set_stylesheet_hovered(False) 522 self.app_bottomright.image_label_child.set_stylesheet_hovered(False) 523 self.app_bottomleft.image_label_child.set_stylesheet_hovered(False) 524 525 def dropEvent(self, event): 526 """event: Override dropEvent() to read filepath(s) from 1-4 dragged images and load the preview(s).""" 527 urls = self.grab_image_urls_from_mimedata(event.mimeData()) 528 n = len(urls) 529 n_str = str(n) 530 if n >= 1 and n <= 4 and urls: 531 event.setDropAction(QtCore.Qt.CopyAction) 532 i = 0 533 file_path = urls[i].toLocalFile() 534 535 self.will_start_loading.emit(True, "Loading to creator " + str(i+1) + "/" + n_str + "...") 536 537 loaded = self.app_main_topleft.load_image(file_path) 538 if not loaded: 539 self.app_main_topleft.image_label_child.set_stylesheet_hovered(False) 540 541 if n >= 2: 542 i += 1 543 file_path = urls[i].toLocalFile() 544 self.will_start_loading.emit(True, "Loading to creator " + str(i+1) + "/" + n_str + "...") 545 loaded = self.app_topright.load_image(file_path) 546 if not loaded: 547 self.app_topright.image_label_child.set_stylesheet_hovered(False) 548 549 if n >= 3: 550 i += 1 551 file_path = urls[i].toLocalFile() 552 self.will_start_loading.emit(True, "Loading to creator " + str(i+1) + "/" + n_str + "...") 553 loaded = self.app_bottomright.load_image(file_path) 554 if not loaded: 555 self.app_bottomright.image_label_child.set_stylesheet_hovered(False) 556 557 if n >= 4: 558 i += 1 559 file_path = urls[i].toLocalFile() 560 self.will_start_loading.emit(True, "Loading to creator " + str(i+1) + "/" + n_str + "...") 561 loaded = self.app_bottomleft.load_image(file_path) 562 if not loaded: 563 self.app_bottomleft.image_label_child.set_stylesheet_hovered(False) 564 565 self.has_stopped_loading.emit(False) 566 567 event.accept() 568 else: 569 event.ignore() 570 571 def grab_image_urls_from_mimedata(self, mimedata): 572 """mimeData: Get urls (filepaths) from drag event.""" 573 urls = list() 574 for url in mimedata.urls(): 575 if any([filetype in url.toLocalFile().lower() for filetype in self.image_filetypes]): 576 urls.append(url) 577 return urls 578 579 580 581def main(): 582 """Demo the drag-and-drop function in the 2x2 panel.""" 583 584 app = QtWidgets.QApplication(sys.argv) 585 586 demo = FourDragDropImageLabel() 587 demo.show() 588 589 sys.exit(app.exec_()) 590 591 592 593if __name__ == '__main__': 594 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", 205 ".tiff", ".tif", 206 ".bmp", 207 ".webp", 208 ".ico", ".cur"] 209 210 self.setAcceptDrops(True) 211 212 main_layout = QtWidgets.QGridLayout() 213 214 if is_main: 215 self.image_label_child = ImageLabelMain() 216 else: 217 self.image_label_child = ImageLabel() 218 219 self.image_label_child.became_occupied.connect(self.became_occupied) 220 221 self.set_text(self.text_default) 222 223 main_layout.addWidget(self.image_label_child, 0, 0) 224 225 self.filename_label = FilenameLabel("No filename available", remove_path=True) 226 main_layout.addWidget(self.filename_label, 0, 0, QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft) 227 self.filename_label.setVisible(False) 228 229 if self.show_pushbuttons is True: 230 self.buttons_layout = QtWidgets.QGridLayout() 231 self.clear_layout = QtWidgets.QGridLayout() 232 233 self.open_pushbutton = QtWidgets.QToolButton() 234 self.open_pushbutton.setText("Select image...") 235 self.open_pushbutton.setToolTip("Select image from file and add here in sliding overlay creator...") 236 self.open_pushbutton.setStyleSheet(""" 237 QToolButton { 238 font-size: 9pt; 239 } 240 """) 241 242 self.clear_pushbutton = QtWidgets.QToolButton() 243 self.clear_pushbutton.setText("×") 244 self.clear_pushbutton.setToolTip("Clear image") 245 self.clear_pushbutton.setStyleSheet(""" 246 QToolButton { 247 font-size: 9pt; 248 } 249 """) 250 251 self.open_pushbutton.clicked.connect(self.was_clicked_open_pushbutton) 252 self.clear_pushbutton.clicked.connect(self.was_clicked_clear_pushbutton) 253 254 w = 8 255 256 self.buttons_layout.addWidget(self.open_pushbutton, 0, 0) 257 self.buttons_layout.setContentsMargins(w,w,w,w) 258 self.buttons_layout.setSpacing(w) 259 260 self.clear_layout.addWidget(self.clear_pushbutton, 0, 0) 261 self.clear_layout.setContentsMargins(w,w,w,w) 262 self.clear_layout.setSpacing(w) 263 264 main_layout.addLayout(self.buttons_layout, 0, 0, QtCore.Qt.AlignBottom) 265 main_layout.addLayout(self.clear_layout, 0, 0, QtCore.Qt.AlignTop|QtCore.Qt.AlignRight) 266 267 self.clear_pushbutton.setEnabled(False) 268 self.clear_pushbutton.setVisible(False) 269 270 271 self.loading_grayout_label = QtWidgets.QLabel("Loading...") 272 self.loading_grayout_label.setAlignment(QtCore.Qt.AlignCenter | QtCore.Qt.AlignVCenter) 273 self.loading_grayout_label.setVisible(False) 274 self.loading_grayout_label.setStyleSheet(""" 275 QLabel { 276 color: white; 277 font-size: 7.5pt; 278 background-color: rgba(0,0,0,223); 279 } 280 """) 281 282 main_layout.addWidget(self.loading_grayout_label, 0, 0) 283 284 main_layout.setContentsMargins(2, 2, 2, 2) 285 main_layout.setSpacing(0) 286 287 self.setLayout(main_layout) 288 289 def set_addable(self, boolean): 290 """bool: Set whether an imaged may be added (dragged and dropped) into the widget.""" 291 self.image_label_child.IS_ADDABLE = boolean 292 self.image_label_child.setEnabled(boolean) 293 self.open_pushbutton.setEnabled(boolean) 294 self.setAcceptDrops(boolean) 295 self.filename_label.setEnabled(boolean) 296 self.image_label_child.set_stylesheet_addable(boolean) 297 298 299 def dragEnterEvent(self, event): 300 """event: Override dragEnterEvent() to set stylesheet as hovered and read filepath from a dragged image, but reject multiple files.""" 301 if len(event.mimeData().urls()) is 1 and self.grab_image_urls_from_mimedata(event.mimeData()): 302 self.image_label_child.set_stylesheet_hovered(True) 303 event.accept() 304 else: 305 event.ignore() 306 307 def dragMoveEvent(self, event): 308 """event: Override dragMoveEvent() to reject multiple files.""" 309 if len(event.mimeData().urls()) is 1 and self.grab_image_urls_from_mimedata(event.mimeData()): 310 event.accept() 311 else: 312 event.ignore() 313 314 def dragLeaveEvent(self, event): 315 """event: Override dragLeaveEvent() to set stylesheet as not hovered.""" 316 self.image_label_child.set_stylesheet_hovered(False) 317 318 def dropEvent(self, event): 319 """event: Override dropEvent() to read filepath from a dragged image and load image preview.""" 320 urls = self.grab_image_urls_from_mimedata(event.mimeData()) 321 if len(urls) is 1 and urls: 322 event.setDropAction(QtCore.Qt.CopyAction) 323 file_path = urls[0].toLocalFile() 324 loaded = self.load_image(file_path) 325 if loaded: 326 event.accept() 327 else: 328 event.ignore() 329 self.image_label_child.set_stylesheet_hovered(False) 330 else: 331 event.ignore() 332 333 def grab_image_urls_from_mimedata(self, mimedata): 334 """mimeData: Get urls (filepaths) from drag event.""" 335 urls = list() 336 for url in mimedata.urls(): 337 if any([filetype in url.toLocalFile().lower() for filetype in self.image_filetypes]): 338 urls.append(url) 339 return urls 340 341 def mouseDoubleClickEvent(self, event): 342 """event: Override mouseDoubleClickEvent() to trigger dialog window to open image.""" 343 self.open_image_via_dialog() 344 345 def was_clicked_open_pushbutton(self): 346 """Trigger dialog window to open image when button to select image is clicked.""" 347 self.open_image_via_dialog() 348 349 def was_clicked_clear_pushbutton(self): 350 """Clear image preview when clear button is clicked.""" 351 self.clear_image() 352 353 def set_image(self, pixmap): 354 """QPixmap: Scale and set preview of image; set status of clear button.""" 355 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)) 356 self.clear_pushbutton.setEnabled(True) 357 self.clear_pushbutton.setVisible(True) 358 359 def load_image(self, file_path): 360 """str: Load image from filepath with loading grayout; set filename text. 361 362 Returns: 363 loaded (bool): True if image successfully loaded; False if not.""" 364 loading_text = "Loading..." 365 if self.show_filepath_while_loading: 366 loading_text = loading_text.replace("...", " '" + file_path.split("/")[-1] + "'...") 367 self.display_loading_grayout(True, loading_text) 368 pixmap = QtGui.QPixmap(file_path) 369 if pixmap.depth() is 0: 370 self.display_loading_grayout(False) 371 return False 372 373 angle = get_exif_rotation_angle(file_path) 374 if angle: 375 pixmap = pixmap.transformed(QtGui.QTransform().rotate(angle)) 376 377 self.set_image(pixmap) 378 self.set_filename_label(file_path) 379 self.file_path = file_path 380 self.display_loading_grayout(False) 381 return True 382 383 def open_image_via_dialog(self): 384 """Open dialog window to select and load image from file.""" 385 file_dialog = QtWidgets.QFileDialog(self) 386 387 file_dialog.setNameFilters([ 388 "All supported image files (*.jpeg *.jpg *.png *.tiff *.tif *.gif *.bmp)", 389 "All files (*)", 390 "JPEG image files (*.jpeg *.jpg)", 391 "PNG image files (*.png)", 392 "TIFF image files (*.tiff *.tif)", 393 "BMP (*.bmp)"]) 394 file_dialog.setFileMode(QtWidgets.QFileDialog.ExistingFile) 395 396 if not file_dialog.exec_(): 397 return 398 399 file_path = file_dialog.selectedFiles()[0] 400 401 self.load_image(file_path) 402 403 def clear_image(self): 404 """Clear image preview and filepath; set status of clear button; set text of drag zone.""" 405 if self.image_label_child.pixmap(): 406 self.image_label_child.clear() 407 408 self.set_text(self.text_default) 409 self.file_path = None 410 self.clear_pushbutton.setEnabled(False) 411 self.clear_pushbutton.setVisible(False) 412 self.filename_label.setText("No filename available") 413 self.filename_label.setVisible(False) 414 415 def set_text(self, text): 416 """str: Set text of drag zone when there is no image preview.""" 417 text_margin_vertical = "\n\n\n" 418 self.image_label_child.setText(text_margin_vertical+text+text_margin_vertical) 419 420 def set_filename_label(self, text): 421 """str: Set text of filename label on image preview.""" 422 self.filename_label.setText(text) 423 self.filename_label.setVisible(self.show_filename) 424 425 def display_loading_grayout(self, boolean, text="Loading...", pseudo_load_time=0.2): 426 """Show/hide grayout overlay on label for loading sequences. 427 428 Args: 429 boolean (bool): True to show grayout; False to hide. 430 text (str): The text to show on the grayout. 431 pseudo_load_time (float): The delay (in seconds) to hide the grayout to give users a feeling of action. 432 """ 433 if not boolean: 434 text = "Loading..." 435 self.loading_grayout_label.setText(text) 436 self.loading_grayout_label.setVisible(boolean) 437 if boolean: 438 self.loading_grayout_label.repaint() 439 if not boolean: 440 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.
289 def set_addable(self, boolean): 290 """bool: Set whether an imaged may be added (dragged and dropped) into the widget.""" 291 self.image_label_child.IS_ADDABLE = boolean 292 self.image_label_child.setEnabled(boolean) 293 self.open_pushbutton.setEnabled(boolean) 294 self.setAcceptDrops(boolean) 295 self.filename_label.setEnabled(boolean) 296 self.image_label_child.set_stylesheet_addable(boolean)
bool: Set whether an imaged may be added (dragged and dropped) into the widget.
299 def dragEnterEvent(self, event): 300 """event: Override dragEnterEvent() to set stylesheet as hovered and read filepath from a dragged image, but reject multiple files.""" 301 if len(event.mimeData().urls()) is 1 and self.grab_image_urls_from_mimedata(event.mimeData()): 302 self.image_label_child.set_stylesheet_hovered(True) 303 event.accept() 304 else: 305 event.ignore()
event: Override dragEnterEvent() to set stylesheet as hovered and read filepath from a dragged image, but reject multiple files.
307 def dragMoveEvent(self, event): 308 """event: Override dragMoveEvent() to reject multiple files.""" 309 if len(event.mimeData().urls()) is 1 and self.grab_image_urls_from_mimedata(event.mimeData()): 310 event.accept() 311 else: 312 event.ignore()
event: Override dragMoveEvent() to reject multiple files.
314 def dragLeaveEvent(self, event): 315 """event: Override dragLeaveEvent() to set stylesheet as not hovered.""" 316 self.image_label_child.set_stylesheet_hovered(False)
event: Override dragLeaveEvent() to set stylesheet as not hovered.
318 def dropEvent(self, event): 319 """event: Override dropEvent() to read filepath from a dragged image and load image preview.""" 320 urls = self.grab_image_urls_from_mimedata(event.mimeData()) 321 if len(urls) is 1 and urls: 322 event.setDropAction(QtCore.Qt.CopyAction) 323 file_path = urls[0].toLocalFile() 324 loaded = self.load_image(file_path) 325 if loaded: 326 event.accept() 327 else: 328 event.ignore() 329 self.image_label_child.set_stylesheet_hovered(False) 330 else: 331 event.ignore()
event: Override dropEvent() to read filepath from a dragged image and load image preview.
333 def grab_image_urls_from_mimedata(self, mimedata): 334 """mimeData: Get urls (filepaths) from drag event.""" 335 urls = list() 336 for url in mimedata.urls(): 337 if any([filetype in url.toLocalFile().lower() for filetype in self.image_filetypes]): 338 urls.append(url) 339 return urls
mimeData: Get urls (filepaths) from drag event.
341 def mouseDoubleClickEvent(self, event): 342 """event: Override mouseDoubleClickEvent() to trigger dialog window to open image.""" 343 self.open_image_via_dialog()
event: Override mouseDoubleClickEvent() to trigger dialog window to open image.
353 def set_image(self, pixmap): 354 """QPixmap: Scale and set preview of image; set status of clear button.""" 355 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)) 356 self.clear_pushbutton.setEnabled(True) 357 self.clear_pushbutton.setVisible(True)
QPixmap: Scale and set preview of image; set status of clear button.
359 def load_image(self, file_path): 360 """str: Load image from filepath with loading grayout; set filename text. 361 362 Returns: 363 loaded (bool): True if image successfully loaded; False if not.""" 364 loading_text = "Loading..." 365 if self.show_filepath_while_loading: 366 loading_text = loading_text.replace("...", " '" + file_path.split("/")[-1] + "'...") 367 self.display_loading_grayout(True, loading_text) 368 pixmap = QtGui.QPixmap(file_path) 369 if pixmap.depth() is 0: 370 self.display_loading_grayout(False) 371 return False 372 373 angle = get_exif_rotation_angle(file_path) 374 if angle: 375 pixmap = pixmap.transformed(QtGui.QTransform().rotate(angle)) 376 377 self.set_image(pixmap) 378 self.set_filename_label(file_path) 379 self.file_path = file_path 380 self.display_loading_grayout(False) 381 return True
str: Load image from filepath with loading grayout; set filename text.
Returns:
- loaded (bool): True if image successfully loaded; False if not.
383 def open_image_via_dialog(self): 384 """Open dialog window to select and load image from file.""" 385 file_dialog = QtWidgets.QFileDialog(self) 386 387 file_dialog.setNameFilters([ 388 "All supported image files (*.jpeg *.jpg *.png *.tiff *.tif *.gif *.bmp)", 389 "All files (*)", 390 "JPEG image files (*.jpeg *.jpg)", 391 "PNG image files (*.png)", 392 "TIFF image files (*.tiff *.tif)", 393 "BMP (*.bmp)"]) 394 file_dialog.setFileMode(QtWidgets.QFileDialog.ExistingFile) 395 396 if not file_dialog.exec_(): 397 return 398 399 file_path = file_dialog.selectedFiles()[0] 400 401 self.load_image(file_path)
Open dialog window to select and load image from file.
403 def clear_image(self): 404 """Clear image preview and filepath; set status of clear button; set text of drag zone.""" 405 if self.image_label_child.pixmap(): 406 self.image_label_child.clear() 407 408 self.set_text(self.text_default) 409 self.file_path = None 410 self.clear_pushbutton.setEnabled(False) 411 self.clear_pushbutton.setVisible(False) 412 self.filename_label.setText("No filename available") 413 self.filename_label.setVisible(False)
Clear image preview and filepath; set status of clear button; set text of drag zone.
415 def set_text(self, text): 416 """str: Set text of drag zone when there is no image preview.""" 417 text_margin_vertical = "\n\n\n" 418 self.image_label_child.setText(text_margin_vertical+text+text_margin_vertical)
str: Set text of drag zone when there is no image preview.
420 def set_filename_label(self, text): 421 """str: Set text of filename label on image preview.""" 422 self.filename_label.setText(text) 423 self.filename_label.setVisible(self.show_filename)
str: Set text of filename label on image preview.
425 def display_loading_grayout(self, boolean, text="Loading...", pseudo_load_time=0.2): 426 """Show/hide grayout overlay on label for loading sequences. 427 428 Args: 429 boolean (bool): True to show grayout; False to hide. 430 text (str): The text to show on the grayout. 431 pseudo_load_time (float): The delay (in seconds) to hide the grayout to give users a feeling of action. 432 """ 433 if not boolean: 434 text = "Loading..." 435 self.loading_grayout_label.setText(text) 436 self.loading_grayout_label.setVisible(boolean) 437 if boolean: 438 self.loading_grayout_label.repaint() 439 if not boolean: 440 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.
444class FourDragDropImageLabel(QtWidgets.QFrame): 445 """2x2 panel of drag-and-drop zones for users to arrange images for a SplitView. 446 447 Instantiate without input. 448 449 Allows dragging multiple files (1–4) at once. 450 """ 451 452 will_start_loading = QtCore.pyqtSignal(bool, str) 453 has_stopped_loading = QtCore.pyqtSignal(bool) 454 455 def __init__(self): 456 super().__init__() 457 458 self.image_filetypes = [ 459 ".jpeg", ".jpg", ".jpe", ".jif", ".jfif", ".jfi", ".pjpeg", ".pjp", 460 ".png", 461 ".tiff", ".tif", 462 ".bmp", 463 ".webp", 464 ".ico", ".cur"] 465 466 self.setAcceptDrops(True) 467 468 main_layout = QtWidgets.QGridLayout() 469 470 self.app_main_topleft = DragDropImageLabel(show_filename=True, show_pushbuttons=True, is_main=True, text_default="Drag image(s)") 471 self.app_topright = DragDropImageLabel(show_filename=True, show_pushbuttons=True) 472 self.app_bottomleft = DragDropImageLabel(show_filename=True, show_pushbuttons=True) 473 self.app_bottomright = DragDropImageLabel(show_filename=True, show_pushbuttons=True) 474 475 main_layout.addWidget(self.app_main_topleft, 0, 0) 476 main_layout.addWidget(self.app_topright, 0, 1) 477 main_layout.addWidget(self.app_bottomleft, 1, 0) 478 main_layout.addWidget(self.app_bottomright, 1, 1) 479 480 main_layout.setColumnStretch(0,1) 481 main_layout.setColumnStretch(1,1) 482 main_layout.setRowStretch(0,1) 483 main_layout.setRowStretch(1,1) 484 485 contents_margins_w = 0 486 main_layout.setContentsMargins(contents_margins_w, contents_margins_w, contents_margins_w, contents_margins_w) 487 main_layout.setSpacing(4) 488 489 self.setLayout(main_layout) 490 491 def dragEnterEvent(self, event): 492 """Override dragEnterEvent() to accept multiple (1-4) image files and set stylesheet(s) as hovered.""" 493 urls = self.grab_image_urls_from_mimedata(event.mimeData()) 494 if len(event.mimeData().urls()) >= 1 and len(event.mimeData().urls()) <= 4 and self.grab_image_urls_from_mimedata(event.mimeData()): 495 self.app_main_topleft.image_label_child.set_stylesheet_hovered(True) 496 i = 0 497 if len(urls) >= 2: 498 i += 1 499 self.app_topright.image_label_child.set_stylesheet_hovered(True) 500 501 if len(urls) >= 3: 502 i += 1 503 self.app_bottomright.image_label_child.set_stylesheet_hovered(True) 504 505 if len(urls) >= 4: 506 i += 1 507 self.app_bottomleft.image_label_child.set_stylesheet_hovered(True) 508 event.accept() 509 else: 510 event.ignore() 511 512 def dragMoveEvent(self, event): 513 """Override dragMoveEvent() to accept multiple (1-4) image files.""" 514 if len(event.mimeData().urls()) >= 1 and len(event.mimeData().urls()) <= 4 and self.grab_image_urls_from_mimedata(event.mimeData()): 515 event.accept() 516 else: 517 event.ignore() 518 519 def dragLeaveEvent(self, event): 520 """Override dragLeaveEvent() to set stylesheet(s) as no longer hovered.""" 521 self.app_main_topleft.image_label_child.set_stylesheet_hovered(False) 522 self.app_topright.image_label_child.set_stylesheet_hovered(False) 523 self.app_bottomright.image_label_child.set_stylesheet_hovered(False) 524 self.app_bottomleft.image_label_child.set_stylesheet_hovered(False) 525 526 def dropEvent(self, event): 527 """event: Override dropEvent() to read filepath(s) from 1-4 dragged images and load the preview(s).""" 528 urls = self.grab_image_urls_from_mimedata(event.mimeData()) 529 n = len(urls) 530 n_str = str(n) 531 if n >= 1 and n <= 4 and urls: 532 event.setDropAction(QtCore.Qt.CopyAction) 533 i = 0 534 file_path = urls[i].toLocalFile() 535 536 self.will_start_loading.emit(True, "Loading to creator " + str(i+1) + "/" + n_str + "...") 537 538 loaded = self.app_main_topleft.load_image(file_path) 539 if not loaded: 540 self.app_main_topleft.image_label_child.set_stylesheet_hovered(False) 541 542 if n >= 2: 543 i += 1 544 file_path = urls[i].toLocalFile() 545 self.will_start_loading.emit(True, "Loading to creator " + str(i+1) + "/" + n_str + "...") 546 loaded = self.app_topright.load_image(file_path) 547 if not loaded: 548 self.app_topright.image_label_child.set_stylesheet_hovered(False) 549 550 if n >= 3: 551 i += 1 552 file_path = urls[i].toLocalFile() 553 self.will_start_loading.emit(True, "Loading to creator " + str(i+1) + "/" + n_str + "...") 554 loaded = self.app_bottomright.load_image(file_path) 555 if not loaded: 556 self.app_bottomright.image_label_child.set_stylesheet_hovered(False) 557 558 if n >= 4: 559 i += 1 560 file_path = urls[i].toLocalFile() 561 self.will_start_loading.emit(True, "Loading to creator " + str(i+1) + "/" + n_str + "...") 562 loaded = self.app_bottomleft.load_image(file_path) 563 if not loaded: 564 self.app_bottomleft.image_label_child.set_stylesheet_hovered(False) 565 566 self.has_stopped_loading.emit(False) 567 568 event.accept() 569 else: 570 event.ignore() 571 572 def grab_image_urls_from_mimedata(self, mimedata): 573 """mimeData: Get urls (filepaths) from drag event.""" 574 urls = list() 575 for url in mimedata.urls(): 576 if any([filetype in url.toLocalFile().lower() for filetype in self.image_filetypes]): 577 urls.append(url) 578 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.
491 def dragEnterEvent(self, event): 492 """Override dragEnterEvent() to accept multiple (1-4) image files and set stylesheet(s) as hovered.""" 493 urls = self.grab_image_urls_from_mimedata(event.mimeData()) 494 if len(event.mimeData().urls()) >= 1 and len(event.mimeData().urls()) <= 4 and self.grab_image_urls_from_mimedata(event.mimeData()): 495 self.app_main_topleft.image_label_child.set_stylesheet_hovered(True) 496 i = 0 497 if len(urls) >= 2: 498 i += 1 499 self.app_topright.image_label_child.set_stylesheet_hovered(True) 500 501 if len(urls) >= 3: 502 i += 1 503 self.app_bottomright.image_label_child.set_stylesheet_hovered(True) 504 505 if len(urls) >= 4: 506 i += 1 507 self.app_bottomleft.image_label_child.set_stylesheet_hovered(True) 508 event.accept() 509 else: 510 event.ignore()
Override dragEnterEvent() to accept multiple (1-4) image files and set stylesheet(s) as hovered.
512 def dragMoveEvent(self, event): 513 """Override dragMoveEvent() to accept multiple (1-4) image files.""" 514 if len(event.mimeData().urls()) >= 1 and len(event.mimeData().urls()) <= 4 and self.grab_image_urls_from_mimedata(event.mimeData()): 515 event.accept() 516 else: 517 event.ignore()
Override dragMoveEvent() to accept multiple (1-4) image files.
519 def dragLeaveEvent(self, event): 520 """Override dragLeaveEvent() to set stylesheet(s) as no longer hovered.""" 521 self.app_main_topleft.image_label_child.set_stylesheet_hovered(False) 522 self.app_topright.image_label_child.set_stylesheet_hovered(False) 523 self.app_bottomright.image_label_child.set_stylesheet_hovered(False) 524 self.app_bottomleft.image_label_child.set_stylesheet_hovered(False)
Override dragLeaveEvent() to set stylesheet(s) as no longer hovered.
526 def dropEvent(self, event): 527 """event: Override dropEvent() to read filepath(s) from 1-4 dragged images and load the preview(s).""" 528 urls = self.grab_image_urls_from_mimedata(event.mimeData()) 529 n = len(urls) 530 n_str = str(n) 531 if n >= 1 and n <= 4 and urls: 532 event.setDropAction(QtCore.Qt.CopyAction) 533 i = 0 534 file_path = urls[i].toLocalFile() 535 536 self.will_start_loading.emit(True, "Loading to creator " + str(i+1) + "/" + n_str + "...") 537 538 loaded = self.app_main_topleft.load_image(file_path) 539 if not loaded: 540 self.app_main_topleft.image_label_child.set_stylesheet_hovered(False) 541 542 if n >= 2: 543 i += 1 544 file_path = urls[i].toLocalFile() 545 self.will_start_loading.emit(True, "Loading to creator " + str(i+1) + "/" + n_str + "...") 546 loaded = self.app_topright.load_image(file_path) 547 if not loaded: 548 self.app_topright.image_label_child.set_stylesheet_hovered(False) 549 550 if n >= 3: 551 i += 1 552 file_path = urls[i].toLocalFile() 553 self.will_start_loading.emit(True, "Loading to creator " + str(i+1) + "/" + n_str + "...") 554 loaded = self.app_bottomright.load_image(file_path) 555 if not loaded: 556 self.app_bottomright.image_label_child.set_stylesheet_hovered(False) 557 558 if n >= 4: 559 i += 1 560 file_path = urls[i].toLocalFile() 561 self.will_start_loading.emit(True, "Loading to creator " + str(i+1) + "/" + n_str + "...") 562 loaded = self.app_bottomleft.load_image(file_path) 563 if not loaded: 564 self.app_bottomleft.image_label_child.set_stylesheet_hovered(False) 565 566 self.has_stopped_loading.emit(False) 567 568 event.accept() 569 else: 570 event.ignore()
event: Override dropEvent() to read filepath(s) from 1-4 dragged images and load the preview(s).
572 def grab_image_urls_from_mimedata(self, mimedata): 573 """mimeData: Get urls (filepaths) from drag event.""" 574 urls = list() 575 for url in mimedata.urls(): 576 if any([filetype in url.toLocalFile().lower() for filetype in self.image_filetypes]): 577 urls.append(url) 578 return urls
mimeData: Get urls (filepaths) from drag event.
582def main(): 583 """Demo the drag-and-drop function in the 2x2 panel.""" 584 585 app = QtWidgets.QApplication(sys.argv) 586 587 demo = FourDragDropImageLabel() 588 demo.show() 589 590 sys.exit(app.exec_())
Demo the drag-and-drop function in the 2x2 panel.