butterfly_viewer.aux_dragdrop

Drag-and-drop interface widgets and their supporting subwidgets for Butterfly Viewer.

Not intended as a script.

Interface widgets are:

DragDropImageLabel, for users to drag and drop an image from local storage and show a preview in the drop area. FourDragDropImageLabel, a 2x2 panel of DragDropImageLabel designed for users to arrange images for a SplitView.

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

Drag-and-drop widget to preview an image from local storage and hold its filepath.

Includes:

Button to select an image from a dialog window. Button to clear the current image.

Arguments:
  • show_filename (bool): True to show label with filename over image preview; False to hide.
  • show_pushbuttons (bool): True to show button for selecting file from dialog and button to clear image; False to hide.
  • is_main (bool): True if the label is the drag zone for the main image of SplitView; False if not.
  • text_default (str): Text to show when no image preview is showing.
def set_addable(self, boolean):
291    def set_addable(self, boolean):
292        """bool: Set whether an imaged may be added (dragged and dropped) into the widget."""
293        self.image_label_child.IS_ADDABLE = boolean
294        self.image_label_child.setEnabled(boolean)
295        self.open_pushbutton.setEnabled(boolean)
296        self.setAcceptDrops(boolean)
297        self.filename_label.setEnabled(boolean)
298        self.image_label_child.set_stylesheet_addable(boolean)

bool: Set whether an imaged may be added (dragged and dropped) into the widget.

def dragEnterEvent(self, event):
301    def dragEnterEvent(self, event):
302        """event: Override dragEnterEvent() to set stylesheet as hovered and read filepath from a dragged image, but reject multiple files."""
303        if len(event.mimeData().urls()) is 1 and self.grab_image_urls_from_mimedata(event.mimeData()):
304            self.image_label_child.set_stylesheet_hovered(True)
305            event.accept()
306        else:
307            event.ignore()

event: Override dragEnterEvent() to set stylesheet as hovered and read filepath from a dragged image, but reject multiple files.

def dragMoveEvent(self, event):
309    def dragMoveEvent(self, event):
310        """event: Override dragMoveEvent() to reject multiple files."""
311        if len(event.mimeData().urls()) is 1 and self.grab_image_urls_from_mimedata(event.mimeData()):
312            event.accept()
313        else:
314            event.ignore()

event: Override dragMoveEvent() to reject multiple files.

def dragLeaveEvent(self, event):
316    def dragLeaveEvent(self, event):
317        """event: Override dragLeaveEvent() to set stylesheet as not hovered."""
318        self.image_label_child.set_stylesheet_hovered(False)

event: Override dragLeaveEvent() to set stylesheet as not hovered.

def dropEvent(self, event):
320    def dropEvent(self, event):
321        """event: Override dropEvent() to read filepath from a dragged image and load image preview."""
322        urls = self.grab_image_urls_from_mimedata(event.mimeData())
323        if len(urls) is 1 and urls:
324            event.setDropAction(QtCore.Qt.CopyAction)
325            file_path = urls[0].toLocalFile()
326            loaded = self.load_image(file_path)
327            if loaded:
328                event.accept()
329            else:
330                event.ignore()
331                self.image_label_child.set_stylesheet_hovered(False)
332        else:
333            event.ignore()

event: Override dropEvent() to read filepath from a dragged image and load image preview.

def grab_image_urls_from_mimedata(self, mimedata):
335    def grab_image_urls_from_mimedata(self, mimedata):
336        """mimeData: Get urls (filepaths) from drag event."""
337        urls = list()
338        for url in mimedata.urls():
339            if any([filetype in url.toLocalFile().lower() for filetype in self.image_filetypes]):
340                urls.append(url)
341        return urls

mimeData: Get urls (filepaths) from drag event.

def mouseDoubleClickEvent(self, event):
343    def mouseDoubleClickEvent(self, event):
344        """event: Override mouseDoubleClickEvent() to trigger dialog window to open image."""
345        self.open_image_via_dialog() 

event: Override mouseDoubleClickEvent() to trigger dialog window to open image.

