Source code for schrodinger.ui.qt.filedialog

"""
Schrodinger version of the QFileDialog class of the QtGui module.

Defines a FileDialog class that mimics the Maestro's file dialogs.

Copyright Schrodinger, LLC. All rights reserved.
"""

# Contributors: Pat Lorton, Matvey Adzhigirey

import os
import os.path
import sys
from collections import OrderedDict
from past.utils import old_div

import schrodinger.ui.qt.icons as icons
from schrodinger.infra import mmproj
from schrodinger.Qt import QtCore
from schrodinger.Qt import QtGui
from schrodinger.Qt import QtWidgets
from schrodinger.utils import fileutils
from schrodinger.ui.qt import utils as qt_utils
from schrodinger.ui import maestro_ui
from schrodinger.ui.maestro_ui import MM_QProjS


def use_native_file_dialog():
    return maestro_ui.maestro_native_file_dialog_is_enabled()


def filter_string_from_formats(formats=[fileutils.MAESTRO]):  # noqa: M511
    """
    Create a Qt file dialog filter string from a list of structure formats.

    :param formats: List of formats, see schrodinger.utils.fileutils module
            for available format string constants.
    :type formats: list of str

    :return: Filter string
    :rtype: str
    """

    _FORMAT_NAMES = {
        fileutils.PDB: "PDB files",
        fileutils.MOL2: "Mol2 files",
        fileutils.SD: "SD files",
        fileutils.MAESTRO: "Maestro files",
        fileutils.MAESTRO_STRICT: "Maestro files",
        fileutils.SMILES: "SMILES files",
        fileutils.SMILESCSV: "SMILES CSV files",
        fileutils.CMS: "Desmond CMS files",
        fileutils.PFX: "PathFinder reactant files"
    }

    filter_items = []
    all_supported = []
    for fmt in formats:
        try:
            name = _FORMAT_NAMES[fmt]
            exts = fileutils.EXTENSIONS[fmt]
        except KeyError:
            raise ValueError("Unsupported format: %s" % fmt)

        exts = ['*' + ext for ext in exts]

        type_str = name + " (%s)" % " ".join(exts)
        filter_items.append(type_str)
        all_supported.extend(exts)

    if len(filter_items) > 1:
        type_str = "All supported files (%s)" % " ".join(all_supported)
        filter_items.insert(0, type_str)

    return ';;'.join(filter_items)


# Common filters:
MAESTRO_FILTER = filter_string_from_formats([fileutils.MAESTRO])
SD_FILTER = filter_string_from_formats([fileutils.SD])
PDB_FILTER = filter_string_from_formats([fileutils.PDB])
MAESTRO_SD_FILTER = filter_string_from_formats(
    [fileutils.MAESTRO, fileutils.SD])


def filter_string_from_extensions(extensions, add_all=False):
    """
    Create a filter string for the given extensions

    :param dict extensions: Keys are descriptive file types ('Image file'),
        values are an iterable of associated extension(s). If there is only one
        extension it can be given as a simple string rather than an iterable of
        length 1 (['.png', '.jpg'] or just '-2d.png').

    :param bool add_all: Whether to add an additional "All files" filter

    :rtype: str
    :return: A string formatted for the file dialog filter keyword
    """
    strings = []
    for name, exts in extensions.items():
        if isinstance(exts, str):
            exts = [exts]
        exstr = ' '.join('*' + x for x in exts)
        strings.append(f'{name} ({exstr})')

    if add_all:
        strings.append('All files (*)')
    return ';;'.join(strings)


