Source code for schrodinger.ui.qt.file_selector

"""
Module containing FileSelector. The widget's file selection can be obtained by
mapping the widget to a param or alternatively by connecting a slot to the
fileSelectionChanged signal.

Mapping this widget to a param will depend on whether or not
support_multiple_files is True. E.g. with `support_multiple_files = False`::

        class WidgetModel(parameters.CompoundParam):
            input_file: str

        class Widget(mappers.MapperMixin, basewidgets.BaseWidget):
            def initSetUp(self):
                super().initSetUp()
                self.file_selector = file_selector.FileSelector(self)

            def defineMappings(self):
                M = self.model_class
                return [(self.file_selector, M.input_file)]

Alternatively, with `support_multiple_files = True`, use::

        class WidgetModel(parameters.CompoundParam):
            input_files: List[str]

        class Widget(mappers.MapperMixin, basewidgets.BaseWidget):
            def initSetUp(self):
                super().initSetUp()
                self.file_selector = file_selector.FileSelector(self)

            def defineMappings(self):
                M = self.model_class
                return [(self.file_selector, M.input_files)]

To use without mapping, hook up the fileSelectionChanged signal to a
slot that calls `FileSelector.getFilePath()` or `getFilePaths()` depending
on which is needed.
"""

from typing import Optional
from typing import Union

from schrodinger.models import mappers
from schrodinger.Qt import QtCore
from schrodinger.Qt import QtWidgets
from schrodinger.ui.qt import basewidgets
from schrodinger.ui.qt import filedialog
from schrodinger.ui.qt.appframework2 import application

INVALID_FILE_MSG = 'file path cannot contain comma (,)'


[docs]class FileSelector(mappers.TargetMixin, basewidgets.BaseWidget): """ Widget for showing an entry field and a browse button to let the user specify a single file or optionally multiple files. :cvar fileSelectionChanged: Signal emitted when the file selection changes. :vartype fileSelectionChanged: QtCore.pyqtSignal """ fileSelectionChanged = QtCore.pyqtSignal()
[docs] def __init__(self, parent: Optional[QtWidgets.QWidget] = None, filter_str: str = 'All Files (*)', support_multiple_files: bool = False, initial_dir: Optional[str] = None): """ :param parent: Parent widget. :param filter_str: The filter to apply to the file dialog that will open upon clicking "Browse". Must be a valid QFileDialog filter. E.g. `Image Files (*.png *.jpeg);;Text Files (*.txt)` :param support_multiple_files: Whether or not to allow the user to select multiple files at once from the file dialog. :param initial_dir: Initial directory. Default is CWD. """ # To implement later: # filerange self._support_multiple_files = support_multiple_files if not isinstance(filter_str, str): msg = ('Expected a string of supported file formats but instead got' f' {type(filter_str)}.') raise TypeError(msg) self._filter_str = filter_str self._initial_dir = initial_dir super().__init__(parent) if support_multiple_files: file_lbl_text = 'File names:' else: file_lbl_text = 'File name:' self.setFileLabelText(file_lbl_text)
[docs] def initSetUp(self): super().initSetUp() self.file_lbl = QtWidgets.QLabel() self.file_le = QtWidgets.QLineEdit() self.file_le.editingFinished.connect(self._filePathEdited) self.file_browse_btn = QtWidgets.QPushButton("Browse...") self.file_browse_btn.clicked.connect(self._browseFile)
[docs] def initLayOut(self): super().initLayOut() self.file_layout = QtWidgets.QHBoxLayout() self.file_layout.addWidget(self.file_lbl) self.file_layout.addWidget(self.file_le) self.file_layout.addWidget(self.file_browse_btn) self.widget_layout.addLayout(self.file_layout)
# ========================================================================== # Main API # ==========================================================================
[docs] def setFileLabelText(self, text: str): self.file_lbl.setText(text)
# ========================================================================== # Non-Mapping API # ==========================================================================
[docs] def getFilePath(self) -> str: """ Return the currently selected file path. Allows access to target :raise AssertionError: If this method is called when supporting multiple files. """ if self._support_multiple_files: msg = 'Use getFilePaths() when supporting multiple files' raise RuntimeError(msg) return self.targetGetValue()
[docs] def getFilePaths(self) -> Union[list, str]: """ Return the currently selected file paths. :raise AssertionError: If this method is called when not supporting multiple files. """ if self._support_multiple_files: return self.targetGetValue() msg = 'Use getFilePath() when not supporting multiple files' raise RuntimeError(msg)
# ========================================================================== # Internal Implementation Methods # ==========================================================================
[docs] def targetGetValue(self) -> Union[str, list]: """ If multiple files supported, split the file paths entered into the line edit by ",". Otherwise, return the line edit text. """ if self._support_multiple_files: file_str = self.file_le.text() file_list = file_str.split(",") return [fn.strip() for fn in file_list if fn] return self.file_le.text()
[docs] def targetSetValue(self, value: Union[str, list]): """ Set the line edit text based for a valid `value` and emit two signals: targetValueChanged for when this widget mapped to a param, and fileSelectionChanged for when it is not. :param value: The path(s) to set to the line edit. May be single path as a string or multliple paths in a list. """ if self._support_multiple_files: msg = (f'Must supply paths as a list when supporting multiple' f' input files, not {type(value)}') if not isinstance(value, list): raise TypeError(msg) for fname in value: if "," in fname: raise ValueError(INVALID_FILE_MSG) file_str = ",".join(value) self.file_le.setText(file_str) else: msg = (f'Must supply path as a string, not {type(value)} when' f' not supporting multiple input files') if not isinstance(value, str): raise TypeError(msg) if "," in value: raise ValueError(INVALID_FILE_MSG) self.file_le.setText(value) self.targetValueChanged.emit() self.fileSelectionChanged.emit()
def _browseFile(self): """ Open a browse file dialog and set the line edit text if the file is valid. """ # TODO PANEL-18646: add option to specify dialog ID to make it possible # for panels to reuse last directory browsed to by the user between # multiple browse dialogs in that panel. kwargs = { 'parent': self, 'dir': self._initial_dir, 'filter': self._filter_str, } if self._support_multiple_files: new_files = filedialog.get_open_file_names( caption="Select Input File(s)", **kwargs) if not new_files: return self.targetSetValue(new_files) else: new_file = filedialog.get_open_file_name( caption="Select Input File", **kwargs) if not new_file: return self.targetSetValue(new_file) def _filePathEdited(self): """ Update the model when user manually edits the file path. """ # This will automatically handle commas separating file names: self.targetSetValue(self.targetGetValue())
[docs]class SlimFileSelector(FileSelector): """ A file selector variant that has no label or entry field, and is made up of a single widget: a "Browse..." button. Panels that use this widget are expected to display the loaded file in a separate UI. """
[docs] def initLayOut(self): super().initLayOut() self.file_lbl.hide() self.file_le.hide()
if __name__ == '__main__': def _main(): FileSelector().run(blocking=True) application.start_application(_main)