def was_clicked_open_pushbutton(self):
347    def was_clicked_open_pushbutton(self):
348        """Trigger dialog window to open image when button to select image is clicked."""
349        self.open_image_via_dialog()

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

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

Clear image preview when clear button is clicked.

def set_image(self, pixmap):
355    def set_image(self, pixmap):
356        """QPixmap: Scale and set preview of image; set status of clear button."""
357        self.image_label_child.setPixmap(pixmap.scaled(self.MAX_DIMENSION_FOR_IMAGE_IN_LABEL, self.MAX_DIMENSION_FOR_IMAGE_IN_LABEL, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation))
358        self.clear_pushbutton.setEnabled(True)
359        self.clear_pushbutton.setVisible(True)

QPixmap: Scale and set preview of image; set status of clear button.

def load_image(self, file_path):
361    def load_image(self, file_path):
362        """str: Load image from filepath with loading grayout; set filename text.
363        
364        Returns:
365            loaded (bool): True if image successfully loaded; False if not."""
366        loading_text = "Loading..."
367        if self.show_filepath_while_loading:
368            loading_text = loading_text.replace("...",  " '" + file_path.split("/")[-1] + "'...")
369        self.display_loading_grayout(True, loading_text)
370        pixmap = QtGui.QPixmap(file_path)
371        if pixmap.depth() is 0:
372            self.display_loading_grayout(False)
373            return False
374        
375        angle = get_exif_rotation_angle(file_path)
376        if angle:
377            pixmap = pixmap.transformed(QtGui.QTransform().rotate(angle))
378
379        self.set_image(pixmap)
380        self.set_filename_label(file_path)
381        self.file_path = file_path
382        self.display_loading_grayout(False)
383        return True

str: Load image from filepath with loading grayout; set filename text.

Returns:
  • loaded (bool): True if image successfully loaded; False if not.
def open_image_via_dialog(self):
385    def open_image_via_dialog(self):
386        """Open dialog window to select and load image from file."""
387        file_dialog = QtWidgets.QFileDialog(self)
388        
389        file_dialog.setNameFilters([
390            "Common image files (*.jpeg *.jpg  *.png *.tiff *.tif *.bmp *.gif *.webp *.svg)",
391            "All files (*)",
392            "JPEG image files (*.jpeg *.jpg)", 
393            "PNG image files (*.png)", 
394            "TIFF image files (*.tiff *.tif)", 
395            "BMP (*.bmp)"])
396        file_dialog.setFileMode(QtWidgets.QFileDialog.ExistingFile)
397        
398        if not file_dialog.exec_():
399            return
400        
401        file_path = file_dialog.selectedFiles()[0]
402        
403        self.load_image(file_path)

Open dialog window to select and load image from file.

def clear_image(self):
405    def clear_image(self):
406        """Clear image preview and filepath; set status of clear button; set text of drag zone."""
407        if self.image_label_child.pixmap():
408            self.image_label_child.clear()
409            
410        self.set_text(self.text_default)
411        self.file_path = None
412        self.clear_pushbutton.setEnabled(False)
413        self.clear_pushbutton.setVisible(False)
414        self.filename_label.setText("No filename available")
415        self.filename_label.setVisible(False)

Clear image preview and filepath; set status of clear button; set text of drag zone.

def set_text(self, text):
417    def set_text(self, text):
418        """str: Set text of drag zone when there is no image preview."""
419        text_margin_vertical = "\n\n\n"
420        self.image_label_child.setText(text_margin_vertical+text+text_margin_vertical)

str: Set text of drag zone when there is no image preview.

def set_filename_label(self, text):
422    def set_filename_label(self, text):
423        """str: Set text of filename label on image preview."""
424        self.filename_label.setText(text)
425        self.filename_label.setVisible(self.show_filename)

str: Set text of filename label on image preview.