class CustomSideBarMixin(object):

    def _setup_sidebar(self, sidebar_links=None):
        """
        Used to set up the sidebar directories in file browsers.

        Note that this method sets up a signal/slot for the ListView model that
        holds the data for the sidebar, and this model is altered by calls to
        setIconProvider(), so this method should be called after any calls to
        that method.

        :type sidebar_links: dict
        :param sidebar_links: dictionary of additional sidebar links.  Keys are
            names of the links and values are paths
        """

        # Needed for customization to work, on Qt5:
        self.setOption(QtWidgets.QFileDialog.DontUseNativeDialog, True)

        # These are our "Fav 4" directories - add them to every dialog
        HOME = 'Home'
        DESKTOP = 'Desktop'
        DOCUMENTS = 'Documents'
        WORKING = 'Working Directory'

        # Load up a dictionary with our favorite 4 first then any requested links
        dirs = OrderedDict()
        services = QtGui.QDesktopServices()
        dirs[HOME] = _get_standard_loc("HomeLocation")
        dirs[DESKTOP] = _get_standard_loc("DesktopLocation")
        dirs[DOCUMENTS] = _get_standard_loc("DocumentsLocation")
        dirs[WORKING] = os.getcwd()
        if sidebar_links:
            dirs.update(sidebar_links)

        # Convert the dirs dictionary to the types needed by PyQt.  We need to avoid
        # adding directories that don't exist, and we need to avoid adding the same
        # directory twice.  Both cases cause problems with QT.  EV:130022,
        # EV:135513.
        self._links = OrderedDict()
        for name, path in dirs.items():
            if os.path.exists(path):
                url = QtCore.QUrl.fromLocalFile(path)
                if url not in list(self._links.values()):
                    self._links[name] = url
        url_list = list(self._links.values())

        self.setSidebarUrls(url_list)

        # We want to give links their proper names, so we need to
        # grab the model that displays them so we can edit the name
        sidebar = self.findChild(QtWidgets.QListView, "sidebar")
        self._smodel = sidebar.model()

        # Update the original side-bar:
        self._updateItemNames()

        # Update the side-bar item names every time it's modified by the user:
        self._smodel.dataChanged.connect(self._updateItemNames)

    def _updateItemNames(self):
        """
        Update the names of the side-bar items. e.g. user's home directory
        will be named "Home" instead of "$USERNAME"; and the CWD will be named
        "Working Directory" instead of the basename of the current path.
        """
        sidebar_urls = self.sidebarUrls()

        for name, url in self._links.items():
            try:
                index = sidebar_urls.index(url)
            except ValueError:
                # This item was removed by the user or Qt (during cleanup)
                continue
            self._smodel.setData(self._smodel.index(index, 0), name)


def _get_standard_loc(standard_loc):
    """
    Get the specified location in a way that works on Qt4 and Qt5.  Once we've
    fully switched to Qt5, this code can be inlined and this function can be
    removed.
    """

    standard_loc = getattr(QtCore.QStandardPaths, standard_loc)
    return QtCore.QStandardPaths.writableLocation(standard_loc)


def get_existing_directory(parent="",
                           caption='Choose Directory',
                           dir=None,
                           accept_label='Choose',
                           file_mode=QtWidgets.QFileDialog.DirectoryOnly,
                           **kwargs):
    """
    Convenience function that returns a directory name as selected by the
    user

    The base class `base_file_dialog` documents the keyword arguments for this
    class.

    :type file_mode: QFileDialog.FileMode
    :param file_mode: What the user can select.  See the PyQt documentation for
        QFileDialog.FileMode (currently AnyFile, ExistingFile, Directory and
        ExistingFiles are the options).  Note that despite what the PyQt
        documentation states, it appears that QFileDialog.DirectoryOnly is needed to
        only display directores - it is not sufficient to use QFileDialog.Directory
        and setOptions(ShowDirsOnly).  Use QFileDialog.Directory to have the dialog
        also show the files in each directory.

    :rtype: str or None
    :return: full pathname of the file selected by the user, or None if Cancel
        was pressed
    """

    files = base_file_dialog(
        parent=parent,
        caption=caption,
        dir=dir,
        accept_label=accept_label,
        file_mode=file_mode,
        **kwargs)
    try:
        return files[0]
    except (IndexError, TypeError):
        return files


