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()
class ImageLabel(PyQt5.QtWidgets.QLabel):
 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.

def setPixmap(self, image):
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.

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

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

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

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

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

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

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

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

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

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

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

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

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

class ImageLabelMain(ImageLabel):
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.

class DragDropImageLabel(PyQt5.QtWidgets.QWidget):
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.
def set_addable(self, boolean):
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.

def dragEnterEvent(self, event):
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.

def dragMoveEvent(self, event):
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.

def dragLeaveEvent(self, event):
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.

def dropEvent(self, event):
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.

def grab_image_urls_from_mimedata(self, mimedata):
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.

def mouseDoubleClickEvent(self, 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.

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

Trigger dialog window to open image when button to select image is clicked.

def was_clicked_clear_pushbutton(self):
349    def was_clicked_clear_pushbutton(self):
350        """Clear image preview when clear button is clicked."""
351        self.clear_image()

Clear image preview when clear button is clicked.

def set_image(self, pixmap):
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.

def load_image(self, file_path):
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.
def open_image_via_dialog(self):
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.

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

def set_text(self, text):
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.

def set_filename_label(self, text):
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.

def display_loading_grayout(self, boolean, text='Loading...', pseudo_load_time=0.2):
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.
class FourDragDropImageLabel(PyQt5.QtWidgets.QFrame):
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.

def dragEnterEvent(self, event):
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.

def dragMoveEvent(self, event):
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.

def dragLeaveEvent(self, event):
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.

def dropEvent(self, event):
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).

def grab_image_urls_from_mimedata(self, mimedata):
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.

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