def display_loading_grayout(self, boolean, text='Loading...', pseudo_load_time=0.2):
427    def display_loading_grayout(self, boolean, text="Loading...", pseudo_load_time=0.2):
428        """Show/hide grayout overlay on label for loading sequences.
429
430        Args:
431            boolean (bool): True to show grayout; False to hide.
432            text (str): The text to show on the grayout.
433            pseudo_load_time (float): The delay (in seconds) to hide the grayout to give users a feeling of action.
434        """ 
435        if not boolean:
436            text = "Loading..."
437        self.loading_grayout_label.setText(text)
438        self.loading_grayout_label.setVisible(boolean)
439        if boolean:
440            self.loading_grayout_label.repaint()
441        if not boolean:
442            time.sleep(pseudo_load_time)

Show/hide grayout overlay on label for loading sequences.

Arguments:
  • boolean (bool): True to show grayout; False to hide.
  • text (str): The text to show on the grayout.
  • pseudo_load_time (float): The delay (in seconds) to hide the grayout to give users a feeling of action.
class FourDragDropImageLabel(PyQt5.QtWidgets.QFrame):
446class FourDragDropImageLabel(QtWidgets.QFrame):
447    """2x2 panel of drag-and-drop zones for users to arrange images for a SplitView.
448
449    Instantiate without input.
450    
451    Allows dragging multiple files (1–4) at once.
452    """
453
454    will_start_loading = QtCore.pyqtSignal(bool, str)
455    has_stopped_loading = QtCore.pyqtSignal(bool)
456
457    def __init__(self):
458        super().__init__()
459
460        self.image_filetypes = [
461            ".jpeg", ".jpg", ".jpe", ".jif", ".jfif", ".jfi", ".pjpeg", ".pjp",
462            ".png", ".apng",
463            ".tiff", ".tif",
464            ".bmp",
465            ".gif",
466            ".webp",
467            ".svg",
468            ".ico", ".cur"]
469
470        self.setAcceptDrops(True)
471
472        main_layout = QtWidgets.QGridLayout()
473        
474        self.app_main_topleft   = DragDropImageLabel(show_filename=True, show_pushbuttons=True, is_main=True, text_default="Drag image(s)")
475        self.app_topright       = DragDropImageLabel(show_filename=True, show_pushbuttons=True)
476        self.app_bottomleft     = DragDropImageLabel(show_filename=True, show_pushbuttons=True)
477        self.app_bottomright    = DragDropImageLabel(show_filename=True, show_pushbuttons=True)
478        
479        main_layout.addWidget(self.app_main_topleft, 0, 0)
480        main_layout.addWidget(self.app_topright, 0, 1)
481        main_layout.addWidget(self.app_bottomleft, 1, 0)
482        main_layout.addWidget(self.app_bottomright, 1, 1)
483        
484        main_layout.setColumnStretch(0,1)
485        main_layout.setColumnStretch(1,1)
486        main_layout.setRowStretch(0,1)
487        main_layout.setRowStretch(1,1)
488        
489        contents_margins_w = 0
490        main_layout.setContentsMargins(contents_margins_w, contents_margins_w, contents_margins_w, contents_margins_w)
491        main_layout.setSpacing(4)
492        
493        self.setLayout(main_layout)
494    
495    def dragEnterEvent(self, event):
496        """Override dragEnterEvent() to accept multiple (1-4) image files and set stylesheet(s) as hovered."""
497        urls = self.grab_image_urls_from_mimedata(event.mimeData())
498        if len(event.mimeData().urls()) >= 1 and len(event.mimeData().urls()) <= 4 and self.grab_image_urls_from_mimedata(event.mimeData()):
499            self.app_main_topleft.image_label_child.set_stylesheet_hovered(True)
500            i = 0
501            if len(urls) >= 2:
502                i += 1
503                self.app_topright.image_label_child.set_stylesheet_hovered(True)
504
505                if len(urls) >= 3:
506                    i += 1
507                    self.app_bottomright.image_label_child.set_stylesheet_hovered(True)
508
509                    if len(urls) >= 4:
510                        i += 1
511                        self.app_bottomleft.image_label_child.set_stylesheet_hovered(True)
512            event.accept()
513        else:
514            event.ignore()
515
516    def dragMoveEvent(self, event):
517        """Override dragMoveEvent() to accept multiple (1-4) image files."""
518        if len(event.mimeData().urls()) >= 1 and len(event.mimeData().urls()) <= 4 and self.grab_image_urls_from_mimedata(event.mimeData()):
519            event.accept()
520        else:
521            event.ignore()
522
523    def dragLeaveEvent(self, event):
524        """Override dragLeaveEvent() to set stylesheet(s) as no longer hovered."""
525        self.app_main_topleft.image_label_child.set_stylesheet_hovered(False)
526        self.app_topright.image_label_child.set_stylesheet_hovered(False)
527        self.app_bottomright.image_label_child.set_stylesheet_hovered(False)
528        self.app_bottomleft.image_label_child.set_stylesheet_hovered(False)
529
530    def dropEvent(self, event):
531        """event: Override dropEvent() to read filepath(s) from 1-4 dragged images and load the preview(s)."""
532        urls = self.grab_image_urls_from_mimedata(event.mimeData())
533        n = len(urls)
534        n_str = str(n)
535        if n >= 1 and n <= 4 and urls:
536            event.setDropAction(QtCore.Qt.CopyAction)
537            i = 0
538            file_path = urls[i].toLocalFile()
539
540            self.will_start_loading.emit(True, "Loading to creator " + str(i+1) + "/" + n_str + "...")
541
542            loaded = self.app_main_topleft.load_image(file_path)
543            if not loaded:
544                self.app_main_topleft.image_label_child.set_stylesheet_hovered(False)
545
546            if n >= 2:
547                i += 1
548                file_path = urls[i].toLocalFile()
549                self.will_start_loading.emit(True, "Loading to creator " + str(i+1) + "/" + n_str + "...")
550                loaded = self.app_topright.load_image(file_path)
551                if not loaded:
552                    self.app_topright.image_label_child.set_stylesheet_hovered(False)
553
554                if n >= 3:
555                    i += 1
556                    file_path = urls[i].toLocalFile()
557                    self.will_start_loading.emit(True, "Loading to creator " + str(i+1) + "/" + n_str + "...")
558                    loaded = self.app_bottomright.load_image(file_path)
559                    if not loaded:
560                        self.app_bottomright.image_label_child.set_stylesheet_hovered(False)
561
562                    if n >= 4:
563                        i += 1
564                        file_path = urls[i].toLocalFile()
565                        self.will_start_loading.emit(True, "Loading to creator " + str(i+1) + "/" + n_str + "...")
566                        loaded = self.app_bottomleft.load_image(file_path)
567                        if not loaded:
568                            self.app_bottomleft.image_label_child.set_stylesheet_hovered(False)
569
570            self.has_stopped_loading.emit(False)
571            
572            event.accept()
573        else:
574            event.ignore()
575
576    def grab_image_urls_from_mimedata(self, mimedata):
577        """mimeData: Get urls (filepaths) from drag event."""
578        urls = list()
579        for url in mimedata.urls():
580            if any([filetype in url.toLocalFile().lower() for filetype in self.image_filetypes]):
581                urls.append(url)
582        return urls