def _add_extension_if_necessary(files, filter):
    """
    Attach the first extension in filter to each filename in files that does not
    already have an extension

    :type files: list
    :param files: list of filenames

    :type filter: str
    :param filter: the list of filters that can be applied. The first extension
                   is used. The format is
                   ``"Filetype1 (*.ex1);;Filetype2 (*.ex2 *.ex3)"``

    :rtype: list
    :return: list of filenames in the same order as files.  Each item in the
        returned list is unchanged if it had an extension, or has an extension
        added to it if it did not. The added extension is the first one in the
        selected filter list of dialog.

    Examples filters::

        "Image files (*.png *.jpg *.bmp)"
        "Images (*.png *.xpm *.jpg);;Text files (*.txt);;All files (*)"

    """

    # Extract the first filter extension
    try:
        chunk = filter.split('(')[1]
    except (IndexError, AttributeError):
        # No extension listed in filter
        return
    chunk = chunk.split(')')[0]
    chunk = chunk.split()[0]
    extension = chunk.replace('*', "")

    new_filenames = []
    for afile in files:
        if not os.path.splitext(afile)[1]:
            afile = afile + extension
        new_filenames.append(afile)
    return new_filenames


def get_save_file_name(parent="",
                       caption='Save File',
                       dir=None,
                       filter='All Files (*)',
                       accept_label='Save',
                       accept_mode=QtWidgets.QFileDialog.AcceptSave,
                       file_mode=QtWidgets.QFileDialog.AnyFile,
                       **kwargs):
    """
    Convenience function that returns a filename as selected by the user

    The base class `base_file_dialog` documents the keyword arguments for this
    class.

    :rtype: str or None
    :return: full pathname of the file selected by the user, or None if Cancel
        was pressed
    """

    files = base_file_dialog(
        parent=parent,
        caption=caption,
        dir=dir,
        filter=filter,
        accept_label=accept_label,
        accept_mode=accept_mode,
        file_mode=file_mode,
        **kwargs)
    #Ignore adding extension to files if the cancel button was used
    if files:
        files = _add_extension_if_necessary(files, _last_selected_filter)
    try:
        return files[0]
    except (IndexError, TypeError):
        return files


def get_open_file_names(parent="",
                        caption='Open Files',
                        dir=None,
                        filter='All Files (*)',
                        accept_label='Open',
                        file_mode=QtWidgets.QFileDialog.ExistingFiles,
                        **kwargs):
    """
    Convenience function that returns a list of filenames as selected by the
    user

    The base class `base_file_dialog` documents the keyword arguments for this
    class.

    :rtype: list of str or None
    :return: list of full file pathnames selected by the user, or None if Cancel
        was pressed.
    """

    files = base_file_dialog(
        parent=parent,
        caption=caption,
        dir=dir,
        filter=filter,
        accept_label=accept_label,
        file_mode=file_mode,
        **kwargs)
    return files


def get_open_file_name(parent="",
                       caption='Open File',
                       dir=None,
                       filter='All Files (*)',
                       **kwargs):
    """
    Convenience function that returns a single filename as selected by the
    user

    The base class `base_file_dialog` documents the keyword arguments for this
    class.

    :rtype: str or None
    :return: full pathname of the file selected by the user, or None if Cancel
        was pressed
    """

    files = base_file_dialog(
        parent=parent, caption=caption, dir=dir, filter=filter, **kwargs)
    try:
        return files[0]
    except (IndexError, TypeError):
        return files


def get_open_wm_file_name(parent="", dir=None, **kwargs):
    """
    Convenience function that returns a single WaterMap file as selected by
    the user.

    See `base_file_dialog` for documentation.

    :return: Full pathname of the WaterMap file selected by
        the user or None if cancel was pressed.
    :rtype: str or None
    """
    caption = "Please select a WaterMap file."
    exts = ["*_wm.mae", "*_wm.maegz", "*_wm.mae.gz"]
    filter = "WaterMap files ({0})".format(" ".join(exts))
    return get_open_file_name(
        parent, caption=caption, dir=dir, filter=filter, **kwargs)


_last_selected_filter = None
"""The last filter chosen in a file dialog"""

_last_selected_directory = {}
""" Dictionary - keys are browser ID's and values are the last directory for
that id """

_history_by_id = {}
""" Dictionary - keys are browser ID's and values are the dialog's history """


def get_last_selected_directory(idval):
    """
    Return the last directory selected by a user in a dialog with the given id
    value. If there is no entry for the given id value, None is returned.

    :type idval: str, int or float
    :param idval: The value passed to a filedialog using the id keyword argument

    :rtype: str or None
    :return: The last directory opened by a dialog with the given id value, or
        None if no entry exists for the id value.
    """

    return _last_selected_directory.get(idval)


def base_file_dialog(parent="",
                     caption='Open File',
                     dir=None,
                     filter='All Files (*)',
                     selectedFilter=None,
                     options=None,
                     default_suffix=None,
                     default_filename=None,
                     accept_label='Open',
                     accept_mode=QtWidgets.QFileDialog.AcceptOpen,
                     file_mode=QtWidgets.QFileDialog.ExistingFile,
                     confirm=True,
                     custom_sidebar=True,
                     sidebar_links=None,
                     id=None):
    """
    Convenience function that creates a highly customizable file dialog

    :type parent: qwidget
    :param parent: the widget over which this dialog should be shown.

    :type caption: str
    :param caption: the name that appears in the titlebar of this dialog.

    :type dir: str
    :param dir: the initial directory displayed in this dialog. If id keyword
        is also supplied, subsequent calls will open in the last opened
        directory, which can be different from dir.

    :type filter: str
    :param filter: the list of filters that can be applied to this directory.
        the format is ``"Filetype1 (*.ex1);;Filetype2 (*.ex2 *.ex3)"``.

    :type selectedFilter: str
    :param selectedFilter: the filter used by default.  if not specified, the
        first filter in the filters string is used.

    :type options: qfiledialog.option enum
    :param options: see the qfiledialog.option and qfiledialog.setoptions
        documentation

    :type default_suffix: str
    :param default_suffix: the suffix applied to a filename if the user does
        supply one.  the suffix will have a leading '.' appended to it.

    :type default_filename: str
    :param default_filename: A default base filename to use to save files in
        save dialogs. By default, the filename field of the dialog is blank.

    :type accept_label: str
    :param accept_label: the text on the 'accept' button

    :type accept_mode: qfiledialog.acceptmode
    :param accept_mode: whether the dialog is in open or save mode.  see the
        pyqt documentation for qfiledialog.acceptmode (currently acceptopen and
        acceptsave are the two options)

    :type file_mode: qfiledialog.filemode
    :param file_mode: what the user can select.  see the pyqt documentation for
        qfiledialog.filemode (currently anyfile, existingfile, directory and
        existingfiles are the options)

    :type confirm: bool
    :param confirm: true if a confirmation dialog should be used if the user
        selects an existing file, false if not

    :type custom_sidebar: bool
    :param custom_sidebar: True if the Schrodinger sidebar should be used,
        False if the default PyQt sidebar should be used.

    :type sidebar_links: dict
    :param sidebar_links: Used to create extra links in the left-hand sidebar of
        the dialog.  The keys of the dictionary are a unique identifier for each
        link (note that 'home' and 'working' are already used), and the values are
        tuples of the form (path, name) where path and name are str, path indicates
        the path the sidebar link points to, and name is the name displayed for the
        link.

    :type id: str, int or float
    :param id: The identifier used for this dialog.  Dialogs with the same
        identifier will remember the last directory chosen by the user with any
        dialog of the same id and open in that directory.  The dir keyword parameter
        can be used to override the initial directory the dialog opens in, but the
        chosen directory will still be stored for the next time a dialog with the
        same identifier opens.  The default (no id given) is to not remember the
        chosen directory. Additionally, the id is used to keep track of
        recent places for the given file dialog.

    :rtype: list or None
    :return: list of full file pathnames selected by the user, or none if cancel
        was pressed.  Note that all pathnames are strings, and have been converted
        to platform-consistent pathnames via os.path.normpath.
    """

    global _last_selected_filter

    mydir = '.'
    if dir is not None:
        mydir = dir
    if id is not None:
        mydir = _last_selected_directory.get(id, mydir)
    dialog = FileDialog(
        parent=parent,
        caption=caption,
        directory=mydir,
        filter=filter,
        custom_sidebar=custom_sidebar,
        sidebar_links=sidebar_links)
    if default_filename:
        dialog.selectFile(default_filename)
    dialog.setAcceptMode(accept_mode)
    dialog.setFileMode(file_mode)

    # Apply the user options
    if not parent or not caption:
        # The QFileDialog class does not apply the other parameters if parent or
        # caption is not set.
        if not parent and caption:
            dialog.setWindowTitle(caption)
        dialog.setDirectory(os.path.abspath(mydir))
        dialog.setNameFilter(filter)
    if selectedFilter:
        dialog.selectNameFilter(selectedFilter)
    if options is not None:
        dialog.setOptions(options)
    if default_suffix:
        dialog.setDefaultSuffix(default_suffix)
    if not confirm:
        dialog.setOption(QFileDialog.DontConfirmOverwrite)
    dialog.setLabelText(QFileDialog.Accept, accept_label)
    if id and _history_by_id.get(id):
        dialog.setHistory(_history_by_id.get(id))

    ok = dialog.exec_()
    if ok:
        # If not cancel
        _last_selected_filter = str(dialog.selectedNameFilter())
        files = [os.path.normpath(str(x)) for x in dialog.selectedFiles()]
        if files and id is not None:
            _last_selected_directory[id] = \
                                        str(dialog.directory().absolutePath())
            _history_by_id[id] = dialog.history()
        return files

    return None