2x2 panel of drag-and-drop zones for users to arrange images for a SplitView.

Instantiate without input.

Allows dragging multiple files (1–4) at once.

def dragEnterEvent(self, event):
495    def dragEnterEvent(self, event):
496        """Override dragEnterEvent() to accept multiple (1-4) image files and set stylesheet(s) as hovered."""
497        urls = self.grab_image_urls_from_mimedata(event.mimeData())
498        if len(event.mimeData().urls()) >= 1 and len(event.mimeData().urls()) <= 4 and self.grab_image_urls_from_mimedata(event.mimeData()):
499            self.app_main_topleft.image_label_child.set_stylesheet_hovered(True)
500            i = 0
501            if len(urls) >= 2:
502                i += 1
503                self.app_topright.image_label_child.set_stylesheet_hovered(True)
504
505                if len(urls) >= 3:
506                    i += 1
507                    self.app_bottomright.image_label_child.set_stylesheet_hovered(True)
508
509                    if len(urls) >= 4:
510                        i += 1
511                        self.app_bottomleft.image_label_child.set_stylesheet_hovered(True)
512            event.accept()
513        else:
514            event.ignore()

Override dragEnterEvent() to accept multiple (1-4) image files and set stylesheet(s) as hovered.

def dragMoveEvent(self, event):
516    def dragMoveEvent(self, event):
517        """Override dragMoveEvent() to accept multiple (1-4) image files."""
518        if len(event.mimeData().urls()) >= 1 and len(event.mimeData().urls()) <= 4 and self.grab_image_urls_from_mimedata(event.mimeData()):
519            event.accept()
520        else:
521            event.ignore()