def fix_splitter(dialog):
    """
    Alters the splitter between the file pane and the directory pane so that
    both sides are visible.  Because Qt saves the state of the dialog, if the
    users moves the splitter all the way to one side or the other, all future
    dialogs will show up that way, and it can be very confusing if the file side
    isn't shown.

    :type dialog: `QtWidgets.QFileDialog`
    :param dialog: The dialog to check & fix if necessary
    """

    try:
        splitter = dialog.findChildren(QtWidgets.QSplitter)[0]
    except IndexError:
        # There should be one splitter in the Dialog - this must be a custom
        # Dialog
        return
    sizes = splitter.sizes()
    try:
        fileside = sizes[1]
        dirside = sizes[0]
    except IndexError:
        # Not the splitter we wanted - this must be a custom Dialog
        return
    if not fileside:
        sizes = [old_div(dirside, 2), old_div(dirside, 2)]
        splitter.setSizes(sizes)
    elif not dirside:
        sizes = [old_div(fileside, 2), old_div(fileside, 2)]
        splitter.setSizes(sizes)
    # Do not let file section of dialog collapse (python-1960)
    splitter.setCollapsible(1, False)


class FileDialog(QtWidgets.QFileDialog, CustomSideBarMixin):
    """
    File browser dialog with custom sidebar.

    This class name was changed from QFileDialog to FileDialog because PyQt on
    the Mac OS uses Mac native dialogs instead of the class object if the class
    name is QFileDialog.
    """

    _pytest_abort_hook = lambda self: None  # To prevent showing during tests
    getExistingDirectory = staticmethod(get_existing_directory)
    getSaveFileName = staticmethod(get_save_file_name)
    getOpenFileNames = staticmethod(get_open_file_names)
    getOpenFileName = staticmethod(get_open_file_name)

    def __init__(self,
                 parent=None,
                 caption="",
                 directory="",
                 filter='All Files (*)',
                 custom_sidebar=True,
                 sidebar_links=None):
        """
        :type parent: qwidget
        :param parent: the widget over which this dialog should be shown.  If
            not given, the Dialog will be placed by PyQt.

        :type caption: str
        :param caption: the name that appears in the titlebar of this dialog.
            If not given the title will be the default PyQt caption.

        :type directory: str
        :param directory: the initial directory displayed in this dialog,
            default is the current directory.

        :type filter: str
        :param filter: the list of filters that can be applied to this directory
            the format is ``"Filetype1 (*.ex1);;Filetype2 (*.ex2 *.ex3)"``.
            Default is all files.

        :type custom_sidebar: bool
        :param custom_sidebar: True if the Schrodinger sidebar should be used,
            False if the default PyQt sidebar should be used.

        :type sidebar_links: dict
        :param sidebar_links: Use to create extra links in the left-hand
            sidebar of the dialog.  the keys of the dictionary are a unique
            identifier for each link (note that 'home' and 'working' are already
            used), and the values are tuples of the form (path, name) where path
            and name are str, path indicates the path the sidebar link points to,
            and name is the name displayed for the link.
        """

        # Ev:98084 directory must be absolute path:
        directory = os.path.abspath(directory)
        if not parent:
            QtWidgets.QFileDialog.__init__(self)
        elif not caption:
            QtWidgets.QFileDialog.__init__(self, parent)
        else:
            QtWidgets.QFileDialog.__init__(self, parent, caption, directory,
                                           filter)

        if custom_sidebar and (not use_native_file_dialog()):
            self._setup_sidebar(sidebar_links=sidebar_links)

        # Make sure both sides of the file-chooser splitter are visible
        fix_splitter(self)
        if use_native_file_dialog():
            self.setOption(QtWidgets.QFileDialog.DontUseNativeDialog, False)

    def exec_(self):
        """
        In Python3, exec is no longer a reserved word, so PyQt now provides
        `exec` wrappings of Qt `exec` methods. Preserving `exec_` for backwards
        compatibility.
        """
        return self.exec()

    def exec(self):
        self._pytest_abort_hook()
        with qt_utils.remove_wait_cursor:
            return super().exec()


# This allows legacy scripts to still access the old class name QFileDialog
QFileDialog = FileDialog


class CustomFileDialog(FileDialog):
    """
    A File Dialog that has all contents below the file view (File name
    and File type fields) shifted down a row so that custom controls can be
    placed.
    """

    def __init__(self, *args, num_free_rows=1, **kwargs):
        super().__init__(*args, **kwargs)
        dlg_layout = self.layout()

        assert num_free_rows >= 0

        fileNameLabel = self.findChild(QtWidgets.QLabel, "fileNameLabel")
        dlg_layout.addWidget(fileNameLabel, 2 + num_free_rows, 0)

        self.file_name_edit = self.findChild(QtWidgets.QLineEdit,
                                             "fileNameEdit")
        dlg_layout.addWidget(self.file_name_edit, 2 + num_free_rows, 1)

        buttonBox = self.findChild(QtWidgets.QDialogButtonBox, "buttonBox")
        dlg_layout.addWidget(buttonBox, 2 + num_free_rows, 2, 2, 1)

        fileTypeLabel = self.findChild(QtWidgets.QLabel, "fileTypeLabel")
        dlg_layout.addWidget(fileTypeLabel, 3 + num_free_rows, 0)

        self.file_type_combo = self.findChild(QtWidgets.QComboBox,
                                              "fileTypeCombo")
        dlg_layout.addWidget(self.file_type_combo, 3 + num_free_rows, 1)


def get_windows_drive_sidebar_links():
    """
    :rtype: dict
    :return: dictionary of attached drives.  Keys are text names of the drive,
        values are tuples of (path, text name).
    """

    drives = {}
    for letter in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ':
        path = letter + ':\\'
        if os.path.exists(path):
            name = letter + ': Drive'
            drives[name] = (path, name)
    return drives


################################################
## Below code is specific to Maestro projects ##
################################################


class OpenDirAsFileDialog(MM_QProjS):
    """
    A file dialog tailored to allow the user to "open" directories such as
    projects or phase databases as if they were files.
    Usage::

        dlg = OpenDirAsFileDialog()
        project_path = dlg.getFilename()
        if project_path:
            # User accepted the dialog with a directory selection.

    """

    def __init__(self,
                 parent=None,
                 caption='Open Project',
                 directory='.',
                 accept='Open',
                 filter=MM_QProjS.MM_QPROJS_MAESTRO,
                 label='Project:'):
        """
        :param QWidget parent: Dialog parent.
        :param str caption: Dialog
        :param str directory: Directory to open in the dialog.
        :param str accept: Text for dialog accept button.
        :type filter: Directory filter (MM_QPROJS_MAESTRO, MM_QPROJS_CANVAS,
                MM_QPROJS_PHDB).
        :param filter: MM_QProjS.MMENUM_QPROJS_APP
        :param str label: Text for dialog file name label.
        """
        cloud_warning = not use_native_file_dialog()
        super().__init__(parent, filter, use_native_file_dialog(),
                         cloud_warning)
        self.setWindowTitle(caption)
        self.setAcceptOpen()
        self.setLabelText(self.Accept, accept)
        self.setLabelText(self.FileName, label)
        self.setDir(os.path.abspath(directory))

    def getFilename(self):
        """
        Open the dialog, allow the user to choose the directory and return the
        path.
        :return: Path to directory if dialog accepted else None.
        :rtype: str or NoneType.
        """

        with qt_utils.remove_wait_cursor:
            if self.exec():
                # FIXME MAE-45189: On Windows - `getSelectedFullPath` is
                #  returning path with both forward and backslash separators.
                return self.getSelectedFullPath().replace('\\', '/')
            else:
                return None