Override dragMoveEvent() to accept multiple (1-4) image files.

def dragLeaveEvent(self, event):
523    def dragLeaveEvent(self, event):
524        """Override dragLeaveEvent() to set stylesheet(s) as no longer hovered."""
525        self.app_main_topleft.image_label_child.set_stylesheet_hovered(False)
526        self.app_topright.image_label_child.set_stylesheet_hovered(False)
527        self.app_bottomright.image_label_child.set_stylesheet_hovered(False)
528        self.app_bottomleft.image_label_child.set_stylesheet_hovered(False)

Override dragLeaveEvent() to set stylesheet(s) as no longer hovered.

def dropEvent(self, event):
530    def dropEvent(self, event):
531        """event: Override dropEvent() to read filepath(s) from 1-4 dragged images and load the preview(s)."""
532        urls = self.grab_image_urls_from_mimedata(event.mimeData())
533        n = len(urls)
534        n_str = str(n)
535        if n >= 1 and n <= 4 and urls:
536            event.setDropAction(QtCore.Qt.CopyAction)
537            i = 0
538            file_path = urls[i].toLocalFile()
539
540            self.will_start_loading.emit(True, "Loading to creator " + str(i+1) + "/" + n_str + "...")
541
542            loaded = self.app_main_topleft.load_image(file_path)
543            if not loaded:
544                self.app_main_topleft.image_label_child.set_stylesheet_hovered(False)
545
546            if n >= 2:
547                i += 1
548                file_path = urls[i].toLocalFile()
549                self.will_start_loading.emit(True, "Loading to creator " + str(i+1) + "/" + n_str + "...")
550                loaded = self.app_topright.load_image(file_path)
551                if not loaded:
552                    self.app_topright.image_label_child.set_stylesheet_hovered(False)
553
554                if n >= 3:
555                    i += 1
556                    file_path = urls[i].toLocalFile()
557                    self.will_start_loading.emit(True, "Loading to creator " + str(i+1) + "/" + n_str + "...")
558                    loaded = self.app_bottomright.load_image(file_path)
559                    if not loaded:
560                        self.app_bottomright.image_label_child.set_stylesheet_hovered(False)
561
562                    if n >= 4:
563                        i += 1
564                        file_path = urls[i].toLocalFile()
565                        self.will_start_loading.emit(True, "Loading to creator " + str(i+1) + "/" + n_str + "...")
566                        loaded = self.app_bottomleft.load_image(file_path)
567                        if not loaded:
568                            self.app_bottomleft.image_label_child.set_stylesheet_hovered(False)
569
570            self.has_stopped_loading.emit(False)
571            
572            event.accept()
573        else:
574            event.ignore()

event: Override dropEvent() to read filepath(s) from 1-4 dragged images and load the preview(s).

def grab_image_urls_from_mimedata(self, mimedata):
576    def grab_image_urls_from_mimedata(self, mimedata):
577        """mimeData: Get urls (filepaths) from drag event."""
578        urls = list()
579        for url in mimedata.urls():
580            if any([filetype in url.toLocalFile().lower() for filetype in self.image_filetypes]):
581                urls.append(url)
582        return urls

mimeData: Get urls (filepaths) from drag event.

def main():
586def main():
587    """Demo the drag-and-drop function in the 2x2 panel."""
588
589    app = QtWidgets.QApplication(sys.argv)
590    
591    demo = FourDragDropImageLabel()
592    demo.show()
593    
594    sys.exit(app.exec_())

Demo the drag-and-drop function in the 2x2 panel.