class ProjectOpenDialog(OpenDirAsFileDialog):
    """
    A file dialog tailored to opening Projects.
    """

    def __init__(self,
                 parent=None,
                 caption='Open Project',
                 directory='.',
                 accept='Open',
                 filter=MM_QProjS.MM_QPROJS_MAESTRO,
                 label='Project:'):
        # See OpenDirAsFileDialog.__init__ for documentation.
        super().__init__(
            parent=parent,
            caption=caption,
            directory=directory,
            accept=accept,
            filter=filter,
            label=label)


def get_existing_project_name(*args, **kwargs):
    """
    Convenience function to open a Open Project dialog and return the path the
    user selects.

    Parameters are passed to and documented in the `ProjectOpenDialog` class.

    :type id: str, int or float
    :param id: The identifier used for this dialog. Dialogs with the same
               identifier will remember the last directory chosen by the user
               with any dialog of the same id and open in that directory. The
               dir keyword parameter can be used to override the initial
               directory the dialog opens in, but the chosen directory will
               still be stored for the next time a dialog with the same
               identifier opens. The default (no id given) is to not remember
               the chosen directory.

    :rtype: str or None
    :return: The path to the project if the user selects one, or None if the
             user cancels the dialog
    """

    # Restore the correct starting directory if requested
    id = kwargs.pop('id', None)
    if id is not None:
        mydir = _last_selected_directory.get(id, '.')
        kwargs['directory'] = mydir

    mydialog = ProjectOpenDialog(*args, **kwargs)
    afile = mydialog.getFilename()

    # Save the ending directory if requested
    if afile and id is not None:
        _last_selected_directory[id] = str(mydialog.directory().absolutePath())

    return afile


###############################################
## Below code is specific to Phase databases ##
###############################################


class PhaseDatabaseOpenDialog(OpenDirAsFileDialog):
    """
    A file dialog for opening phase databases.
    """

    def __init__(self,
                 parent=None,
                 caption='Open Phase database',
                 directory='.',
                 accept='Open',
                 filter=MM_QProjS.MM_QPROJS_PHDB,
                 label='Project:'):
        # See OpenDirAsFileDialog.__init__ for documentation.
        super().__init__(
            parent=parent,
            caption=caption,
            directory=directory,
            accept=accept,
            filter=filter,
            label=label)


def get_existing_phase_database(*args, **kwargs):
    """
    Convenience function to open an Open Project dialog and return the path the
    user selects.

    All parameters are passed to and documented in the PhaseDatabaseOpenDialog
    class.

    :type id: str, int or float
    :param id: The identifier used for this dialog. Dialogs with the same
               identifier will remember the last directory chosen by the user
               with any dialog of the same id and open in that directory. The
               dir keyword parameter can be used to override the initial
               directory the dialog opens in, but the chosen directory will
               still be stored for the next time a dialog with the same
               identifier opens. The default (no id given) is to not remember
               the chosen directory.

    :rtype: str or None
    :return: The path to the project if the user selects one, or None if the
             user cancels the dialog
    """

    # Restore the correct starting directory if requested
    id = kwargs.pop('id', None)
    if id is not None:
        mydir = _last_selected_directory.get(id, '.')
        kwargs['directory'] = mydir

    mydialog = PhaseDatabaseOpenDialog(*args, **kwargs)
    afile = mydialog.getFilename()

    # Save the ending directory if requested
    if afile and id is not None:
        _last_selected_directory[id] = str(mydialog.directory().absolutePath())

    return afile


# For testing purposes:
if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)

    mydialog = PhaseDatabaseOpenDialog()
    print(mydialog.getFilename())

#EOF