示例#1
0
class KwargsDictDialogPage(BaseBox):
    """
    Defines the :class:`~KwargsDictDialogPage` class for the kwargs dict
    dialog.

    This class provides the tab/page in the kwargs dict dialog where the items
    of the associated kwargs dict can be viewed and modified by the user.

    """

    @e13.docstring_substitute(optional=kwargs_doc.format(
        'prism._gui.widgets.core.BaseBox'))
    def __init__(self, kwargs_dict_dialog_obj, name, std_entries,
                 banned_entries, *args, **kwargs):
        """
        Initialize an instance of the :class:`~KwargsDictDialogPage` class.

        Parameters
        ----------
        kwargs_dict_dialog_obj : :obj:`~KwargsDictDialog` object
            Instance of the :class:`~KwargsDictDialog` class that initialized
            this kwargs dict page.
        name : str
            The name of this kwargs dict page.
        std_entries : list of str
            A list of all standard entry types that this kwargs dict should
            accept.
        banned_entries : list of str
            A list of all entry types that this kwargs dict should not accept.
            Usually, these entry types are used by *PRISM* and therefore should
            not be modified by the user.

        %(optional)s

        """

        # Save provided kwargs_dict_dialog_obj
        self.entry_height = kwargs_dict_dialog_obj.entry_height
        self.name = name
        self.std_entries = sset(std_entries)
        self.banned_entries = sset(banned_entries)

        # Call super constructor
        super().__init__(*args, **kwargs)

        # Create the kwargs dict window
        self.init()

    # This function creates the kwargs dict page
    def init(self):
        """
        Sets up the kwargs dict page after it has been initialized.

        This function is mainly responsibe for creating the layout of the page;
        determining what entry types are available; and preparing for the user
        to add entries.

        """

        # Create page layout
        page_layout = QW.QVBoxLayout(self)

        # Create a grid for this layout
        self.kwargs_grid = QW.QGridLayout()
        self.kwargs_grid.setColumnStretch(1, 1)
        self.kwargs_grid.setColumnStretch(2, 2)
        page_layout.addLayout(self.kwargs_grid)

        # Add a header
        self.kwargs_grid.addWidget(QW_QLabel(""), 0, 0)
        self.kwargs_grid.addWidget(QW_QLabel("Entry type"), 0, 1)
        self.kwargs_grid.addWidget(QW_QLabel("Field value"), 0, 2)

        # Make sure that '' is not in std_entries or banned_entries
        self.std_entries.discard('')
        self.banned_entries.discard('')

        # Create list of available entry types
        self.avail_entries = sset([attr[12:] for attr in dir(self)
                                   if attr.startswith('create_type_')])

        # Convert std_entries to solely contain valid available entry types
        self.std_entries.intersection_update(self.avail_entries)
        self.std_entries.difference_update(self.banned_entries)

        # Add an 'add' button at the bottom of this layout
        add_but = QW.QToolButton()
        add_but.setFixedSize(self.entry_height, self.entry_height)
        add_but.setToolTip("Add a new entry")
        add_but.clicked.connect(self.add_editable_entry)
        self.add_but = add_but

        # If this theme has an 'add' icon, use it
        if QG.QIcon.hasThemeIcon('add'):    # pragma: no cover
            add_but.setIcon(QG.QIcon.fromTheme('add'))
        # Else, use a simple plus
        else:
            add_but.setText('+')

        # Add button to layout
        page_layout.addWidget(add_but)
        page_layout.addStretch()

        # Set a minimum width for the first grid column
        self.kwargs_grid.setColumnMinimumWidth(0, self.entry_height)

    # This function gets the dict value of a page
    def get_box_value(self):
        """
        Returns the current value of the kwargs dict page.

        Returns
        -------
        page_dict : dict
            A dict containing all valid entries that are currently on this
            kwargs dict page. Any invalid entries (banned or empty ones) are
            ignored.

        """

        # Create an empty dict to hold the values in
        page_dict = sdict()

        # Loop over all items in grid and save them to page_dict
        for row in range(1, self.kwargs_grid.rowCount()):
            # Obtain item that should contain the entry_type in this row
            entry_type_item = self.kwargs_grid.itemAtPosition(row, 1)

            # If entry_type_item is None, this row contains no items
            if entry_type_item is None:
                continue

            # Obtain the entry_type
            entry_type = get_box_value(entry_type_item.widget())

            # If the entry_type is empty or banned, skip this row
            if not entry_type or entry_type in self.banned_entries:
                continue

            # Obtain the value of the corresponding field box
            field_value = get_box_value(
                self.kwargs_grid.itemAtPosition(row, 2).widget())

            # Add this to the dict
            page_dict[entry_type] = field_value

        # Return page_dict
        return(page_dict)

    # This function sets the dict value of a page
    def set_box_value(self, page_dict):
        """
        Sets the current value of the kwargs dict page to `page_dict`.

        Parameters
        ----------
        page_dict : dict
            A dict containing all entries that this kwargs dict page must have.
            Current entries that are also in `page_dict` will be reused,
            otherwise they are deleted.

        """

        # Make empty dict containing all current valid entries
        cur_entry_dict = sdict()

        # Remove all entries from kwargs_grid
        for row in range(1, self.kwargs_grid.rowCount()):
            # Obtain item that should contain the entry_type in this row
            entry_type_item = self.kwargs_grid.itemAtPosition(row, 1)

            # If entry_type_item is None, this row contains no items
            if entry_type_item is None:
                continue

            # Obtain the entry_type
            entry_type = get_box_value(entry_type_item.widget())

            # Delete this entry if not in page_dict or if it is not allowed
            if(entry_type not in page_dict or not entry_type or
               entry_type in self.banned_entries):
                self.remove_editable_entry(entry_type_item.widget())
                continue

            # If this entry appears multiple times, delete its previous entry
            if entry_type in cur_entry_dict:
                for item in cur_entry_dict[entry_type]:
                    item.widget().close()
                    del item
                cur_entry_dict.pop(entry_type)

            # Add this entry to cur_entry_dict
            cur_entry_dict[entry_type] =\
                [self.kwargs_grid.takeAt(3) for _ in range(3)]

        # Loop over all items in page_dict and act accordingly
        for row, (entry_type, field_value) in enumerate(page_dict.items(), 1):
            # Check if this entry_type already existed
            if entry_type in cur_entry_dict:
                # If so, put it back into kwargs_grid
                for col, item in enumerate(cur_entry_dict[entry_type]):
                    self.kwargs_grid.addItem(item, row, col)
            else:
                # If not, add it to kwargs_grid
                self.add_editable_entry()

                # Set this new entry to the proper type
                set_box_value(self.kwargs_grid.itemAtPosition(row, 1).widget(),
                              entry_type)

            # Set the value of the corresponding field
            set_box_value(self.kwargs_grid.itemAtPosition(row, 2).widget(),
                          field_value)

    # This function creates an editable entry
    @QC.Slot()
    @e13.docstring_substitute(qt_slot=qt_slot_doc)
    def add_editable_entry(self):
        """
        Adds a new editable entry to the kwargs dict page, which allows for the
        user to edit the contents of the kwargs dict.

        %(qt_slot)s

        """

        # Create a combobox with different standard kwargs
        kwargs_box = QW_QEditableComboBox()
        kwargs_box.setFixedHeight(self.entry_height)
        kwargs_box.addItem('')
        kwargs_box.addItems(self.std_entries)
        kwargs_box.setToolTip("Select a standard type for this entry or add "
                              "it manually")
        kwargs_box.currentTextChanged.connect(
            lambda x: self.entry_type_selected(x, kwargs_box))

        # Create a delete button
        delete_but = QW.QToolButton()
        delete_but.setFixedSize(self.entry_height, self.entry_height)
        delete_but.setToolTip("Delete this entry")
        delete_but.clicked.connect(
            lambda: self.remove_editable_entry(kwargs_box))

        # If this theme has a 'remove' icon, use it
        if QG.QIcon.hasThemeIcon('remove'):     # pragma: no cover
            delete_but.setIcon(QG.QIcon.fromTheme('remove'))
        # Else, use a simple cross
        else:
            delete_but.setText('X')

        # Determine the number of entries currently in kwargs_grid
        next_row = self.kwargs_grid.getItemPosition(
            self.kwargs_grid.count()-1)[0]+1

        # Make a new editable entry
        self.kwargs_grid.addWidget(delete_but, next_row, 0)
        self.kwargs_grid.addWidget(kwargs_box, next_row, 1)
        self.kwargs_grid.addWidget(QW.QLabel(''), next_row, 2)

    # This function deletes an editable entry
    @QC.Slot(QW.QComboBox)
    @e13.docstring_substitute(qt_slot=qt_slot_doc)
    def remove_editable_entry(self, kwargs_box):
        """
        Removes the editable entry associated with the provided `kwargs_box`.

        %(qt_slot)s

        Parameters
        ----------
        kwargs_box : \
            :obj:`~prism._gui.widgets.base_widgets.QW_QEditableComboBox` object
            The combobox that is used for setting the entry type of this entry.

        """

        # Determine at what index the provided kwargs_box currently is
        index = self.kwargs_grid.indexOf(kwargs_box)

        # As every row contains 3 items, remove item 3 times at this index-1
        for _ in range(3):
            # Take the current layoutitem at this index-1
            item = self.kwargs_grid.takeAt(index-1)

            # Close the widget in this item and delete the item
            item.widget().close()
            del item

    # This function is called when an item in the combobox is selected
    # TODO: Make sure that two fields cannot have the same name
    @QC.Slot(str, QW.QComboBox)
    def entry_type_selected(self, entry_type, kwargs_box):
        """
        Qt slot that modifies the field box associated with the provided
        `kwargs_box` to given `entry_type`.

        Parameters
        ----------
        entry_type : str
            The entry type that is requested for the field box.
        kwargs_box : \
            :obj:`~prism._gui.widgets.base_widgets.QW_QEditableComboBox` object
            The combobox that is used for setting the entry type of this entry.

        """

        # Determine at what index the provided kwargs_box currently is
        index = self.kwargs_grid.indexOf(kwargs_box)

        # Retrieve what the current field_box is
        cur_box = self.kwargs_grid.itemAt(index+1).widget()

        # Check what entry_type is given and act accordingly
        if not entry_type:
            # If '' is selected, use an empty label widget
            field_box = QW.QLabel('')
        elif entry_type in self.banned_entries:
            # If one of the banned types is selected, show a warning message
            warn_msg = (r"<b><i>%s</i></b> is a reserved or banned entry type!"
                        % (entry_type))
            field_box = QW.QLabel(warn_msg)
        elif entry_type in self.std_entries:
            # If one of the standard types is selected, create its box
            field_box = getattr(self, 'create_type_%s' % (entry_type))()
        else:
            # If an unknown type is given, add default box if not used already
            if isinstance(cur_box, DefaultBox):
                return
            else:
                field_box = DefaultBox()

        # Set the height of this box
        field_box.setFixedHeight(self.entry_height)

        # Replace current field_box with new field_box
        cur_item = self.kwargs_grid.replaceWidget(cur_box, field_box)
        cur_item.widget().close()
        del cur_item

    # This function creates an alpha box
    @e13.docstring_append(create_type_doc.format('alpha'))
    def create_type_alpha(self):
        # Make double spinbox for alpha
        alpha_box = QW_QDoubleSpinBox()
        alpha_box.setRange(0, 1)
        set_box_value(alpha_box, 1)
        alpha_box.setToolTip("Alpha value to use for the plotted data")
        return(alpha_box)

    # This function creates a cmap box
    @e13.docstring_append(create_type_doc.format('cmap'))
    def create_type_cmap(self):
        return(ColorMapBox())

    # This function creates a color picker box
    @e13.docstring_append(create_type_doc.format('color'))
    def create_type_color(self):
        return(ColorBox())

    # This function creates a dpi box
    @e13.docstring_append(create_type_doc.format('dpi'))
    def create_type_dpi(self):
        # Make spinbox for dpi
        dpi_box = QW_QSpinBox()
        dpi_box.setRange(1, 9999999)
        set_box_value(dpi_box, rcParams['figure.dpi'])
        dpi_box.setToolTip("DPI (dots per inch) to use for the projection "
                           "figure")
        return(dpi_box)

    # This function creates a linestyle box
    @e13.docstring_append(create_type_doc.format('linestyle'))
    def create_type_linestyle(self):
        # Obtain list with all supported linestyles
        linestyles_lst = [(key, value[6:]) for key, value in lineStyles.items()
                          if value != '_draw_nothing']
        linestyles_lst.sort(key=lambda x: x[0])

        # Make combobox for linestyles
        linestyle_box = QW_QComboBox()
        for i, (linestyle, tooltip) in enumerate(linestyles_lst):
            linestyle_box.addItem(linestyle)
            linestyle_box.setItemData(i, tooltip, QC.Qt.ToolTipRole)
        set_box_value(linestyle_box, rcParams['lines.linestyle'])
        linestyle_box.setToolTip("Linestyle to be used for the corresponding "
                                 "plot type")
        return(linestyle_box)

    # This function creates a linewidth box
    @e13.docstring_append(create_type_doc.format('linewidth'))
    def create_type_linewidth(self):
        # Make a double spinbox for linewidth
        linewidth_box = QW_QDoubleSpinBox()
        linewidth_box.setRange(0, 9999999)
        linewidth_box.setSuffix(" pts")
        set_box_value(linewidth_box, rcParams['lines.linewidth'])
        linewidth_box.setToolTip("Width of the plotted line")
        return(linewidth_box)

    # This function creates a marker box
    @e13.docstring_append(create_type_doc.format('marker'))
    def create_type_marker(self):
        # Obtain list with all supported markers
        markers_lst = [(key, value) for key, value in lineMarkers.items()
                       if(value != 'nothing' and isinstance(key, str))]
        markers_lst.append(('', 'nothing'))
        markers_lst.sort(key=lambda x: x[0])

        # Make combobox for markers
        marker_box = QW_QComboBox()
        for i, (marker, tooltip) in enumerate(markers_lst):
            marker_box.addItem(marker)
            marker_box.setItemData(i, tooltip, QC.Qt.ToolTipRole)
        set_box_value(marker_box, rcParams['lines.marker'])
        marker_box.setToolTip("Marker to be used for the corresponding plot "
                              "type")
        return(marker_box)

    # This function creates a markersize box
    @e13.docstring_append(create_type_doc.format('markersize'))
    def create_type_markersize(self):
        # Make a double spinbox for markersize
        markersize_box = QW_QDoubleSpinBox()
        markersize_box.setRange(0, 9999999)
        markersize_box.setSuffix(" pts")
        markersize_box.setToolTip("Size of the plotted markers")
        set_box_value(markersize_box, rcParams['lines.markersize'])
        return(markersize_box)

    # This function creates a fh_arrowlength box
    @e13.docstring_append(create_type_doc.format('fh_arrowlength'))
    def create_type_fh_arrowlength(self):
        # Make a double spinbox for arrow head length
        head_length_box = QW_QDoubleSpinBox()
        head_length_box.setRange(0, 9999999)
        head_length_box.setDecimals(4)
        head_length_box.setToolTip("Scaling factor for the length of the "
                                   "plotted arrow head")
        return(head_length_box)

    # This function creates a ft_arrowlength box
    @e13.docstring_append(create_type_doc.format('ft_arrowlength'))
    def create_type_ft_arrowlength(self):
        # Make a double spinbox for arrow tail length
        tail_length_box = QW_QDoubleSpinBox()
        tail_length_box.setRange(0, 9999999)
        tail_length_box.setDecimals(2)
        tail_length_box.setToolTip("Length of the plotted arrow tail relative "
                                   "to the length of its head")
        return(tail_length_box)

    # This function creates a fh_arrowwidth box
    @e13.docstring_append(create_type_doc.format('fh_arrowwidth'))
    def create_type_fh_arrowwidth(self):
        # Make a double spinbox for arrow head width
        head_length_box = QW_QDoubleSpinBox()
        head_length_box.setRange(0, 9999999)
        head_length_box.setDecimals(4)
        head_length_box.setToolTip("Scaling factor for the width of the "
                                   "plotted arrow head")
        return(head_length_box)

    # This function creates a ft_arrowwidth box
    @e13.docstring_append(create_type_doc.format('ft_arrowwidth'))
    def create_type_ft_arrowwidth(self):
        # Make a double spinbox for arrow tail width
        tail_length_box = QW_QDoubleSpinBox()
        tail_length_box.setRange(0, 9999999)
        tail_length_box.setDecimals(4)
        tail_length_box.setToolTip("Scaling factor for the width of the "
                                   "plotted arrow tail")
        return(tail_length_box)

    # This function creates a rel_xpos box
    @e13.docstring_append(create_type_doc.format('rel_xpos'))
    def create_type_rel_xpos(self):
        # Make a double spinbox for arrow relative x-position
        x_pos_box = QW_QDoubleSpinBox()
        x_pos_box.setRange(0, 1)
        x_pos_box.setDecimals(4)
        x_pos_box.setToolTip("Relative x-position of the arrow.")
        return(x_pos_box)

    # This function creates a rel_xpos box
    @e13.docstring_append(create_type_doc.format('rel_ypos'))
    def create_type_rel_ypos(self):
        # Make a double spinbox for arrow relative y-position
        y_pos_box = QW_QDoubleSpinBox()
        y_pos_box.setRange(0, 1)
        y_pos_box.setDecimals(4)
        y_pos_box.setToolTip("Relative y-position of the arrow.")
        return(y_pos_box)

    # This function creates a scale box
    def create_type_scale(self, axis):
        """
        Base function for creating the entry types 'xscale' and 'yscale'.

        """

        # Make a combobox for scale
        scale_box = QW_QComboBox()
        scale_box.addItems(['linear', 'log'])
        scale_box.setToolTip("Scale type to use on the %s-axis" % (axis))
        return(scale_box)

    # This function creates a xscale box
    @e13.docstring_append(create_type_doc.format('xscale'))
    def create_type_xscale(self):
        return(self.create_type_scale('x'))

    # This function creates a yscale box
    @e13.docstring_append(create_type_doc.format('yscale'))
    def create_type_yscale(self):
        return(self.create_type_scale('y'))
示例#2
0
class KwargsDictBoxLayout(QW.QHBoxLayout):
    """
    Defines the :class:`~KwargsDictBoxLayout` class for the preferences window.

    This class provides the options entry box that gives the user access to a
    separate window, where the various different keyword dicts can be modified.

    """

    # This function creates an editable list of input boxes for the kwargs
    @e13.docstring_substitute(optional=kwargs_doc.format(
        'PyQt5.QtWidgets.QHBoxLayout'))
    def __init__(self, options_dialog_obj, *args, **kwargs):
        """
        Initialize an instance of the :class:`~KwargsDictBoxLayout` class.

        Parameters
        ----------
        options_dialog_obj : \
            :obj:`~prism._gui.widgets.preferences.OptionsDialog` object
            Instance of the
            :class:`~prism._gui.widgets.preferences.OptionsDialog` class that
            acts as the parent of the :class:`~KwargsDictDialog` this layout
            creates.

        %(optional)s

        """

        # Save provided options_dialog_obj
        self.options = options_dialog_obj

        # Call super constructor
        super().__init__(*args, **kwargs)

        # Create the kwargs dict window
        self.init()

    # This function creates the kwargs dict window
    def init(self):
        """
        Sets up the box layout after it has been initialized.

        This function is mainly responsible for initializing the
        :class:`~KwargsDictDialog` class and binding it.

        """

        # Initialize the window for the kwargs dict
        self.dict_dialog = KwargsDictDialog(self.options)

        # Add a view button
        view_but = QW.QPushButton('View')
        view_but.setToolTip("View/edit the projection keyword dicts")
        view_but.setSizePolicy(QW.QSizePolicy.Fixed, QW.QSizePolicy.Fixed)
        view_but.clicked.connect(self.dict_dialog)
        self.view_but = view_but
        self.addWidget(view_but)

    # This function calls the add_page()-method of dict_dialog
    def add_dict(self, *args, **kwargs):
        """
        Adds a new kwargs dict to the box layout, by calling the
        :meth:`~KwargsDictDialog.add_page` method using the provided `args` and
        `kwargs`.

        """

        self.dict_dialog.add_page(*args, **kwargs)
示例#3
0
class OverviewDockWidget(QW.QDockWidget):
    """
    Defines the :class:`~OverviewDockWidget` class for the Projection GUI.

    This class provides the user with the ability to quickly create; draw;
    view; and save projection figures.

    """

    # TODO: Allow for the lists to be sorted differently?
    @docstring_substitute(
        optional=kwargs_doc.format('PyQt5.QtWidgets.QDockWidget'))
    def __init__(self, main_window_obj, *args, **kwargs):
        """
        Initialize an instance of the :class:`~OverviewDockWidget` class.

        Parameters
        ----------
        main_window_obj : :obj:`~prism._gui.widgets.MainViewerWindow` object
            Instance of the :class:`~prism._gui.widgets.MainViewerWindow` class
            that acts as the parent of this dock widget.

        %(optional)s

        """

        # Save provided MainWindow object
        self.main = main_window_obj
        self.pipe = self.main.pipe
        self.set_proj_attr = self.main.set_proj_attr
        self.all_set_proj_attr = self.main.all_set_proj_attr
        self.get_proj_attr = self.main.get_proj_attr
        self.call_proj_attr = self.main.call_proj_attr
        self.all_call_proj_attr = self.main.all_call_proj_attr

        # Call the super constructor
        super().__init__("Overview", self.main, *args, **kwargs)

        # Create the overview widget
        self.init()

    # This function is called when the main window is closed
    def closeEvent(self, *args, **kwargs):
        """
        Special :meth:`~PyQt5.QtWidgets.QWidget.closeEvent` event that
        automatically performs some clean-up operations before the overview
        dock widget closes.

        """

        # Close all currently opened figures and subwindows
        for fig, subwindow in self.proj_fig_registry.values():
            plt.close(fig)
            subwindow.close()

        # Close the projection overview
        super().closeEvent(*args, **kwargs)

    # This function creates the projection overview
    def init(self):
        """
        Sets up the projection overview dock widget after it has been
        initialized.

        This function is mainly responsible for creating the different overview
        lists and menus that allow the user to manipulate projection figures.

        """

        # Create an overview
        overview_widget = QW.QWidget()
        self.proj_overview = QW.QVBoxLayout()
        overview_widget.setLayout(self.proj_overview)
        self.setWidget(overview_widget)

        # Set the contents margins at the bottom to zero
        contents_margins = self.proj_overview.getContentsMargins()
        self.proj_overview.setContentsMargins(*contents_margins[:3], 0)

        # Create empty dict containing all projection figure instances
        self.proj_fig_registry = {}

        # Make lists of all hcubes and their names
        self.hcubes = list(self.get_proj_attr('hcubes'))
        self.names = [
            self.call_proj_attr('get_hcube_name', hcube)
            for hcube in self.hcubes
        ]

        # Divide all hcubes up into three different lists
        # Drawn; available; unavailable
        unavail_hcubes = [
            self.call_proj_attr('get_hcube_name', hcube)
            for hcube in self.get_proj_attr('create_hcubes')
        ]
        avail_hcubes = [
            name for name in self.names if name not in unavail_hcubes
        ]
        drawn_hcubes = []

        # DRAWN PROJECTIONS
        # Add list for drawn projections
        self.proj_list_d = OverviewListWidget(
            hcubes_list=drawn_hcubes,
            status_tip="Lists all projections that have been drawn",
            context_menu=self.show_drawn_context_menu,
            activated=self.show_projection_figures)
        self.proj_overview.addWidget(QW.QLabel("Drawn:"))
        self.proj_overview.addWidget(self.proj_list_d)
        self.create_drawn_context_menu()

        # AVAILABLE PROJECTIONS
        # Add list for available projections
        self.proj_list_a = OverviewListWidget(
            hcubes_list=avail_hcubes,
            status_tip=("Lists all projections that have been calculated but "
                        "not drawn"),
            context_menu=self.show_available_context_menu,
            activated=self.draw_projection_figures)
        self.proj_overview.addWidget(QW.QLabel("Available:"))
        self.proj_overview.addWidget(self.proj_list_a)
        self.create_available_context_menu()

        # UNAVAILABLE PROJECTIONS
        # Add list for projections that can be created
        self.proj_list_u = OverviewListWidget(
            hcubes_list=unavail_hcubes,
            status_tip="Lists all projections that have not been calculated",
            context_menu=self.show_unavailable_context_menu,
            activated=self.create_projection_figures)
        self.proj_overview.addWidget(QW.QLabel("Unavailable:"))
        self.proj_overview.addWidget(self.proj_list_u)
        self.create_unavailable_context_menu()

    # This function creates the context menu for drawn projections
    def create_drawn_context_menu(self):
        """
        Creates the context (right-click) menu for the 'Drawn' overview list.

        This menu contains all actions that are available for drawn projection
        figures.

        """

        # Create context menu
        menu = QW_QMenu(self, 'Drawn')

        # Add show action to menu
        show_act = QW_QAction(self,
                              'S&how',
                              statustip="Show selected projection figure(s)",
                              triggered=self.show_projection_figures)
        menu.addAction(show_act)

        # Add save action to menu
        save_act = QW_QAction(
            self,
            '&Save',
            statustip="Save selected projection figure(s) to file",
            triggered=self.save_projection_figures)
        menu.addAction(save_act)

        # Add save as action to menu
        save_as_act = QW_QAction(
            self,
            'Save &as...',
            statustip="Save selected projection figure(s) to chosen file",
            triggered=self.save_as_projection_figures)
        menu.addAction(save_as_act)

        # Add redraw action to menu
        redraw_act = QW_QAction(
            self,
            '&Redraw',
            statustip="Redraw selected projection figure(s)",
            triggered=self.redraw_projection_figures)
        menu.addAction(redraw_act)

        # Add close action to menu
        close_act = QW_QAction(self,
                               '&Close',
                               statustip="Close selected projection figure(s)",
                               triggered=self.close_projection_figures)
        menu.addAction(close_act)

        # Add details action to menu (single item only)
        self.details_u_act = QW_QAction(
            self,
            'De&tails',
            statustip="Show details about selected projection figure",
            triggered=self.details_drawn_projection_figure)
        menu.addAction(self.details_u_act)

        # Save made menu as an attribute
        self.context_menu_d = menu

    # This function shows the context menu for drawn projections
    @QC.Slot()
    @docstring_substitute(qt_slot=qt_slot_doc)
    def show_drawn_context_menu(self):
        """
        Shows the 'Drawn' context menu, giving the user access to its actions.

        %(qt_slot)s

        """

        # Calculate number of selected items
        n_items = len(self.proj_list_d.selectedItems())

        # If there is currently at least one item selected, show context menu
        if n_items:
            # If there is exactly one item selected, enable details
            self.details_u_act.setEnabled(n_items == 1)

            # Show context menu
            self.context_menu_d.popup(QG.QCursor.pos())

    # This function creates the context menu for available projections
    def create_available_context_menu(self):
        """
        Creates the context (right-click) menu for the 'Available' overview
        list.

        This menu contains all actions that are available for created (but not
        drawn) projection figures.

        """

        # Create context menu
        menu = QW_QMenu(self, 'Available')

        # Add draw action to menu
        draw_act = QW_QAction(self,
                              '&Draw',
                              statustip="Draw selected projection figure(s)",
                              triggered=self.draw_projection_figures)
        menu.addAction(draw_act)

        # Add draw&save action to menu
        draw_save_act = QW_QAction(
            self,
            'Draw && &Save',
            statustip="Draw & save selected projection figure(s)",
            triggered=self.draw_save_projection_figures)
        menu.addAction(draw_save_act)

        # Add recreate action to menu
        recreate_act = QW_QAction(
            self,
            '&Recreate',
            statustip="Recreate selected projection figure(s)",
            triggered=self.recreate_projection_figures)
        menu.addAction(recreate_act)

        # Add delete action to menu
        delete_act = QW_QAction(
            self,
            'D&elete',
            statustip="Delete selected projection figure(s)",
            triggered=self.delete_projection_figures)
        menu.addAction(delete_act)

        # Add details action to menu (single item only)
        self.details_a_act = QW_QAction(
            self,
            'De&tails',
            statustip="Show details about selected projection figure",
            triggered=self.details_available_projection_figure)
        menu.addAction(self.details_a_act)

        # Save made menu as an attribute
        self.context_menu_a = menu

    # This function shows the context menu for available projections
    @QC.Slot()
    @docstring_substitute(qt_slot=qt_slot_doc)
    def show_available_context_menu(self):
        """
        Shows the 'Available' context menu, giving the user access to its
        actions.

        %(qt_slot)s

        """

        # Calculate number of selected items
        n_items = len(self.proj_list_a.selectedItems())

        # If there is currently at least one item selected, show context menu
        if n_items:
            # If there is exactly one item selected, enable details
            self.details_a_act.setEnabled(n_items == 1)

            # Show context menu
            self.context_menu_a.popup(QG.QCursor.pos())

    # This function creates the context menu for unavailable projections
    def create_unavailable_context_menu(self):
        """
        Creates the context (right-click) menu for the 'Unavailable' overview
        list.

        This menu contains all actions that are available for non-existing
        projection figures.

        """

        # Create context menu
        menu = QW_QMenu(self, 'Unavailable')

        # Add create action to menu
        create_act = QW_QAction(
            self,
            '&Create',
            statustip="Create selected projection figure(s)",
            triggered=self.create_projection_figures)
        menu.addAction(create_act)

        # Add create&draw action to menu
        create_draw_act = QW_QAction(
            self,
            'Create && &Draw',
            statustip="Create & draw selected projection figure(s)",
            triggered=self.create_draw_projection_figures)
        menu.addAction(create_draw_act)

        # Add create, draw & save action to menu
        create_draw_save_act = QW_QAction(
            self,
            'Create, Draw && &Save',
            statustip="Create, draw & save selected projection figure(s)",
            triggered=self.create_draw_save_projection_figures)
        menu.addAction(create_draw_save_act)

        # Save made menu as an attribute
        self.context_menu_u = menu

    # This function shows the context menu for unavailable projections
    @QC.Slot()
    @docstring_substitute(qt_slot=qt_slot_doc)
    def show_unavailable_context_menu(self):
        """
        Shows the 'Unavailable' context menu, giving the user access to its
        actions.

        %(qt_slot)s

        """

        # If there is currently at least one item selected, show context menu
        if self.proj_list_u.selectedItems():
            self.context_menu_u.popup(QG.QCursor.pos())

    # This function shows a list of projection figures in the viewing area
    @QC.Slot()
    @QC.Slot(list)
    @docstring_substitute(qt_slot=qt_slot_doc,
                          optional=list_items_optional_doc)
    def show_projection_figures(self, list_items=None):
        """
        Retrieves the projection figures requested in the provided `list_items`
        and shows them in the projection viewing area.

        %(qt_slot)s

        %(optional)s

        """

        # Obtain the list_items
        if list_items is None:
            list_items = self.proj_list_d.selectedItems()

        # Loop over all items in list_items
        for list_item in list_items:
            # Retrieve text of list_item
            hcube_name = list_item.text()

            # Obtain the corresponding figure and subwindow
            fig, subwindow = self.proj_fig_registry[hcube_name]

            # If subwindow is None, create a new one
            if subwindow is None:
                # Create a new subwindow
                subwindow = QW.QMdiSubWindow(self.main.area_dock.proj_area)
                subwindow.setWindowTitle(hcube_name)

                # Set a few properties of the subwindow
                # TODO: Make subwindow frameless when not being hovered
                subwindow.setOption(QW.QMdiSubWindow.RubberBandResize)

                # Add subwindow to registry
                self.proj_fig_registry[hcube_name][1] = subwindow

            # If subwindow is currently not visible, create a canvas for it
            if not subwindow.isVisible():
                # Create a FigureCanvas instance
                canvas = FigureCanvas(fig)

                # Add canvas to subwindow
                subwindow.setWidget(canvas)

            # Add new subwindow to viewing area if not shown before
            if subwindow not in self.main.area_dock.proj_area.subWindowList():
                self.main.area_dock.proj_area.addSubWindow(subwindow)

            # Show subwindow
            subwindow.showNormal()
            subwindow.setFocus()

        # If auto_tile is set to True, tile all the windows
        if self.main.get_option('auto_tile'):
            self.main.area_dock.proj_area.tileSubWindows()

    # This function closes a list of projection figures permanently
    @QC.Slot()
    @QC.Slot(list)
    @docstring_substitute(qt_slot=qt_slot_doc,
                          optional=list_items_optional_doc)
    def close_projection_figures(self, list_items=None):
        """
        Retrieves the projection figures requested in the provided `list_items`
        and closes their :obj:`~matplotlib.figure.Figure` objects.

        %(qt_slot)s

        %(optional)s

        """

        # Obtain the list_items
        if list_items is None:
            list_items = self.proj_list_d.selectedItems()

        # Loop over all items in list_items
        for list_item in list_items:
            # Retrieve text of list_item
            hcube_name = list_item.text()

            # Pop the figure from the registry
            fig, subwindow = self.proj_fig_registry.pop(hcube_name)

            # Close the figure, canvas and subwindow
            plt.close(fig)
            subwindow.close()

            # Move figure from drawn to available
            item = self.proj_list_d.takeItem(self.proj_list_d.row(list_item))
            self.proj_list_a.addItem(item)

    # This function draws a list of projection figures
    # OPTIMIZE: Reshaping a 3D projection figure takes up to 15 seconds
    # TODO: Figure out if there is a way to make a figure static, and only
    # resize when explicitly told to do so
    @QC.Slot()
    @QC.Slot(list)
    @docstring_substitute(qt_slot=qt_slot_doc,
                          optional=list_items_optional_doc)
    def draw_projection_figures(self, list_items=None):
        """
        Retrieves the projection figures requested in the provided `list_items`
        and draws them, creating their :class:`~matplotlib.figure.Figure`
        instances.

        If the `auto_show` option is *True*, drawn figures will be shown
        automatically as well.

        %(qt_slot)s

        %(optional)s

        """

        # Obtain the list_items
        if list_items is None:
            list_items = self.proj_list_a.selectedItems()

        # Draw projections
        result = self.use_progress_dialog("Drawing projection figures...",
                                          self._draw_projection_figure,
                                          list_items)

        # Show all drawn projection figures if the dialog was not cancelled
        if result and self.main.get_option('auto_show'):
            self.show_projection_figures(list_items)

        # Return result
        return (result)

    # This function draws a projection figure
    def _draw_projection_figure(self, list_item):
        """
        Draws the projection figure requested in the provided `list_item`,
        creating its :class:`~matplotlib.figure.Figure` instance.

        This function is used iteratively by :meth:`~draw_projection_figures`.

        Parameters
        ----------
        list_item : :obj:`~PyQt5.QtWidgets.QListWidgetItem` object
            The item that contains the requested projection figure.

        """

        # Retrieve text of list_item
        hcube_name = list_item.text()
        hcube = self.hcubes[self.names.index(hcube_name)]

        # Load in the data corresponding to the requested figure
        impl_min, impl_los, proj_res, _ =\
            self.call_proj_attr('get_proj_data', hcube)

        # Call the proper function for drawing the projection figure
        if (len(hcube) == 2):
            fig = self.call_proj_attr('draw_2D_proj_fig', hcube, impl_min,
                                      impl_los, proj_res)
        else:
            fig = self.call_proj_attr('draw_3D_proj_fig', hcube, impl_min,
                                      impl_los, proj_res)

        # Register figure in the registry
        self.proj_fig_registry[hcube_name] = [fig, None]

        # Move figure from available to drawn
        item = self.proj_list_a.takeItem(self.proj_list_a.row(list_item))
        self.proj_list_d.addItem(item)

    # This function deletes a list of projection figures
    # TODO: Avoid reimplementing the __get_req_hcubes() logic here
    @QC.Slot()
    @QC.Slot(list)
    @docstring_substitute(qt_slot=qt_slot_doc,
                          optional=list_items_optional_doc)
    def delete_projection_figures(self,
                                  list_items=None,
                                  *,
                                  skip_warning=False):
        """
        Retrieves the projection figures requested in the provided `list_items`
        and delete them, permanently removing their corresponding projection
        data.

        %(qt_slot)s

        %(optional)s
        skip_warning : bool. Default: False
            Whether or not to skip showing the warning asking the user if they
            are sure they want to permanently delete all items in `list_items`.
            If *True*, the answer is taken to be *True*.

        """

        # Obtain the list_items
        if list_items is None:
            list_items = self.proj_list_a.selectedItems()

        # If skip_warning is False, ask the user if they really want this
        if not skip_warning:
            button_clicked = QW.QMessageBox.warning(
                self, "WARNING: Delete projection(s)",
                ("Are you sure you want to delete the selected projection "
                 "figure(s)? (<i>Note: This action is irreversible!</i>)"),
                QW.QMessageBox.Yes | QW.QMessageBox.No, QW.QMessageBox.No)
        # Else, this answer is always yes
        else:
            button_clicked = QW.QMessageBox.Yes

        # If the answer is yes, loop over all items in list_items
        if (button_clicked == QW.QMessageBox.Yes):
            for list_item in list_items:
                # Retrieve text of list_item
                hcube_name = list_item.text()
                hcube = self.hcubes[self.names.index(hcube_name)]

                # Retrieve the emul_i of this hcube
                emul_i = hcube[0]

                # Open hdf5-file
                with self.pipe._File('r+', None) as file:
                    # Remove the data belonging to this hcube
                    del file['%i/proj_hcube/%s' % (emul_i, hcube_name)]

                # Try to remove figures as well
                fig_path, fig_path_s =\
                    self.call_proj_attr('get_fig_path', hcube)
                if path.exists(fig_path):
                    os.remove(fig_path)
                if path.exists(fig_path_s):
                    os.remove(fig_path_s)

                # Move figure from available to unavailable
                item = self.proj_list_a.takeItem(
                    self.proj_list_a.row(list_item))
                self.proj_list_u.addItem(item)

    # This function creates a list of projection figures
    @QC.Slot()
    @QC.Slot(list)
    @docstring_substitute(qt_slot=qt_slot_doc,
                          optional=list_items_optional_doc)
    def create_projection_figures(self, list_items=None):
        """
        Retrieves the projection figures requested in the provided `list_items`
        and creates them, calculating their corresponding projection data.

        %(qt_slot)s

        %(optional)s

        """

        # Obtain the list_items
        if list_items is None:
            list_items = self.proj_list_u.selectedItems()

        # Create projections
        result = self.use_progress_dialog("Creating projection figures...",
                                          self._create_projection_figure,
                                          list_items)

        # Return result
        return (result)

    # This function creates a projection figure
    def _create_projection_figure(self, list_item):
        """
        Creates the projection figure requested in the provided `list_item`,
        calculating its projection data.

        This function is used iteratively by
        :meth:`~create_projection_figures`.

        Parameters
        ----------
        list_item : :obj:`~PyQt5.QtWidgets.QListWidgetItem` object
            The item that contains the requested projection figure.

        """

        # Retrieve text of list_item
        hcube_name = list_item.text()
        hcube = self.hcubes[self.names.index(hcube_name)]

        # Calculate projection data
        _, _ = self.all_call_proj_attr('analyze_proj_hcube', hcube)

        # Move figure from unavailable to available
        item = self.proj_list_u.takeItem(self.proj_list_u.row(list_item))
        self.proj_list_a.addItem(item)

    # This function saves a list of projection figures to file
    @QC.Slot()
    @QC.Slot(list)
    @docstring_substitute(qt_slot=qt_slot_doc,
                          optional=list_items_optional_doc)
    def save_projection_figures(self, list_items=None, *, choose=False):
        """
        Retrieves the projection figures requested in the provided `list_items`
        and saves their :obj:`~matplotlib.figure.Figure` objects.

        %(qt_slot)s

        %(optional)s
        choose : bool. Default: False
            Whether or not the user is allowed to choose where the projection
            figure is saved to.
            If *False*, it uses the default filename as defined by
            :meth:`~prism.Pipeline._Projection__get_fig_path`.

        """

        # Obtain the list_items
        if list_items is None:
            list_items = self.proj_list_d.selectedItems()

        # Loop over all items in list_items
        for list_item in list_items:
            # Retrieve text of list_item
            hcube_name = list_item.text()
            hcube = self.hcubes[self.names.index(hcube_name)]

            # Obtain the corresponding figure
            fig, _ = self.proj_fig_registry[hcube_name]

            # Obtain the default figure path
            fig_paths = self.call_proj_attr('get_fig_path', hcube)
            fig_path = fig_paths[self.get_proj_attr('smooth')]

            # If choose, save using non-default figure path
            if choose:
                # Get the supported filetypes
                filetypes = FigureCanvas.get_supported_filetypes_grouped()

                # Get dict of all supported file extensions in MPL
                ext_dict = sdict()
                for name, exts in filetypes.items():
                    ext_dict[name] = ' '.join(['*.%s' % (ext) for ext in exts])

                # Set default extension
                default_ext = '*.png'

                # Initialize empty list of filters and default filter
                file_filters = []
                default_filter = None

                # Obtain list with the different file filters
                for name, ext in ext_dict.items():
                    # Create proper string layout for this filter
                    file_filter = "%s (%s)" % (name, ext)
                    file_filters.append(file_filter)

                    # If this extension is the default one, save it as such
                    if default_ext in file_filter:
                        default_filter = file_filter

                # Add 'All (Image) Files' filter to the list of filters
                file_filters.append("All Image Files (%s)" %
                                    (' '.join(ext_dict.values())))
                file_filters.append("All Files (*)")

                # Combine list into a single string
                file_filters = ';;'.join(file_filters)

                # Create an OS-dependent options dict
                options = {}

                # Do not use Linux' native dialog as it is bad on some dists
                if platform.startswith('linux'):
                    options = {'options': QW.QFileDialog.DontUseNativeDialog}

                # Open the file saving system
                # Don't use native dialog as it is terrible on some Linux dists
                filename, _ = QW.QFileDialog.getSaveFileName(
                    parent=self.main,
                    caption="Save %s as..." % (hcube_name),
                    directory=fig_path,
                    filter=file_filters,
                    initialFilter=default_filter,
                    **options)

                # If filename was provided, save image
                if filename:
                    fig.savefig(filename)
                # Else, break the loop
                else:
                    break

            # Else, use default figure path
            else:
                fig.savefig(fig_path)

    # This function saves a list of projection figures to file
    @QC.Slot()
    @QC.Slot(list)
    @docstring_substitute(qt_slot=qt_slot_doc,
                          optional=list_items_optional_doc)
    def save_as_projection_figures(self, list_items=None):
        """
        Retrieves the projection figures requested in the provided `list_items`
        and saves their :obj:`~matplotlib.figure.Figure` objects, asking the
        user where to save each one.

        This function basically calls :meth:`~save_projection_figures` with
        `choose` set to *True*.

        %(qt_slot)s

        %(optional)s

        """

        self.save_projection_figures(list_items, choose=True)

    # This function redraws a list of projection figures
    @QC.Slot()
    @QC.Slot(list)
    @docstring_substitute(qt_slot=qt_slot_doc,
                          optional=list_items_optional_doc)
    def redraw_projection_figures(self, list_items=None):
        """
        Retrieves the projection figures requested in the provided `list_items`
        and redraws them, closing and recreating their
        :obj:`~matplotlib.figure.Figure` objects.

        This function is basically a combination of
        :meth:`~close_projection_figures` and :meth:`~draw_projection_figures`.

        %(qt_slot)s

        %(optional)s

        """

        # Obtain the list_items
        if list_items is None:
            list_items = self.proj_list_d.selectedItems()

        # Close and redraw all projection figures in list_items
        self.close_projection_figures(list_items)
        self.draw_projection_figures(list_items)

    # This function draws and saves a list of projection figures
    @QC.Slot()
    @QC.Slot(list)
    @docstring_substitute(qt_slot=qt_slot_doc,
                          optional=list_items_optional_doc)
    def draw_save_projection_figures(self, list_items=None):
        """
        Retrieves the projection figures requested in the provided
        `list_items`, draws their :obj:`~matplotlib.figure.Figure` objects and
        saves them afterward.

        This function is basically a combination of
        :meth:`~draw_projection_figures` and :meth:`~save_projection_figures`.

        %(qt_slot)s

        %(optional)s

        """

        # Obtain the list_items
        if list_items is None:
            list_items = self.proj_list_a.selectedItems()

        # Draw and save all projection figures in list_items
        if self.draw_projection_figures(list_items):
            self.save_projection_figures(list_items)

    # This function recreates a list of projection figures
    @QC.Slot()
    @QC.Slot(list)
    @docstring_substitute(qt_slot=qt_slot_doc,
                          optional=list_items_optional_doc)
    def recreate_projection_figures(self, list_items=None):
        """
        Retrieves the projection figures requested in the provided `list_items`
        and recreates them, permanently removing their corresponding projection
        data and recalculating it.

        This function is basically a combination of
        :meth:`~delete_projection_figures` and
        :meth:`~create_projection_figures`.

        %(qt_slot)s

        %(optional)s

        """

        # Obtain the list_items
        if list_items is None:
            list_items = self.proj_list_a.selectedItems()

        # Ask the user if they really want to recreate the figures
        button_clicked = QW.QMessageBox.warning(
            self, "WARNING: Recreate projection(s)",
            ("Are you sure you want to recreate the selected projection "
             "figure(s)? (<i>Note: This action is irreversible!</i>)"),
            QW.QMessageBox.Yes | QW.QMessageBox.No, QW.QMessageBox.No)

        # Delete and recreate all projection figures in list_items if yes
        if (button_clicked == QW.QMessageBox.Yes):
            self.delete_projection_figures(list_items, skip_warning=True)
            self.create_projection_figures(list_items)

    # This function creates and draws a list of projection figures
    @QC.Slot()
    @QC.Slot(list)
    @docstring_substitute(qt_slot=qt_slot_doc,
                          optional=list_items_optional_doc)
    def create_draw_projection_figures(self, list_items=None):
        """
        Retrieves the projection figures requested in the provided
        `list_items`, calculates their projection data and draws their
        :obj:`~matplotlib.figure.Figure` objects afterward.

        This function is basically a combination of
        :meth:`~create_projection_figures` and
        :meth:`~draw_projection_figures`.

        %(qt_slot)s

        %(optional)s

        """

        # Obtain the list_items
        if list_items is None:
            list_items = self.proj_list_u.selectedItems()

        # Create and draw all projection figures in list_items
        if self.create_projection_figures(list_items):
            self.draw_projection_figures(list_items)

    # This function creates, draws and saves a list of projection figures
    @QC.Slot()
    @QC.Slot(list)
    @docstring_substitute(qt_slot=qt_slot_doc,
                          optional=list_items_optional_doc)
    def create_draw_save_projection_figures(self, list_items=None):
        """
        Retrieves the projection figures requested in the provided
        `list_items`, calculates their projection data, draws their
        :obj:`~matplotlib.figure.Figure` objects and saves them afterward.

        This function is basically a combination of
        :meth:`~create_projection_figures`; :meth:`~draw_projection_figures`
        and :meth:`~save_projection_figures`.

        %(qt_slot)s

        %(optional)s

        """

        # Obtain the list_items
        if list_items is None:
            list_items = self.proj_list_u.selectedItems()

        # Create, draw and save all projection figures in list_items
        if self.create_projection_figures(list_items):
            if self.draw_projection_figures(list_items):
                self.save_projection_figures(list_items)

    # This function shows a details overview of a drawn projection figure
    @QC.Slot()
    @QC.Slot(QW.QListWidgetItem)
    @docstring_substitute(qt_slot=qt_slot_doc, optional=list_item_optional_doc)
    def details_drawn_projection_figure(self, list_item=None):
        """
        Retrieves the projection figure requested in the provided `list_item`,
        gathers its properties and shows a details dialog listing them.

        This function is used for projections in the 'Drawn' list.

        %(qt_slot)s

        %(optional)s

        """

        # Obtain the list_item
        if list_item is None:
            list_item = self.proj_list_d.selectedItems()[0]

        # Show details
        self._details_projection_figure(list_item)

    # This function shows a details overview of an available projection figure
    @QC.Slot()
    @QC.Slot(QW.QListWidgetItem)
    @docstring_substitute(qt_slot=qt_slot_doc, optional=list_item_optional_doc)
    def details_available_projection_figure(self, list_item=None):
        """
        Retrieves the projection figure requested in the provided `list_item`,
        gathers its properties and shows a details dialog listing them.

        This function is used for projections in the 'Available' list.

        %(qt_slot)s

        %(optional)s

        """

        # Obtain the list_item
        if list_item is None:
            list_item = self.proj_list_a.selectedItems()[0]

        # Show details
        self._details_projection_figure(list_item)

    # This function shows a details overview of a projection figure
    # TODO: Add section on how the figure was drawn for drawn projections?
    def _details_projection_figure(self, list_item):
        """
        Creates and shows a details dialog for the projection figure requested
        in the provided `list_item`.

        Parameters
        ----------
        list_item : :obj:`~PyQt5.QtWidgets.QListWidgetItem` object
            The item that contains the requested projection figure.

        """

        # Retrieve text of list_item
        hcube_name = list_item.text()
        hcube = self.hcubes[self.names.index(hcube_name)]

        # Is this a 3D projection?
        is_3D = (len(hcube) == 3)

        # Gather some details about this projection figure
        emul_i = hcube[0]  # Emulator iteration
        pars = hcube[1:]  # Plotted parameters
        proj_type = '%iD' % (len(hcube))  # Projection type

        # Open hdf5-file
        with self.pipe._File('r', None) as file:
            # Get the group that contains the data for this projection figure
            group = file["%i/proj_hcube/%s" % (emul_i, hcube_name)]

            # Gather more details about this projection figure
            impl_cut = group.attrs['impl_cut']  # Implausibility cut-offs
            cut_idx = group.attrs['cut_idx']  # Number of wildcards
            res = group.attrs['proj_res']  # Projection resolution
            depth = group.attrs['proj_depth']  # Projection depth

        # Get the percentage of plausible space remaining
        if self.pipe._n_eval_sam[emul_i]:
            pl_space_rem = "{0:#.3%}".format((self.pipe._n_impl_sam[emul_i] /
                                              self.pipe._n_eval_sam[emul_i]))
        else:
            pl_space_rem = "N/A"
        pl_space_rem = QW.QLabel(pl_space_rem)

        # Obtain QLabel instances of all details
        emul_i = QW.QLabel(str(emul_i))
        pars = ', '.join([self.pipe._modellink._par_name[par] for par in pars])
        pars = QW.QLabel(pars)
        proj_type = QW.QLabel(proj_type)
        impl_cut = QW.QLabel(str(impl_cut.tolist()))
        cut_idx = QW.QLabel(str(cut_idx))

        # Get the labels for the grid shape and size
        if is_3D:
            grid_shape = QW.QLabel("{0:,}x{0:,}x{1:,}".format(res, depth))
            grid_size = QW.QLabel("{0:,}".format(res * res * depth))
        else:
            grid_shape = QW.QLabel("{0:,}x{1:,}".format(res, depth))
            grid_size = QW.QLabel("{0:,}".format(res * depth))

        # Convert res and depth as well
        res = QW.QLabel("{0:,}".format(res))
        depth = QW.QLabel("{0:,}".format(depth))

        # Create a layout for the details
        details_layout = QW.QVBoxLayout()

        # GENERAL
        # Create a group for the general details
        general_group = QW.QGroupBox("General")
        details_layout.addWidget(general_group)
        general_layout = QW.QFormLayout()
        general_group.setLayout(general_layout)

        # Add general details
        general_layout.addRow("Emulator iteration", emul_i)
        general_layout.addRow("Parameters", pars)
        general_layout.addRow("Projection type", proj_type)
        general_layout.addRow("% of parameter space remaining", pl_space_rem)

        # PROJECTION DATA
        # Create a group for the projection data details
        data_group = QW.QGroupBox("Projection data")
        details_layout.addWidget(data_group)
        data_layout = QW.QFormLayout()
        data_group.setLayout(data_layout)

        # Add projection data details
        data_layout.addRow("Grid shape", grid_shape)
        data_layout.addRow("Grid size", grid_size)
        data_layout.addRow("# of implausibility wildcards", cut_idx)
        data_layout.addRow("Implausibility cut-offs", impl_cut)

        # Create a details message box for this projection figure
        details_box = QW.QDialog(self.main)
        details_box.setWindowModality(QC.Qt.NonModal)
        details_box.setAttribute(QC.Qt.WA_DeleteOnClose)
        details_box.setWindowFlags(QC.Qt.WindowSystemMenuHint | QC.Qt.Window
                                   | QC.Qt.WindowCloseButtonHint
                                   | QC.Qt.MSWindowsOwnDC
                                   | QC.Qt.MSWindowsFixedSizeDialogHint)
        details_layout.setSizeConstraint(QW.QLayout.SetFixedSize)
        details_box.setWindowTitle("%s: %s details" % (APP_NAME, hcube_name))
        details_box.setLayout(details_layout)

        # Show the details message box
        details_box.show()

    # This function creates the proper progress dialog for given operation
    def use_progress_dialog(self, label, func, *iterables):
        """
        Creates a progress dialog with the given `label`, and executes the
        requested `func` using the provided `iterables`.

        Depending on the current settings, this function will either create a
        :obj:`~prism._gui.widgets.helpers.ThreadedProgressDialog` object that
        allows the user to abort the operation (but is slower), or a static
        dialog that cannot be interrupted.

        Parameters
        ----------
        label : str
            The label that is used as the description of what operation is
            currently being executed.
        func : function
            The function that must be called iteratively using the arguments
            provided in `iterables`.
        iterables : positional arguments
            All iterables that must be used to call `func` with.

        Returns
        -------
        result : bool
            Whether or not the operations ended successfully, which can be used
            by other functions to determine if it should continue.

        """

        # Set result to False
        result = False

        # Use a progress dialog if one was requested
        if self.main.get_option('use_progress_dialog'):
            # Create a threaded progress dialog for creating projections
            progress_dialog = ThreadedProgressDialog(self.main, label, func,
                                                     *iterables)

            # Wrap in try-statement to make sure the dialog is always closed
            try:
                # Execute the function provided to the progress dialog
                result = progress_dialog()
            # Close dialog
            finally:
                progress_dialog.close()

        # Else, do not use one and execute on main thread
        else:
            # Create a dialog showing that an operation is being executed
            dialog = QW.QDialog(self.main)
            dialog.setWindowModality(QC.Qt.ApplicationModal)
            dialog.setWindowTitle(APP_NAME)
            dialog.setAttribute(QC.Qt.WA_DeleteOnClose)
            dialog.setWindowFlags(QC.Qt.WindowTitleHint | QC.Qt.Dialog
                                  | QC.Qt.CustomizeWindowHint)
            layout = QW.QVBoxLayout(dialog)
            layout.addWidget(QW.QLabel(label))

            # Show the dialog
            dialog.show()

            # Make cursor show that it is busy
            QW.QApplication.setOverrideCursor(QG.QCursor(QC.Qt.WaitCursor))

            # Wait for 0.1 seconds and then process events to update dialog
            time.sleep(0.1)
            QW.QApplication.instance().processEvents()

            # Wrap in try-statement to make sure the dialog is always closed
            try:
                # Loop over all iterables
                for items in zip(*iterables):
                    func(*items)

                # If this finishes successfully, set result to True
                else:
                    result = True

            # Restore mouse cursor and close dialog
            finally:
                QW.QApplication.restoreOverrideCursor()
                dialog.close()

        # Return result
        return (result)
示例#4
0
class KwargsDictDialog(QW.QDialog):
    """
    Defines the :class:`~KwargsDictDialog` class for the preferences window.

    This class provides the 'Projection keyword argument dicts' dialog, which
    allows for the various different kwargs dicts to be modified by the user.

    """

    @e13.docstring_substitute(optional=kwargs_doc.format(
        'PyQt5.QtWidgets.QDialog'))
    def __init__(self, options_dialog_obj, *args, **kwargs):
        """
        Initialize an instance of the :class:`~KwargsDictDialog` class.

        Parameters
        ----------
        options_dialog_obj : \
            :obj:`~prism._gui.widgets.preferences.OptionsDialog` object
            Instance of the
            :class:`~prism._gui.widgets.preferences.OptionsDialog` class that
            acts as the parent of this dialog.

        %(optional)s

        """

        # Save provided options_dialog_obj
        self.options = options_dialog_obj
        self.options.dict_dialog = self

        # Call super constructor
        super().__init__(self.options, *args, **kwargs)

        # Create the kwargs dict window
        self.init()

    # This function shows an editable window with the entries in the dict
    @QC.Slot()
    def __call__(self):
        """
        Qt slot that shows the kwargs dict dialog in the center of the
        preferences window.

        """

        # Show it
        self.show()

        # Move the kwargs_dicts window to the center of the main window
        self.move(self.options.geometry().center()-self.rect().center())

    # This function creates the kwargs dict window
    def init(self):
        """
        Sets up the kwargs dict dialog after it has been initialized.

        This function is mainly responsible for setting up the layout of the
        dialog, and making sure that new kwargs dict pages can be added.

        """

        # Create a window layout
        window_layout = QW.QVBoxLayout(self)

        # Create a splitter widget for this window
        splitter_widget = QW.QSplitter()
        splitter_widget.setChildrenCollapsible(False)
        window_layout.addWidget(splitter_widget)

        # Create a contents widget
        self.contents = QW.QListWidget()
        self.contents.setMovement(QW.QListView.Static)
        self.contents.setSpacing(1)
        splitter_widget.addWidget(self.contents)

        # Create pages widget
        self.pages = QW.QStackedWidget()
        splitter_widget.addWidget(self.pages)

        # Set signal handling
        self.contents.currentRowChanged.connect(self.pages.setCurrentIndex)

        # Add a close button
        button_box = QW.QDialogButtonBox()
        window_layout.addWidget(button_box)
        close_but = button_box.addButton(button_box.Close)
        close_but.clicked.connect(self.close)
        self.close_but = close_but

        # Set some properties for this window
        self.setWindowTitle("Viewing projection keyword dicts")     # Title

        # Set the height of an editable entry
        self.entry_height = 24

    # This function creates a new page
    @e13.docstring_substitute(optional=kwargs_doc.format(
        'prism._gui.widgets.preferences.KwargsDictDialogPage'))
    def add_page(self, name, option_key, tooltip, *args, **kwargs):
        """
        Initializes a new :obj:`~KwargsDictDialogPage` object with name `name`
        and adds it to this dialog.

        Parameters
        ----------
        name : str
            The name that this kwargs dict page will have.
        option_key : str
            The name of the options entry that this page will create.
            The value of `option_key` must correspond to the name the
            associated dict has in the :meth:`~prism.Pipeline.project` method.
        tooltip : str
            The tooltip that must be used for this kwargs dict page.

        %(optional)s

        """

        # Create a page
        kwargs_page = KwargsDictDialogPage(self, name, *args, **kwargs)

        # Add this new page to the option_entries
        self.options.create_entry(option_key, kwargs_page,
                                  self.options.proj_defaults[option_key])

        # Create a scrollarea for the page
        scrollarea = QW.QScrollArea(self)
        scrollarea.setWidgetResizable(True)
        scrollarea.setWidget(kwargs_page)

        # Add it to the contents and pages widgets
        n_contents = self.contents.count()
        self.contents.addItem(name)
        self.contents.item(n_contents).setData(QC.Qt.ToolTipRole, tooltip)
        self.contents.setFixedWidth(
            int(1.1*self.contents.sizeHintForColumn(0)))
        self.pages.addWidget(scrollarea)
示例#5
0
class DefaultBox(BaseBox):
    """
    Defines the :class:`~DefaultBox` class.

    This class is used for making a non-standard entry in the
    :class:`~prism._gui.widgets.preferences.KwargsDictDialogPage` class.
    It currently supports inputs of type bool; float; int; and str.

    """
    @docstring_substitute(
        optional=kwargs_doc.format('prism._gui.widgets.core.BaseBox'))
    def __init__(self, *args, **kwargs):
        """
        Initialize an instance of the :class:`~DefaultBox` class.

        %(optional)s

        """

        # Call super constructor
        super().__init__(*args, **kwargs)

        # Create the default box
        self.init()

    # This function creates a double box with type and lineedit
    def init(self):
        """
        Sets up the non-standard default box entry after it has been
        initialized.

        This function is mainly responsible for creating the type combobox and
        allowing for different field boxes to be used for different value
        types.

        """

        # Create the box_layout
        box_layout = QW.QHBoxLayout(self)
        box_layout.setContentsMargins(0, 0, 0, 0)
        self.box_layout = box_layout
        self.setToolTip("Enter the type and value for this unknown entry type")

        # Make a look-up dict for types
        self.type_dict = {bool: 'bool', float: 'float', int: 'int', str: 'str'}

        # Create a combobox for the type
        type_box = QW_QComboBox()
        type_box.addItems(self.type_dict.values())
        type_box.setToolTip("Type of the entered value")
        type_box.setSizePolicy(QW.QSizePolicy.Fixed, QW.QSizePolicy.Fixed)
        type_box.currentTextChanged.connect(self.create_field_box)
        self.type_box = type_box

        # Make value box corresponding to the current type
        val_box = getattr(self, "create_type_%s" % (type_box.currentText()))()
        self.value_box = val_box

        # Add everything to the box_layout
        box_layout.addWidget(type_box)
        box_layout.addWidget(val_box)

    # This function creates a field_box depending on the type that was selected
    @QC.Slot(str)
    @docstring_substitute(qt_slot=qt_slot_doc)
    def create_field_box(self, value_type):
        """
        Creates a field box for the provided type `value_type` and replaces the
        current field box with it.

        %(qt_slot)s

        Parameters
        ----------
        value_type : {'bool'; 'float'; 'int'; 'str'}
            The string that defines what type of field box is requested.

        """

        # Create a widget box for the specified value_type
        value_box = getattr(self, "create_type_%s" % (value_type))()

        # Set this value_box in the layout
        cur_item = self.box_layout.replaceWidget(self.value_box, value_box)
        cur_item.widget().close()
        del cur_item

        # Save new value_box
        self.value_box = value_box

    # This function creates the value box for bools
    def create_type_bool(self):
        """
        Creates the field box for values of type 'bool' and returns it.

        """

        # Create a checkbox for bools
        bool_box = QW.QCheckBox()
        bool_box.setToolTip("Boolean value for this entry type")
        return (bool_box)

    # This function creates the value box for floats
    def create_type_float(self):
        """
        Creates the field box for values of type 'float' and returns it.

        """

        # Create a spinbox for floats
        float_box = QW_QDoubleSpinBox()
        float_box.setRange(-9999999, 9999999)
        float_box.setDecimals(6)
        float_box.setToolTip("Float value for this entry type")
        return (float_box)

    # This function creates the value box for integers
    def create_type_int(self):
        """
        Creates the field box for values of type 'int' and returns it.

        """

        # Create a spinbox for integers
        int_box = QW_QSpinBox()
        int_box.setRange(-9999999, 9999999)
        int_box.setToolTip("Integer value for this entry type")
        return (int_box)

    # This function creates the value box for strings
    def create_type_str(self):
        """
        Creates the field box for values of type 'str' and returns it.

        """

        # Create a lineedit for strings
        str_box = QW.QLineEdit()
        str_box.setToolTip("String value for this entry type")
        return (str_box)

    # This function retrieves a value of this special box
    def get_box_value(self):
        """
        Returns the current value of the field box.

        Returns
        -------
        value : bool, float, int or str
            The current value of this default box.

        """

        return (get_box_value(self.value_box))

    # This function sets the value of this special box
    def set_box_value(self, value):
        """
        Sets the value type to `type(value)` and the field value to `value`.

        Parameters
        ----------
        value : bool, float, int or str
            The value to use for this default box. The type of `value`
            determines which field box must be used.

        """

        set_box_value(self.type_box, self.type_dict[type(value)])
        set_box_value(self.value_box, value)
示例#6
0
class FigSizeBox(BaseBox):
    """
    Defines the :class:`~FigSizeBox` class.

    This class is used for making the 'figsize' entry in the
    :class:`~prism._gui.widgets.preferences.KwargsDictDialogPage` class.

    """
    @docstring_substitute(
        optional=kwargs_doc.format('prism._gui.widgets.core.BaseBox'))
    def __init__(self, *args, **kwargs):
        """
        Initialize an instance of the :class:`~FigSizeBox` class.

        %(optional)s

        """

        # Call super constructor
        super().__init__(*args, **kwargs)

        # Create the figsize box
        self.init()

    # This function creates the figsize box
    def init(self):
        """
        Sets up the figure size entry after it has been initialized.

        This function is mainly responsible for simply creating the two double
        spinboxes that allow for the width and height to be set.

        """

        # Create the box_layout
        box_layout = QW.QHBoxLayout(self)
        box_layout.setContentsMargins(0, 0, 0, 0)
        self.setToolTip("Figure size dimensions to use for the projection "
                        "figure")

        # Create two double spinboxes for the width and height
        # WIDTH
        width_box = QW_QDoubleSpinBox()
        width_box.setRange(1, 9999999)
        width_box.setSuffix("'")
        width_box.setStepType(width_box.AdaptiveDecimalStepType)
        width_box.setToolTip("Width (in inches) of projection figure")
        self.width_box = width_box

        # HEIGHT
        height_box = QW_QDoubleSpinBox()
        height_box.setRange(1, 9999999)
        height_box.setSuffix("'")
        height_box.setToolTip("Height (in inches) of projection figure")
        self.height_box = height_box

        # Also create a textlabel with 'X'
        x_label = QW.QLabel('X')
        x_label.setSizePolicy(QW.QSizePolicy.Fixed, QW.QSizePolicy.Fixed)

        # Add everything to the box_layout
        box_layout.addWidget(width_box)
        box_layout.addWidget(x_label)
        box_layout.addWidget(height_box)

        # Set default value
        set_box_value(self, rcParams['figure.figsize'])

    # This function retrieves a value of this special box
    def get_box_value(self):
        """
        Returns the current width and height of this figsize box and returns
        it.

        Returns
        -------
        figsize : tuple
            A tuple containing the width and height values of the figsize,
            formatted as `(width, height)`.

        """

        return ((get_box_value(self.width_box),
                 get_box_value(self.height_box)))

    # This function sets the value of this special box
    def set_box_value(self, value):
        """
        Sets the current value of the figsize box to `value`.

        Parameters
        ----------
        value : tuple
            A tuple containing the width and height values of the figsize,
            formatted as `(width, height)`.

        """

        set_box_value(self.width_box, value[0])
        set_box_value(self.height_box, value[1])
示例#7
0
class ColorMapBox(BaseBox):
    """
    Defines the :class:`~ColorMapBox` class.

    This class is used for making the 'cmap' entry in the
    :class:`~prism._gui.widgets.preferences.KwargsDictDialogPage` class.

    """
    @docstring_substitute(
        optional=kwargs_doc.format('prism._gui.widgets.core.BaseBox'))
    def __init__(self, *args, **kwargs):
        """
        Initialize an instance of the :class:`~ColorMapBox` class.

        %(optional)s

        """

        # Call super constructor
        super().__init__(*args, **kwargs)

        # Create the colormap box
        self.init()

    # This function creates a combobox with colormaps
    def init(self):
        # Define set of CMasher colormaps that should be at the top
        cmr_cmaps = sset(
            ['dusk', 'freeze', 'gothic', 'heat', 'rainforest', 'sunburst'])

        # Check that all of those colormaps are available in CMasher
        cmr_cmaps.intersection_update(cmr.cm.cmap_d)

        # Obtain a set with default MPL colormaps that should be at the top
        std_cmaps = sset(['cividis', 'inferno', 'magma', 'plasma', 'viridis'])

        # Add CMasher colormaps to it
        std_cmaps.update(['cmr.' + cmap for cmap in cmr_cmaps])

        # Obtain reversed set of recommended colormaps
        std_cmaps_r = sset([cmap + '_r' for cmap in std_cmaps])

        # Obtain a list with all colormaps and their reverses
        all_cmaps = sset(
            [cmap for cmap in cm.cmap_d if not cmap.endswith('_r')])
        all_cmaps_r = sset([cmap for cmap in cm.cmap_d if cmap.endswith('_r')])

        # Gather all sets together
        cmaps = (std_cmaps, std_cmaps_r, all_cmaps, all_cmaps_r)

        # Determine the cumulative lengths of all four sets
        cum_len = np.cumsum(list(map(len, cmaps)))

        # Set the size for the colormap previews
        cmap_size = (100, 15)

        # If the colormap icons have not been created yet, do that now
        if not hasattr(self, 'cmap_icons'):
            cmap_icons = sdict()
            for cmap in chain(all_cmaps, all_cmaps_r):
                cmap_icons[cmap] = self.create_cmap_icon(cmap, cmap_size)
            ColorMapBox.cmap_icons = cmap_icons

        # Create a layout for this widget
        box_layout = QW.QHBoxLayout(self)
        box_layout.setContentsMargins(0, 0, 0, 0)
        self.setToolTip("Colormap to be used for the corresponding plot type")

        # Create a combobox for cmaps
        cmaps_box = QW_QComboBox()
        for cmap in chain(*cmaps):
            cmap_icon = self.cmap_icons[cmap]
            cmaps_box.addItem(cmap_icon, cmap)

        # Add some separators
        for i in reversed(cum_len[:-1]):
            cmaps_box.insertSeparator(i)
        cmaps_box.insertSeparator(cum_len[1] + 1)

        # Set remaining properties
        set_box_value(cmaps_box, rcParams['image.cmap'])
        cmaps_box.setIconSize(QC.QSize(*cmap_size))
        cmaps_box.currentTextChanged.connect(self.cmap_selected)

        # Add cmaps_box to layout
        box_layout.addWidget(cmaps_box)
        self.cmaps_box = cmaps_box

    # This function creates an icon of a colormap
    @staticmethod
    def create_cmap_icon(cmap, size):
        """
        Creates a :obj:`~PyQt5.QtGui.QIcon` object of the given `cmap` with the
        provided `size`.

        Parameters
        ----------
        cmap : :obj:`~matplotlib.colors.Colormap` object or str
            The colormap for which an icon needs to be created.
        size : tuple
            A tuple containing the width and height dimension values of the
            icon to be created.

        Returns
        -------
        icon : :obj:`~PyQt5.QtGui.QIcon` object
            The instance of the :class:`~PyQt5.QtGui.QIcon` class that was
            created from the provided `cmap` and `size`.

        """

        # Obtain the cmap
        cmap = cm.get_cmap(cmap)

        # Obtain the RGBA values of the colormap
        # TODO: Figure out why setting 256 to cmap.N does not work for N > 256
        x = np.linspace(0, 1, 256)
        rgba = cmap(x)

        # Convert to Qt RGBA values
        rgba = [
            QG.QColor(int(r * 255), int(g * 255), int(b * 255),
                      int(a * 255)).rgba() for r, g, b, a in rgba
        ]

        # Create an image object
        image = QG.QImage(256, 1, QG.QImage.Format_Indexed8)

        # Set the value of every pixel in this image
        image.setColorTable(rgba)
        for i in range(256):
            image.setPixel(i, 0, i)

        # Scale the image to its proper size
        image = image.scaled(*size)

        # Convert the image to a pixmap
        pixmap = QG.QPixmap.fromImage(image)

        # Convert the pixmap to an icon
        icon = QG.QIcon(pixmap)

        # Return the icon
        return (icon)

    # This function checks a selected cmap
    @QC.Slot(str)
    def cmap_selected(self, cmap):
        """
        Qt slot that checks a provided `cmap` and shows an error message if
        `cmap` is a terrible colormap.

        """

        # Make a tuple with terrible colormaps
        bad_cmaps = ('gist_ncar', 'gist_rainbow', 'gist_stern', 'jet',
                     'nipy_spectral')

        # If a terrible colormap is selected, show error message
        if cmap.startswith(bad_cmaps):
            # Create error message
            err_msg = ("The selected <b><i>%s</i></b> cmap is terrible for "
                       "drawing PRISM's projection figures. To avoid "
                       "introducing fake perceptual features, it is "
                       "recommended to pick a <i>perceptually uniform "
                       "sequential</i> colormap, like the ones at the top of "
                       "this list.<br><br>"
                       "See <a href=\"%s\">here</a> for more information on "
                       "this subject." %
                       (cmap, ("https://cmasher.readthedocs.io/en/latest")))

            # Show error window
            QW.QMessageBox.warning(self, "%s WARNING" % (cmap.upper()),
                                   err_msg)

    # This function retrieves a value of this special box
    def get_box_value(self):
        """
        Returns the current colormap of the colormap box.

        Returns
        -------
        cmap : :obj:`~matplotlib.colors.Colormap` object
            The currently selected colormap.

        """

        # Obtain the value
        colormap = get_box_value(self.cmaps_box)

        # Convert to matplotlib colormap
        cmap = cm.get_cmap(colormap)

        # Return it
        return (cmap)

    # This function sets the value of this special box
    def set_box_value(self, cmap):
        """
        Sets the current colormap to `cmap`.

        Parameters
        ----------
        cmap : :obj:`~matplotlib.colors.Colormap` object
            The colormap that must be used for this colormap box.

        """

        # Obtain the name of the provided colormap
        name = cmap.name

        # Set this as the current colormap
        set_box_value(self.cmaps_box, name)
示例#8
0
class ColorBox(BaseBox):
    """
    Defines the :class:`~ColorBox` class.

    This class is used for making the 'color' entry in the
    :class:`~prism._gui.widgets.preferences.KwargsDictDialogPage` class.

    """
    @docstring_substitute(
        optional=kwargs_doc.format('prism._gui.widgets.core.BaseBox'))
    def __init__(self, *args, **kwargs):
        """
        Initialize an instance of the :class:`~ColorBox` class.

        %(optional)s

        """

        # Call super constructor
        super().__init__(*args, **kwargs)

        # Create the color box
        self.init()

    # This function creates the color box
    def init(self):
        """
        Sets up the color box entry after it has been initialized.

        This function is mainly responsible for creating the color wheel and
        color label, that allow the user to quickly cycle through different
        color options.

        """

        # Create the box layout
        box_layout = QW.QHBoxLayout(self)
        box_layout.setContentsMargins(0, 0, 0, 0)
        self.setToolTip("Color to be used for the corresponding plot type")

        # Declare the default color
        self.default_color = rcParams['lines.color']

        # Create a color label
        color_label = self.create_color_label()
        self.color_label = color_label
        box_layout.addWidget(color_label)

        # Create a color combobox
        color_combobox = self.create_color_combobox()
        box_layout.addWidget(color_combobox)
        self.color_combobox = color_combobox

        # Set the starting color of the color box
        self.set_box_value(self.default_color)

    # This function creates the color label
    def create_color_label(self):
        """
        Creates a special label that shows the currently selected or hovered
        color, and returns it.

        """

        # Create the color label
        color_label = QW_QLabel()

        # Set some properties
        color_label.setFrameShape(QW.QFrame.StyledPanel)
        color_label.setScaledContents(True)
        color_label.setToolTip("Click to open the custom color picker")
        color_label.setSizePolicy(QW.QSizePolicy.Fixed, QW.QSizePolicy.Fixed)
        color_label.mousePressed.connect(self.show_colorpicker)

        # Return it
        return (color_label)

    # This function creates the color combobox
    def create_color_combobox(self):
        """
        Creates a combobox that holds all default colors accepted by matplotlib
        and returns it.

        """

        # Obtain the CN colors
        n_cyclic = len(rcParams['axes.prop_cycle'])
        CN_COLORS = [("C%i" % (i), "This is MPL cyclic color #%i" % (i))
                     for i in range(n_cyclic)]

        # Make tuple of all colors
        colors = (CN_COLORS, BASE_COLORS, CSS4_COLORS)

        # Determine the cumulative lengths of all four sets
        cum_len = np.cumsum(list(map(len, colors)))

        # Make combobox for colors
        color_box = QW_QEditableComboBox()

        # Fill combobox with all colors
        for i, color in enumerate(chain(*colors)):
            # If color is a tuple, it consists of (color, tooltip)
            if isinstance(color, tuple):
                color_box.addItem(color[0])
                color_box.setItemData(i, color[1], QC.Qt.ToolTipRole)
            else:
                color_box.addItem(color)

        # Add some separators
        for i in reversed(cum_len[:-1]):
            color_box.insertSeparator(i)

        # Set remaining properties
        color_box.setToolTip("Select or type (in HEX) the color")
        color_box.highlighted[str].connect(self.set_color_label)
        color_box.popup_hidden[str].connect(self.set_color_label)
        color_box.currentTextChanged.connect(self.set_color)
        return (color_box)

    # This function converts an MPL color to a QColor
    @staticmethod
    def convert_to_qcolor(color):
        """
        Converts a provided matplotlib color `color` to a
        :obj:`~PyQt5.QtGui.QColor` object.

        Parameters
        ----------
        color : str
            The matplotlib color that must be converted.
            If `color` is a float string, an error will be raised, as Qt5 does
            not accept those.

        Returns
        -------
        qcolor : :obj:`~PyQt5.QtGui.QColor` object
            The instance of the :class:`~PyQt5.QtGui.QColor` class that
            corresponds to the provided `color`.

        """

        # If the color can be converted to a float, raise a ValueError
        # This is because MPL accepts float strings as valid colors
        try:
            float(color)
        except ValueError:
            pass
        else:
            raise ValueError

        # Obtain the RGBA values of an MPL color
        r, g, b, a = to_rgba(color)

        # Convert to Qt RGBA values
        color = QG.QColor(int(r * 255), int(g * 255), int(b * 255),
                          int(a * 255))

        # Return color
        return (color)

    # This function converts a QColor to an MPL color
    @staticmethod
    def convert_to_mpl_color(qcolor):
        """
        Converts a provided :obj:`~PyQt5.QtGui.QColor` object `color` to a
        matplotlib color.

        Parameters
        ----------
        qcolor : :obj:`~PyQt5.QtGui.QColor` object
            The instance of the :class:`~PyQt5.QtGui.QColor` class must be
            converted to a matplotlib color.

        Returns
        -------
        color : str
            The corresponding matplotlib color.
            The returned `color` is always written in HEX.

        """

        hexid = qcolor.name()
        return str(hexid)

    # This function creates a pixmap of an MPL color
    @staticmethod
    def create_color_pixmap(color, size):
        """
        Creates a :obj:`~PyQt5.QtGui.QPixmap` object consisting of the given
        `color` with the provided `size`.

        Parameters
        ----------
        color : str
            The matplotlib color that must be used for the pixmap.
        size : tuple
            The width and height dimension values of the pixmap to be created.

        Returns
        -------
        pixmap : :obj:`~PyQt5.QtGui.QPixmap` object
            The instance of the :class:`~PyQt5.QtGui.QPixmap` class that was
            created from the provided `color` and `size`.

        """

        # Obtain the RGBA values of an MPL color
        color = ColorBox.convert_to_qcolor(color)

        # Create an image object
        image = QG.QImage(*size, QG.QImage.Format_RGB32)

        # Fill the entire image with the same color
        image.fill(color)

        # Convert the image to a pixmap
        pixmap = QG.QPixmap.fromImage(image)

        # Return the pixmap
        return (pixmap)

    # This function shows the custom color picker dialog
    @QC.Slot()
    @docstring_substitute(qt_slot=qt_slot_doc)
    def show_colorpicker(self):
        """
        Shows the colorwheel picker dialog to the user, allowing for any color
        option to be selected.

        %(qt_slot)s

        """

        # Obtain current qcolor
        qcolor = self.convert_to_qcolor(self.get_box_value())

        # Show color dialog
        color = QW.QColorDialog.getColor(
            qcolor, parent=self, options=QW.QColorDialog.DontUseNativeDialog)

        # If the returned color is valid, save it
        if color.isValid():
            self.set_color(self.convert_to_mpl_color(color))

    # This function sets a given color as the current color
    @QC.Slot(str)
    @docstring_substitute(qt_slot=qt_slot_doc)
    def set_color(self, color):
        """
        Sets the current color to the provided `color`, and updates the entry
        in the combobox and the label accordingly.

        %(qt_slot)s

        Parameters
        ----------
        color : str
            The color that needs to be used as the current color. The provided
            `color` can be any string that is accepted as a color by
            matplotlib.
            If `color` is invalid, it is set to the current default color
            instead.

        """

        # If color can be converted to a hex integer, do so and add hash to it
        try:
            int(color, 16)
        except ValueError:
            pass
        else:
            # Make sure that color has a length of 6
            if (len(color) == 6):
                color = "#%s" % (color)

        # Set the color label
        default_flag = self.set_color_label(color)

        # If default was not used, set the combobox to the proper value as well
        if not default_flag:
            set_box_value(self.color_combobox, color)

    # This function sets the color of the colorlabel
    @QC.Slot(str)
    @docstring_substitute(qt_slot=qt_slot_doc)
    def set_color_label(self, color):
        """
        Sets the current color label to the provided `color`.

        %(qt_slot)s

        Parameters
        ----------
        color : str
            The color that needs to be used as the current color label. The
            provided `color` can be any string that is accepted as a color by
            matplotlib.
            If `color` is invalid, it is set to the current default color
            instead.

        Returns
        -------
        default_flag : bool
            Whether or not the color label is currently set to the default
            color. This happens when `color` is an invalid color.

        """

        # Try to create the pixmap of the colorlabel
        try:
            pixmap = self.create_color_pixmap(
                color, (70, self.color_combobox.height() - 2))
            default_flag = False
        # If that cannot be done, create the default instead
        except ValueError:
            pixmap = self.create_color_pixmap(
                self.default_color, (70, self.color_combobox.height() - 2))
            default_flag = True

        # Set the colorlabel
        self.color_label.setPixmap(pixmap)

        # Return if default was used or not
        return (default_flag)

    # This function retrieves a value of this special box
    def get_box_value(self):
        """
        Returns the current (valid) color value of the color combobox.

        Returns
        -------
        color : str
            The current valid matplotlib color value.

        """

        # Obtain the value
        color = get_box_value(self.color_combobox)

        # Try to convert this to QColor
        try:
            self.convert_to_qcolor(color)
        # If this fails, return the default color
        except ValueError:
            return (self.default_color)
        # Else, return the retrieved color
        else:
            return (color)

    # This function sets the value of this special box
    def set_box_value(self, value):
        """
        Sets the current (default) color value to `value`.

        Parameters
        ----------
        value : str
            The matplotlib color value that must be set for this colorbox.

        """

        self.set_color(value)
        self.default_color = value
        self.color_combobox.lineEdit().setPlaceholderText(value)
示例#9
0
class MainViewerWindow(QW.QMainWindow):
    """
    Defines the :class:`~MainViewerWindow` class for the Projection GUI.

    This class provides the main window for the GUI and combines all other
    widgets; layouts; and elements together.

    """

    # Create signal for exception that are raised
    exception = QC.Signal()

    # Initialize MainViewerWindow class
    @docstring_substitute(optional=kwargs_doc.format(
        'PyQt5.QtWidgets.QMainWindow'))
    def __init__(self, pipeline_obj, *args, **kwargs):
        """
        Initialize an instance of the :class:`~MainViewerWindow` class.

        Parameters
        ----------
        pipeline_obj : :obj:`~prism.Pipeline` object
            Instance of the :class:`~prism.Pipeline` class for which the GUI
            needs to be initialized.

        %(optional)s

        """

        # Save pipeline_obj as pipe
        self.pipe = pipeline_obj

        # Call super constructor
        super().__init__(*args, **kwargs)

        # Set up the main window
        self.init()

    # This function sets up the main window
    def init(self):
        """
        Sets up the main window after it has been initialized.

        This function is mainly responsible for initializing all other widgets
        that are required to make the GUI work, and connecting them together.

        """

        # Tell the Projection class that the GUI is being used
        self.all_set_proj_attr('use_GUI', 1)

        # Determine the last emulator iteration
        emul_i = self.pipe._make_call('_emulator._get_emul_i', None)

        # Try to prepare projections for all iterations
        try:
            self.all_call_proj_attr('prepare_projections',
                                    emul_i, None, force=False, figure=True)

        # If this raises a RequestError, the last iteration cannot be prepared
        except RequestError as error:
            # If that happens, emit a warning about it
            warnings.warn("%s. Falling back to previous iteration."
                          % (error), RequestWarning, stacklevel=2)

            # Reprepare up to the previous iteration
            self.all_call_proj_attr('prepare_projections',
                                    emul_i-1, None, force=False, figure=True)

        # Save some statistics about pipeline and modellink
        self.n_par = self.pipe._modellink._n_par

        # Make sure that the viewer is deleted when window is closed
        self.setAttribute(QC.Qt.WA_DeleteOnClose)

        # Disable the default context menu (right-click menu)
        self.setContextMenuPolicy(QC.Qt.NoContextMenu)

        # Set window icon and title
        self.setWindowTitle(APP_NAME)
        self.setWindowIcon(QG.QIcon(APP_ICON_PATH))

        # Create statusbar
        self.create_statusbar()

        # Prepare the windows and toolbars menus
        self.windows_menu = QW_QMenu(self, '&Windows')
        self.toolbars_menu = QW_QMenu(self, '&Toolbars')

        # Get default positions of all dock widgets
        self.default_pos = self.get_default_dock_positions()

        # OVERVIEW DOCK WIDGET
        # Create the projection overview dock widget
        self.overview_dock = OverviewDockWidget(self)

        # Create an action for enabling/disabling the overview
        proj_overview_act = self.overview_dock.toggleViewAction()
        proj_overview_act.setShortcut(QC.Qt.ALT + QC.Qt.SHIFT + QC.Qt.Key_O)
        proj_overview_act.setStatusTip("Enable/disable the 'Overview' window")
        self.windows_menu.addAction(proj_overview_act)

        # VIEWING AREA DOCK WIDGET
        # Create the projection viewing area dock widget
        self.area_dock = ViewingAreaDockWidget(self)

        # Create an action for enabling/disabling the viewing area
        proj_area_act = self.area_dock.toggleViewAction()
        proj_area_act.setShortcut(QC.Qt.ALT + QC.Qt.SHIFT + QC.Qt.Key_V)
        proj_area_act.setStatusTip("Enable/disable the 'Viewing area' window")
        self.windows_menu.addAction(proj_area_act)

        # Create menubar
        self.create_menubar()
        self.menubar.setFocus()

        # Set resolution of window
        self.resize(800, 600)

        # Set all dock widgets to their default positions
        self.set_default_dock_positions()

        # Set the exception handler to an internal message window
        gui_excepthook = partial(show_exception_details, self)
        sys.excepthook = gui_excepthook

        # Turn off all regular logging in Pipeline
        self.was_logging = bool(self.pipe.do_logging)
        self.pipe._make_call('__setattr__', 'do_logging', False)

    # This function creates the menubar in the viewer
    def create_menubar(self):
        """
        Creates the top-level menubar of the main window.

        Other widgets can modify this menubar to add additional actions to it.

        """

        # Obtain menubar
        self.menubar = self.menuBar()

        # FILE
        # Create file menu
        file_menu = self.menubar.addMenu('&File')

        # Add save action to file menu
        save_act = QW_QAction(
            self, '&Save view as...',
            shortcut=QG.QKeySequence.Save,
            statustip="Save current projection viewing area as an image",
            triggered=self.area_dock.save_view,
            role=QW_QAction.ApplicationSpecificRole)
        file_menu.addAction(save_act)

        # Add quit action to file menu
        quit_act = QW_QAction(
            self, '&Quit',
            shortcut=QG.QKeySequence.Quit,
            statustip="Quit %s" % (APP_NAME),
            triggered=self.close,
            role=QW_QAction.QuitRole)
        file_menu.addAction(quit_act)

        # TOOLS
        # Create tools menu, which includes all actions in the proj_toolbar
        tools_menu = self.menubar.addMenu('&Tools')
        tools_menu.addActions(self.area_dock.proj_toolbar.actions())

        # VIEW
        # Create view menu
        view_menu = self.menubar.addMenu('&View')

        # Add the windows submenu to view menu
        view_menu.addMenu(self.windows_menu)

        # Add default layout action to view menu
        default_layout_act = QW_QAction(
            self, '&Default layout',
            statustip=("Reset all windows and toolbars back to their default "
                       "layout"),
            triggered=self.set_default_dock_positions)
        view_menu.addAction(default_layout_act)

        # Add a separator
        view_menu.addSeparator()

        # Add the toolbars submenu to view menu
        view_menu.addMenu(self.toolbars_menu)

        # HELP
        # Create help menu
        help_menu = self.menubar.addMenu('&Help')

        # Add options action to help menu
        self.options = OptionsDialog(self)
        options_act = QW_QAction(
            self, '&Preferences',
            shortcut=QC.Qt.CTRL + QC.Qt.Key_P,
            statustip="Adjust viewer preferences",
            triggered=self.options,
            role=QW_QAction.PreferencesRole)
        help_menu.addAction(options_act)

        # Add details action to help menu
        details_act = QW_QAction(
            self, '&Details',
            shortcut=QC.Qt.CTRL + QC.Qt.Key_D,
            statustip=("Show the pipeline details overview of a specified "
                       "iteration"),
            triggered=self.show_pipeline_details_overview,
            role=QW_QAction.ApplicationSpecificRole)
        help_menu.addAction(details_act)

        # Add a separator
        help_menu.addSeparator()

        # Add API reference action to help menu
        apiref_act = QW_QAction(
            self, 'API reference',
            statustip="Open %s's API reference in a webbrowser" % (APP_NAME),
            triggered=self.api_reference,
            role=QW_QAction.ApplicationSpecificRole)
        help_menu.addAction(apiref_act)

        # Add a separator
        help_menu.addSeparator()

        # Add about action to help menu
        about_act = QW_QAction(
            self, '&About...',
            statustip="About %s" % (APP_NAME),
            triggered=self.about,
            role=QW_QAction.AboutRole)
        help_menu.addAction(about_act)

        # Add aboutQt action to help menu
        aboutqt_act = QW_QAction(
            self, 'About &Qt...',
            statustip="About Qt framework",
            triggered=QW.QApplication.aboutQt,
            role=QW_QAction.AboutQtRole)
        help_menu.addAction(aboutqt_act)

    # This function creates the statusbar in the viewer
    def create_statusbar(self):
        """
        Creates the bottom-level statusbar of the main window, primarily used
        for displaying extended descriptions of actions.

        """

        # Obtain statusbar
        self.statusbar = self.statusBar()

    # This function creates a message box with the 'about' information
    @QC.Slot()
    @docstring_substitute(qt_slot=qt_slot_doc)
    def about(self):
        """
        Displays a small section with information about the GUI.

        %(qt_slot)s

        """

        # Make shortcuts for certain links
        github_repo = "https://github.com/1313e/PRISM"

        # Create the text for the 'about' dialog
        text = dedent(r"""
            <b>{name} | <a href="{github}">PRISM</a> v{version}</b><br>
            Copyright &copy; 2019 Ellert van der Velden<br>
            Distributed under the
            <a href="{github}/raw/master/LICENSE">BSD-3 License</a>.
            """.format(name=APP_NAME,
                       version=__version__,
                       github=github_repo))

        # Create the 'about' dialog
        QW.QMessageBox.about(self, "About %s" % (APP_NAME), text)

    # This function opens the RTD API reference documentation in a webbrowser
    @QC.Slot()
    @docstring_substitute(qt_slot=qt_slot_doc)
    def api_reference(self):
        """
        Opens the API reference documentation of the GUI in a webbrowser.

        %(qt_slot)s

        """

        # Open webbrowser
        QG.QDesktopServices.openUrl(QC.QUrl(
            "https://prism-tool.readthedocs.io/en/latest/api/prism._gui.html"))

    # This function is called when the viewer is closed
    def closeEvent(self, *args, **kwargs):
        """
        Special :meth:`~PyQt5.QtWidgets.QWidget.closeEvent` event that
        automatically performs some clean-up operations before the main window
        closes.

        """

        # Call the closeEvent of the dock widgets
        self.overview_dock.close()
        self.area_dock.close()

        # Save that Projection GUI is no longer being used
        self.all_set_proj_attr('use_GUI', 0)

        # Set data parameters in Projection class back to defaults
        self.options.reset_options()

        # Set logging in Pipeline back to what it was before
        self.pipe._make_call('__setattr__', 'do_logging', self.was_logging)

        # Set the excepthook back to its default value
        sys.excepthook = sys.__excepthook__

        # Close the main window
        super().closeEvent(*args, **kwargs)

    # This function allows for projection attributes to be set more easily
    def set_proj_attr(self, name, value):
        """
        Sets the requested :class:`~prism._projection.Projection` attribute
        `name` to `value` on the controller rank.

        """

        # Set the attribute
        setattr(self.pipe, '_Projection__%s' % (name), value)

    # This function is an MPI-version of set_proj_attr
    def all_set_proj_attr(self, name, value):
        """
        Sets the requested :class:`~prism._projection.Projection` attribute
        `name` to `value` on all ranks.

        """

        # Set the attribute on all ranks
        self.pipe._make_call('__setattr__', '_Projection__%s' % (name), value)

    # This function allows for projection attributes to be read more easily
    def get_proj_attr(self, name):
        """
        Gets the value of the requested :class:`~prism._projection.Projection`
        attribute `name` on the controller rank.

        """

        # Retrieve the attribute
        return(getattr(self.pipe, '_Projection__%s' % (name)))

    # This function allows for projection attributes to be called more easily
    def call_proj_attr(self, name, *args, **kwargs):
        """
        Calls the requested :class:`~prism._projection.Projection` attribute
        `name` using the provided `args` and `kwargs` on the controller rank.

        """

        # Call the attribute
        return(getattr(self.pipe, '_Projection__%s' % (name))(*args, **kwargs))

    # This function is an MPI-version of call_proj_attr
    def all_call_proj_attr(self, name, *args, **kwargs):
        """
        Calls the requested :class:`~prism._projection.Projection` attribute
        `name` using the provided `args` and `kwargs` on all ranks.

        """

        # Call the attribute on all ranks
        return(self.pipe._make_call('_Projection__%s' % (name),
                                    *args, **kwargs))

    # This function returns the default positions of dock widgets and toolbars
    def get_default_dock_positions(self):
        """
        Returns the default positions of all dock widgets connected to the main
        window.

        """

        # Make dict including the default docking positions
        default_pos = {
            'Viewing area': QC.Qt.RightDockWidgetArea,
            'Overview': QC.Qt.LeftDockWidgetArea}

        # Return it
        return(default_pos)

    # This function sets dock widgets and toolbars to their default position
    @QC.Slot()
    @docstring_substitute(qt_slot=qt_slot_doc)
    def set_default_dock_positions(self):
        """
        Sets the positions of all dock widgets connected to the main window to
        their default positions.

        %(qt_slot)s

        """

        # Set the dock widgets and toolbars to their default positions
        # OVERVIEW
        self.overview_dock.setVisible(True)
        self.overview_dock.setFloating(False)
        self.addDockWidget(self.default_pos['Overview'], self.overview_dock)

        # VIEWING AREA
        self.area_dock.setVisible(True)
        self.area_dock.setFloating(False)
        self.addDockWidget(self.default_pos['Viewing area'], self.area_dock)
        self.area_dock.set_default_dock_positions()

    # This function shows the details() overview of a given emulator iteration
    @QC.Slot()
    @docstring_substitute(qt_slot=qt_slot_doc)
    def show_pipeline_details_overview(self):
        """
        Creates and shows a dialog containing the output of the
        :meth:`~prism.Pipeline.details` method for all emulator iterations.

        %(qt_slot)s

        """

        # Make a details dialog
        details_box = QW.QDialog(self)
        details_box.setWindowModality(QC.Qt.NonModal)
        details_box.setAttribute(QC.Qt.WA_DeleteOnClose)
        details_box.setWindowFlags(
            QC.Qt.MSWindowsOwnDC |
            QC.Qt.Window |
            QC.Qt.WindowTitleHint |
            QC.Qt.WindowSystemMenuHint |
            QC.Qt.WindowCloseButtonHint)
        details_box.setWindowTitle("%s: Pipeline details" % (APP_NAME))

        # Create a layout for this dialog
        layout = QW.QVBoxLayout(details_box)

        # Obtain the latest emul_i
        emul_i = self.pipe._emulator._emul_i

        # Create a details tab widget
        tab_widget = QW.QTabWidget()
        tab_widget.setCornerWidget(QW.QLabel("Emulator iteration:"),
                                   QC.Qt.TopLeftCorner)
        tab_widget.setUsesScrollButtons(True)
        tab_widget.setStyleSheet(
            """
            QTabWidget::pane {
                padding: 1px;}
            QTabWidget::tab-bar {
                left: 10px;}
            """)
        layout.addWidget(tab_widget)

        # Loop over all emulator iterations and add their pages
        for i in range(1, emul_i+1):
            # Initialize a StringIO stream to capture the output with
            with StringIO() as string_stream:
                # Use this stream to capture the overview of details()
                with redirect_stdout(string_stream):
                    # Obtain the details at specified emulator iteration
                    self.pipe._make_call('details', i)

                # Save the entire string stream as a separate object
                details = string_stream.getvalue()

            # Strip gathered details of all whitespaces
            details = details.strip()

            # Cut everything off before "GENERAL"
            index = details.find("GENERAL")
            details = details[index:]

            # Now split details up line-by-line
            details = details.splitlines()

            # Remove all empty lines in details
            while True:
                try:
                    details.remove('')
                except ValueError:
                    break

            # Search for lines that are in all caps, which are group titles
            group_idx = [j for j, line in enumerate(details)
                         if line.isupper() and line[0].isalpha()]
            group_idx.append(-1)

            # Create an empty ordered dict with groups
            groups = odict()

            # Split details at these indices
            for j, k in zip(group_idx[:-1], group_idx[1:]):
                # Extract the part of the list for this group
                group = details[j:k]

                # Extract the name and entry for this group
                name = group[0].capitalize()
                entry = group[1:]

                # If first or last lines contain dashes, remove
                if(entry[-1].count('-') == len(entry[-1])):
                    entry.pop(-1)
                if(entry[0].count('-') == len(entry[0])):
                    entry.pop(0)

                # Loop over all remaining lines in entry and split at \t
                entry = [line.split('\t') for line in entry]

                # Add this group entry to the dict
                groups[name] = entry

            # Make a details layout
            details_layout = QW.QVBoxLayout()
            details_layout.setSizeConstraint(QW.QLayout.SetFixedSize)

            # Make QGroupBoxes for all groups
            for name, entry in groups.items():
                # Make a QGroupBox for this group
                group = QW.QGroupBox(name)
                details_layout.addWidget(group)
                group_layout = QW.QFormLayout()
                group.setLayout(group_layout)

                # Loop over all lines in this group's entry
                for line in entry:
                    # If line is a list with one element, it spans both columns
                    if(len(line) == 1):
                        # Extract this one element
                        line = line[0]

                        # If line is solely dashes, add a separator
                        if(line.count('-') == len(line)):
                            sep_line = QW.QFrame()
                            sep_line.setFrameShape(sep_line.HLine)
                            sep_line.setFrameShadow(sep_line.Sunken)
                            group_layout.addRow(sep_line)
                        # If not, add line as a QLabel
                        else:
                            group_layout.addRow(QW.QLabel(line))
                    # Else, it contains two elements
                    else:
                        group_layout.addRow(*map(QW.QLabel, line))

            # Add a stretch to the layout
            details_layout.addStretch()

            # Add this details_layout to a new widget
            details_tab = QW.QWidget(details_box)
            details_tab.setLayout(details_layout)

            # Add the tab to a scrollarea
            scrollarea = QW.QScrollArea(details_box)
            scrollarea.setFrameStyle(QW.QFrame.NoFrame)
            scrollarea.setContentsMargins(0, 0, 0, 0)
            scrollarea.setWidgetResizable(True)
            scrollarea.setHorizontalScrollBarPolicy(QC.Qt.ScrollBarAlwaysOff)
            scrollarea.horizontalScrollBar().setEnabled(False)
            scrollarea.setWidget(details_tab)

            # Set size constraints on the scrollarea
            scrollarea.setMaximumHeight(details_tab.height() + 2)

            # Add it to the tab_widget
            tab_widget.addTab(scrollarea, "&%i" % (i))

        # Set size constraints on the details box
        details_box.setFixedWidth(tab_widget.sizeHint().width() +
                                  scrollarea.verticalScrollBar().width() +
                                  layout.contentsMargins().left() +
                                  layout.contentsMargins().right())
        details_box.setMaximumHeight(scrollarea.maximumHeight() +
                                     tab_widget.tabBar().sizeHint().height() +
                                     layout.contentsMargins().top() +
                                     layout.contentsMargins().bottom())

        # Show the details message box
        details_box.show()
示例#10
0
class OptionsDialog(QW.QDialog):
    """
    Defines the :class:`~OptionsDialog` class for the Projection GUI.

    This class provides both the 'Preferences' dialog and the functions that
    are required to load; save; set; and change them.

    """

    # Create saving, resetting and discarding signals
    saving = QC.Signal()
    resetting = QC.Signal()
    discarding = QC.Signal()

    @e13.docstring_substitute(optional=kwargs_doc.format(
        'PyQt5.QtWidgets.QDialog'))
    def __init__(self, main_window_obj, *args, **kwargs):
        """
        Initialize an instance of the :class:`~OptionsDialog` class.

        Parameters
        ----------
        main_window_obj : :obj:`~prism._gui.widgets.MainViewerWindow` object
            Instance of the :class:`~prism._gui.widgets.MainViewerWindow` class
            that acts as the parent of this dialog.

        %(optional)s

        """

        # Save provided MainWindow object
        self.main = main_window_obj
        self.pipe = self.main.pipe
        self.n_par = self.main.n_par
        self.set_proj_attr = self.main.set_proj_attr
        self.all_set_proj_attr = self.main.all_set_proj_attr
        self.get_proj_attr = self.main.get_proj_attr
        self.call_proj_attr = self.main.call_proj_attr
        self.all_call_proj_attr = self.main.all_call_proj_attr

        # Call super constructor
        super().__init__(self.main, *args, **kwargs)

        # Create the options window
        self.init()

    # This function shows the options window
    @QC.Slot()
    def __call__(self):
        """
        Qt slot that shows the options dialog in the center of the main window.

        """

        # Show it
        self.show()

        # Move the options window to the center of the main window
        self.move(self.main.geometry().center()-self.rect().center())

    # This function overrides the closeEvent method
    def closeEvent(self, *args, **kwargs):
        """
        Special :meth:`~PyQt5.QtWidgets.QWidget.closeEvent` event that makes
        sure that all dialogs will be closed related to the options menu, and
        discards all changes made.

        """

        # Make sure the kwargs dict dialog is closed
        self.dict_dialog.close()

        # Close the window
        super().closeEvent(*args, **kwargs)

        # Set all option boxes back to their saved values
        self.discard_options()

    # This function creates the options window
    def init(self):
        """
        Sets up the options dialog after it has been initialized.

        This function is mainly responsible for initializing all option entries
        that the GUI has, and creating a database for them. It also creates the
        layout of the options dialog.

        """

        # Create a window layout
        window_layout = QW.QVBoxLayout(self)

        # Create a tab widget
        window_tabs = QW.QTabWidget()
        window_layout.addWidget(window_tabs)

        # Create a options dict
        self.option_entries = sdict()

        # Define list with all tabs that should be available in what order
        option_tabs = ['general', 'appearance']

        # Include all tabs named in options_tabs
        for tab in option_tabs:
            window_tabs.addTab(*getattr(self, 'create_tab_%s' % (tab))())

        # Also add the buttons
        self.create_group_buttons(window_layout)

        # Set a few properties of options window
        self.setWindowModality(QC.Qt.WindowModal)           # Modality
        self.setWindowTitle("Preferences")                  # Title

        # Add a new method to self.main
        self.main.get_option = self.get_option

    # This function returns the value of a specific option
    def get_option(self, name):
        """
        Returns the value of the option entry associated with the given `name`.

        """

        return(self.option_entries[name].value)

    # This function creates a new options entry
    def create_entry(self, name, box, default):
        """
        Creates a new :class:`~OptionsEntry` instance, using the provided
        `name`, `box` and `default`, and registers it in the options dialog.

        Parameters
        ----------
        name : str
            The name of this options entry.
        box : :obj:`~PyQt5.QtWidgets.QWidget` object
            The widget that will hold the values of this entry.
        default : object
            The default value of this entry.

        """

        # Create new options entry
        entry = OptionsEntry(self, name, box, default)

        # Connect box signals
        get_modified_box_signal(box).connect(self.enable_save_button)

        # Connect entry slots
        self.saving.connect(entry.save_value)
        self.resetting.connect(entry.reset_value)
        self.discarding.connect(entry.discard_value)

        # Add new entry to option_entries
        self.option_entries[name] = entry

    # This function creates a new tab
    def create_tab(self, name, groups_list):
        """
        Creates a new options tab with the given `name` and adds the groups
        defined in `groups_list` to it.

        This function acts as a base function called by `create_tab_`
        functions.

        Parameters
        ----------
        name : str
            The name of this options tab.
        groups_list : list of str
            A list containing the names of all option groups that need to be
            added to this tab.

        Returns
        -------
        tab : :obj:`~PyQt5.QtWidgets.QWidget` object
            The created options tab.
        name : str
            The name of this options tab as provided with `name`.
            This variable is mainly returned such that it is easier to pass tab
            names between functions.

        """

        # Create a tab
        tab = QW.QWidget()
        layout = QW.QVBoxLayout()
        tab.setLayout(layout)

        # Include all groups named in groups_list
        for group in groups_list:
            layout.addWidget(getattr(self, 'create_group_%s' % (group))())

        # Add a stretch
        layout.addStretch()

        # Return tab
        return(tab, name)

    # This function creates a new group
    def create_group(self, name, options_list):
        """
        Creates a new option group with the given `name` and adds the options
        defined in `options_list` to it.

        This function acts as a base function called by `create_group_`
        functions.

        Parameters
        ----------
        name : str
            The name of this option group.
        options_list : list of str
            A list containing the names of all options that need to be added to
            this group.

        Returns
        -------
        group : :obj:`~PyQt5.QtWidgets.QGroupBox` object
            The created option group.

        """

        # Create a group
        group = QW.QGroupBox(name)
        layout = QW.QFormLayout()
        group.setLayout(layout)

        # Include all options named in options_list
        for option in options_list:
            layout.addRow(*getattr(self, 'create_option_%s' % (option))())

        # Return group
        return(group)

    # GENERAL TAB
    def create_tab_general(self):
        """
        Creates the 'General' tab and returns it.

        """

        self.proj_defaults = sdict(self.get_proj_attr('proj_kwargs'))
        self.proj_keys = list(self.proj_defaults.keys())
        self.proj_keys.remove('align')
        self.proj_keys.extend(['align_col', 'align_row'])
        return(self.create_tab("General", ['proj_grid', 'proj_kwargs']))

    # INTERFACE TAB
    def create_tab_appearance(self):
        """
        Creates the 'Appearance' tab and returns it.

        """

        return(self.create_tab("Appearance", ['interface']))

    # PROJ_GRID GROUP
    def create_group_proj_grid(self):
        """
        Creates the 'Projection grid' group and returns it.

        """

        return(self.create_group("Projection grid",
                                 ['proj_res', 'proj_depth']))

    # PROJ_KWARGS GROUP
    def create_group_proj_kwargs(self):
        """
        Creates the 'Projection keywords' group and returns it.

        """

        return(self.create_group("Projection keywords",
                                 ['align', 'show_cuts', 'smooth',
                                  'use_par_space', 'full_impl_rng',
                                  'kwargs_dicts']))

    # INTERFACE GROUP
    def create_group_interface(self):
        """
        Creates the 'Interface' group and returns it.

        """

        return(self.create_group("Interface", ['auto_show', 'auto_tile',
                                               'progress_dialog']))

    # FONTS GROUP
    def create_group_fonts(self):   # pragma: no cover
        """
        Creates the 'Fonts' group and returns it.

        """

        return(self.create_group("Fonts", ['text_fonts']))

    # TEXT_FONTS OPTION
    # TODO: Further implement this
    def create_option_text_fonts(self):     # pragma: no cover
        """
        Creates the 'text_fonts' option and returns it.

        This option allows for the fonts used in the GUI to be modified.

        """

        # PLAIN TEXT
        # Create a font families combobox
        plain_box = QW.QFontComboBox()
        plain_box.setFontFilters(QW.QFontComboBox.MonospacedFonts)
        plain_box.setEditable(True)
        plain_box.setInsertPolicy(plain_box.NoInsert)
        plain_box.completer().setCompletionMode(QW.QCompleter.PopupCompletion)

        # Create a font size spinbox
        plain_size = QW_QSpinBox()
        plain_size.setRange(7, 9999999)
        plain_size.setSuffix(" pts")

        # RICH TEXT
        # Create a font families combobox
        rich_box = QW.QFontComboBox()
        rich_box.setEditable(True)
        rich_box.setInsertPolicy(rich_box.NoInsert)
        rich_box.completer().setCompletionMode(QW.QCompleter.PopupCompletion)

        # Create a font size spinbox
        rich_size = QW_QSpinBox()
        rich_size.setRange(7, 9999999)
        rich_size.setSuffix(" pts")

        # Create a grid for the families and size boxes
        font_grid = QW.QGridLayout()
        font_grid.setColumnStretch(1, 2)
        font_grid.setColumnStretch(3, 1)

        # Add everything to this grid
        font_grid.addWidget(QW.QLabel("Plain text:"), 0, 0)
        font_grid.addWidget(plain_box, 0, 1)
        font_grid.addWidget(QW.QLabel("Size:"), 0, 2)
        font_grid.addWidget(plain_size, 0, 3)
        font_grid.addWidget(QW.QLabel("Rich text:"), 1, 0)
        font_grid.addWidget(rich_box, 1, 1)
        font_grid.addWidget(QW.QLabel("Size:"), 1, 2)
        font_grid.addWidget(rich_size, 1, 3)

        font_grid.addWidget(QW.QLabel("NOTE: Does not work yet"), 2, 0, 1, 4)

        # Return the grid
        return(font_grid,)

    # DPI OPTION
    # TODO: Further implement this one as well
    def create_option_dpi(self):    # pragma: no cover
        """
        Creates the 'dpi' option and returns it.

        This option allows for the DPI used in the GUI to be modified.

        """

        # Make a checkbox for setting a custom DPI scaling
        dpi_check = QW.QCheckBox("Custom DPI scaling:")
        dpi_check.setToolTip("Set this to enable custom DPI scaling of the "
                             "GUI")
        self.create_entry('dpi_flag', dpi_check, False)

        # Make a spinbox for setting the DPI scaling
        dpi_box = QW_QDoubleSpinBox()
        dpi_box.setRange(0, 100)
        dpi_box.setSuffix("x")
        dpi_box.setSpecialValueText("Auto")
        dpi_box.setToolTip("Custom DPI scaling factor to use. "
                           "'1.0' is no scaling. "
                           "'Auto' is automatic scaling.")
        dpi_check.toggled.connect(dpi_box.setEnabled)
        dpi_box.setEnabled(False)
        self.create_entry('dpi_scaling', dpi_box, 1.0)

        # Return DPI box
        return(dpi_check, dpi_box)

    # AUTO_TILE OPTION
    def create_option_auto_tile(self):
        """
        Creates the 'auto_tile' option and returns it.

        This option sets whether the projection subwindows are automatically
        tiled.

        """

        # Make check box for auto tiling
        auto_tile_box = QW.QCheckBox("Auto-tile subwindows")
        auto_tile_box.setToolTip("Set this to automatically tile all "
                                 "projection subwindows whenever a new one is "
                                 "added")
        self.create_entry('auto_tile', auto_tile_box, True)

        # Return auto_tile box
        return(auto_tile_box,)

    # AUTO_SHOW OPTION
    def create_option_auto_show(self):
        """
        Creates the 'auto_show' option and returns it.

        This option sets whether the projection subwindows are automatically
        shown whenever created.

        """

        # Make check box for auto showing projection figures/subwindows
        auto_show_box = QW.QCheckBox("Auto-show subwindows")
        auto_show_box.setToolTip("Set this to automatically show a projection "
                                 "subwindow after it has been drawn")
        self.create_entry('auto_show', auto_show_box, True)

        # Return auto_show box
        return(auto_show_box,)

    # PROGRESS_DIALOG OPTION
    def create_option_progress_dialog(self):
        """
        Creates the 'progress_dialog' option and returns it.

        This option sets whether a threaded progress dialog is used for some
        operations.

        """

        # Make check box for using a threaded progress dialog
        progress_dialog_box = QW.QCheckBox("Use threaded progress dialog")
        progress_dialog_box.setToolTip(
            "Set this to use a threaded progress dialog whenever projections "
            "are created or drawn.\nThis allows for the operation to be "
            "monitored and/or aborted, but also slows down the execution")
        self.create_entry('use_progress_dialog', progress_dialog_box, True)

        # Return progress_dialog box
        return(progress_dialog_box,)

    # PROJ_RES OPTION
    def create_option_proj_res(self):
        """
        Creates the 'proj_res' option and returns it.

        This option sets the value of the 'proj_res' projection parameter.

        """

        # Make spinbox for option proj_res
        proj_res_box = QW_QSpinBox()
        proj_res_box.setRange(0, 9999999)
        proj_res_box.setToolTip(proj_res_doc)
        self.create_entry('proj_res', proj_res_box,
                          self.proj_defaults['proj_res'])

        # Return resolution box
        return('Resolution:', proj_res_box)

    # PROJ_DEPTH OPTION
    def create_option_proj_depth(self):
        """
        Creates the 'proj_depth' option and returns it.

        This option sets the value of the 'proj_depth' projection parameter.

        """

        # Make spinbox for option proj_depth
        proj_depth_box = QW_QSpinBox()
        proj_depth_box.setRange(0, 9999999)
        proj_depth_box.setToolTip(proj_depth_doc)
        self.create_entry('proj_depth', proj_depth_box,
                          self.proj_defaults['proj_depth'])

        # Return depth box
        return('Depth:', proj_depth_box)

    # ALIGN OPTION
    def create_option_align(self):
        """
        Creates the 'align' option and returns it.

        This option sets the value of the 'align' projection parameter.

        """

        # Column align
        align_col_box = QW.QRadioButton('Column')
        align_col_box.setToolTip("Align the projection subplots in a single "
                                 "column")
        self.create_entry('align_col', align_col_box,
                          self.proj_defaults['align'] == 'col')

        # Row align
        align_row_box = QW.QRadioButton('Row')
        align_row_box.setToolTip("Align the projection subplots in a single "
                                 "row")
        self.create_entry('align_row', align_row_box,
                          self.proj_defaults['align'] == 'row')

        # Create layout for align and add it to options layout
        align_box = QW.QHBoxLayout()
        align_box.addWidget(align_col_box)
        align_box.addWidget(align_row_box)
        align_box.addStretch()

        # Return alignment box
        return('Alignment:', align_box)

    # SHOW_CUTS OPTION
    def create_option_show_cuts(self):
        """
        Creates the 'show_cuts' option and returns it.

        This option sets the value of the 'show_cuts' projection parameter.

        """

        # Make check box for show_cuts
        show_cuts_box = QW.QCheckBox()
        show_cuts_box.setToolTip("Enable/disable showing all implausibility "
                                 "cut-off lines in 2D projections")
        self.create_entry('show_cuts', show_cuts_box,
                          self.proj_defaults['show_cuts'])

        # Return shot_cuts box
        return('Show cuts?', show_cuts_box)

    # SMOOTH OPTION
    def create_option_smooth(self):
        """
        Creates the 'smooth' option and returns it.

        This option sets the value of the 'smooth' projection parameter.

        """

        # Make check box for smooth
        smooth_box = QW.QCheckBox()
        smooth_box.setToolTip("Enable/disable smoothing the projections. When "
                              "smoothed, the minimum implausibility is forced "
                              "to be above the first cut-off for implausible "
                              "regions")
        self.create_entry('smooth', smooth_box, self.proj_defaults['smooth'])

        # Return smooth box
        return('Smooth?', smooth_box)

    # USE_PAR_SPACE OPTION
    def create_option_use_par_space(self):
        """
        Creates the 'use_par_space' option and returns it.

        This option sets the value of the 'use_par_space' projection parameter.

        """

        # Make check box for use_par_space
        use_par_space_box = QW.QCheckBox()
        use_par_space_box.setToolTip("Enable/disable using the model parameter"
                                     " space as the axes limits.")
        self.create_entry('use_par_space', use_par_space_box,
                          self.proj_defaults['use_par_space'])

        # Return use_par_space box
        return('Use parameter space?', use_par_space_box)

    # FULL_IMPL_RNG OPTION
    def create_option_full_impl_rng(self):
        """
        Creates the 'full_impl_rng' option and returns it.

        This option sets the value of the 'full_impl_rng' projection parameter.

        """

        # Make check box for full_impl_rng
        full_impl_rng_box = QW.QCheckBox()
        full_impl_rng_box.setToolTip("Enable/disable using the full "
                                     "implausibility value range as the axis "
                                     "limit.")
        self.create_entry('full_impl_rng', full_impl_rng_box,
                          self.proj_defaults['full_impl_rng'])

        # Return full_impl_rng box
        return('Use full implausibility range?', full_impl_rng_box)

    # KWARGS_DICTS OPTION
    def create_option_kwargs_dicts(self):
        """
        Creates the 'kwargs_dicts' option and returns it.

        This option allows for the
        :class:`~prism._gui.widgets.preferences.KwargsDictDialog` to be shown
        to the user.
        This dialog is able to set the values of all 'XXX_kwargs' projection
        parameters.

        """

        # Create a kwargs_dict_box
        kwargs_dict_box = KwargsDictBoxLayout(self)
        self.kwargs_dict_box = kwargs_dict_box

        # Add all kwargs_dicts to it
        # FIG_KWARGS
        tooltip = ("Keyword arguments used when creating the subplots figure "
                   "(<i>plt.figure</i> kwargs)")
        kwargs_dict_box.add_dict(
            "Figure", 'fig_kwargs', tooltip,
            std_entries=['dpi'],
            banned_entries=self.get_proj_attr('pop_fig_kwargs'))

        # IMPL_KWARGS_2D
        tooltip = ("Keyword arguments used for making the 2D minimum "
                   "implausibility plot (<i>plt.plot</i> kwargs)")
        kwargs_dict_box.add_dict(
            "2D implausibility", 'impl_kwargs_2D', tooltip,
            std_entries=['linestyle', 'linewidth', 'marker', 'markersize',
                         'color', 'alpha'],
            banned_entries=[*self.get_proj_attr('pop_plt_kwargs'), 'cmap'])

        # IMPL_KWARGS_3D
        tooltip = ("Keyword arguments used for making the 3D minimum "
                   "implausibility plot (<i>plt.hexbin</i> kwargs)")
        kwargs_dict_box.add_dict(
            "3D implausibility", 'impl_kwargs_3D', tooltip,
            std_entries=['cmap', 'alpha', 'xscale', 'yscale'],
            banned_entries=self.get_proj_attr('pop_plt_kwargs'))

        # LOS_KWARGS_2D
        tooltip = ("Keyword arguments used for making the 2D line-of-sight "
                   "plot (<i>plt.plot</i> kwargs)")
        kwargs_dict_box.add_dict(
            "2D line-of-sight", 'los_kwargs_2D', tooltip,
            std_entries=['linestyle', 'linewidth', 'marker', 'markersize',
                         'color', 'alpha'],
            banned_entries=[*self.get_proj_attr('pop_plt_kwargs'), 'cmap'])

        # LOS_KWARGS_3D
        tooltip = ("Keyword arguments used for making the 3D line-of-sight "
                   "plot (<i>plt.hexbin</i> kwargs)")
        kwargs_dict_box.add_dict(
            "3D line-of-sight", 'los_kwargs_3D', tooltip,
            std_entries=['cmap', 'alpha', 'xscale', 'yscale'],
            banned_entries=self.get_proj_attr('pop_plt_kwargs'))

        # LINE_KWARGS_EST
        tooltip = ("Keyword arguments used for drawing the parameter estimate "
                   "lines (<i>plt.plot</i> kwargs)")
        kwargs_dict_box.add_dict(
            "Estimate lines", 'line_kwargs_est', tooltip,
            std_entries=['linestyle', 'color', 'alpha', 'linewidth'],
            banned_entries=self.get_proj_attr('pop_line_kwargs'))

        # ARROW_KWARGS_EST
        tooltip = ("Keyword arguments used for drawing the parameter estimate "
                   "arrows (<i>plt.arrow</i> kwargs)")
        kwargs_dict_box.add_dict(
            "Estimate arrows", 'arrow_kwargs_est', tooltip,
            std_entries=['color', 'alpha', 'fh_arrowlength', 'ft_arrowlength',
                         'fh_arrowwidth', 'ft_arrowwidth', 'rel_xpos',
                         'rel_ypos'],
            banned_entries=self.get_proj_attr('pop_arrow_kwargs'))

        # LINE_KWARGS_CUT
        tooltip = ("Keyword arguments used for drawing the implausibility "
                   "cut-off line(s) in 2D projections (<i>plt.plot</i> kwargs)"
                   )
        kwargs_dict_box.add_dict(
            "Cut-off lines", 'line_kwargs_cut', tooltip,
            std_entries=['linestyle', 'color', 'alpha', 'linewidth'],
            banned_entries=self.get_proj_attr('pop_line_kwargs'))

        # Return kwargs_dict box
        return('Projection keyword dicts:', kwargs_dict_box)

    # BUTTONS GROUP
    def create_group_buttons(self, window_layout):
        """
        Creates the button box that is shown at the bottom of the options
        dialog and registers it in the provided `window_layout`.

        """

        # Create a button_box
        button_box = QW.QDialogButtonBox()
        window_layout.addWidget(button_box)

        # Make a 'Reset' button
        reset_but = button_box.addButton(button_box.Reset)
        reset_but.setToolTip("Reset to defaults")
        reset_but.clicked.connect(self.reset_options)
        self.reset_but = reset_but

        # Make an 'Apply' button
        save_but = button_box.addButton(button_box.Apply)
        save_but.setToolTip("Apply changes")
        save_but.clicked.connect(self.save_options)
        save_but.setEnabled(False)
        self.save_but = save_but

        # Make a 'Close' button
        close_but = button_box.addButton(button_box.Close)
        close_but.setToolTip("Close without saving")
        close_but.clicked.connect(self.close)
        close_but.setDefault(True)
        self.close_but = close_but

    # This function saves the new options values
    @QC.Slot()
    @e13.docstring_substitute(qt_slot=qt_slot_doc)
    def save_options(self):
        """
        Saves all current values of all option entries.

        Option entries that affect projection parameters are automatically
        modified as well.

        %(qt_slot)s

        """

        # Emit the saving signal
        self.saving.emit()

        # Save all new values
        for key, entry in self.option_entries.items():
            # If key is a projection parameter, save it in the Pipeline as well
            if key in self.proj_keys:
                # Align
                if key in ['align_col', 'align_row']:
                    if entry.box.isChecked():
                        self.set_proj_attr('align', key[6:])
                else:
                    self.set_proj_attr(key, entry.value)

        # Disable the save button
        self.disable_save_button()

    # This function enables the save button
    @QC.Slot()
    def enable_save_button(self):
        """
        Qt slot that enables the save button at the bottom of the options
        dialog.
        The save button is enabled if at least one change has been made to any
        option entry.

        """

        self.save_but.setEnabled(True)

    # This function disables the save button
    @QC.Slot()
    def disable_save_button(self):
        """
        Qt slot that disables the save button at the bottom of the options
        dialog.
        The save button is disabled whenever no changes have been made to any
        option entry.

        """

        self.save_but.setEnabled(False)

    # This function resets the options to default
    @QC.Slot()
    @e13.docstring_substitute(qt_slot=qt_slot_doc)
    def reset_options(self):
        """
        Resets the saved and current values of all option entries back to their
        default values.

        %(qt_slot)s

        """

        # Emit the resetting signal
        self.resetting.emit()

        # Save current options
        self.save_options()

    # This function discards all changes to the options
    @QC.Slot()
    @e13.docstring_substitute(qt_slot=qt_slot_doc)
    def discard_options(self):
        """
        Discards the current values of all option entries and sets them back to
        their saved values.

        %(qt_slot)s

        """

        # Emit the discarding signal
        self.discarding.emit()

        # Disable the save button
        self.disable_save_button()
示例#11
0
class ViewingAreaDockWidget(QW.QDockWidget):
    """
    Defines the :class:`~ViewingAreaDockWidget` class for the Projection GUI.

    This class provides the user with an MDI (Multiple Document Interface) area
    using the :class:`~PyQt5.QtWidgets.QMdiArea` class. All drawn projection
    figures live in this area and can be interacted with.

    """
    @e13.docstring_substitute(
        optional=kwargs_doc.format('PyQt5.QtWidgets.QDockWidget'))
    def __init__(self, main_window_obj, *args, **kwargs):
        """
        Initialize an instance of the :class:`~ViewingAreaDockWidget` class.

        Parameters
        ----------
        main_window_obj : :obj:`~prism._gui.widgets.MainViewerWindow` object
            Instance of the :class:`~prism._gui.widgets.MainViewerWindow` class
            that acts as the parent of this dock widget.

        %(optional)s

        """

        # Save provided MainWindow object
        self.main = main_window_obj
        self.pipe = self.main.pipe
        self.set_proj_attr = self.main.set_proj_attr
        self.all_set_proj_attr = self.main.all_set_proj_attr
        self.get_proj_attr = self.main.get_proj_attr
        self.call_proj_attr = self.main.call_proj_attr
        self.all_call_proj_attr = self.main.all_call_proj_attr

        # Call super constructor
        super().__init__("Viewing area", self.main, *args, **kwargs)

        # Create the projection viewing area
        self.init()

    # This function creates the main projection viewing area
    def init(self):
        """
        Sets up the projection viewing area dock widget after it has been
        initialized.

        This function is mainly responsible for enabling the
        :class:`~prism._gui.widgets.OverviewDockWidget` to properly interact
        and control the projection figures that have been drawn.

        """

        # Create an MdiArea for the viewing area
        self.area_window = QW.QMainWindow()
        self.proj_area = QW.QMdiArea(self)
        self.area_window.setCentralWidget(self.proj_area)
        self.proj_area.setFocus()
        self.setWidget(self.area_window)

        # Options for proj_area
        self.proj_area.setViewMode(QW.QMdiArea.SubWindowView)
        self.proj_area.setOption(QW.QMdiArea.DontMaximizeSubWindowOnActivation)
        self.proj_area.setActivationOrder(QW.QMdiArea.StackingOrder)
        self.proj_area.setStatusTip("Main projection viewing area")

        # Options for area_window
        self.area_window.setAttribute(QC.Qt.WA_DeleteOnClose)
        self.area_window.setContextMenuPolicy(QC.Qt.NoContextMenu)

        # Obtain dict of default docking positions
        self.default_pos = self.get_default_dock_positions()

        # Add toolbar to the projection viewer
        self.create_projection_toolbar()

    # This function saves the current state of the viewer to file
    # TODO: See if the window frames can be removed from the saved image
    @QC.Slot()
    @e13.docstring_substitute(qt_slot=qt_slot_doc)
    def save_view(self):
        """
        Saves the current view of the viewing area to file.

        %(qt_slot)s

        """

        # Get dict of all file extensions allowed
        exts = sdict({
            'Portable Network Graphics': "*.png",
            'Joint Photographic Experts Group': "*.jpg *.jpeg",
            'Windows Bitmap': "*.bmp",
            'Portable Pixmap': "*.ppm",
            'X11 Bitmap': "*.xbm",
            'X11 Pixmap': "*.xpm"
        })

        # Set default extension
        default_ext = '*.png'

        # Initialize empty list of filters and default filter
        file_filters = []
        default_filter = None

        # Obtain list with the different file filters
        for name, ext in exts.items():
            # Create proper string layout for this filter
            file_filter = "%s (%s)" % (name, ext)
            file_filters.append(file_filter)

            # If this extension is the default one, save it as such
            if default_ext in file_filter:
                default_filter = file_filter

        # Add 'All (Image) Files' filter to the list of filters for convenience
        file_filters.append("All Image Files (%s)" % (' '.join(exts.values())))
        file_filters.append("All Files (*)")

        # Combine list into a single string
        file_filters = ';;'.join(file_filters)

        # Create an OS-dependent options dict
        options = {}

        # Do not use Linux' native dialog as it is bad on some dists
        if platform.startswith('linux'):
            options = {'options': QW.QFileDialog.DontUseNativeDialog}

        # Open the file saving system
        filename, _ = QW.QFileDialog.getSaveFileName(
            parent=self.main,
            caption="Save view as...",
            directory=path.join(self.pipe._working_dir, "proj_area.png"),
            filter=file_filters,
            initialFilter=default_filter,
            **options)

        # If filename was provided, save image
        if filename:
            # Grab the current state of the projection area as a Pixmap
            pixmap = self.proj_area.grab()

            # Save pixmap with chosen filename
            pixmap.save(filename)

    # This function is called when the main window is closed
    def closeEvent(self, *args, **kwargs):
        """
        Special :meth:`~PyQt5.QtWidgets.QWidget.closeEvent` event that
        automatically performs some clean-up operations before the viewing area
        closes.

        """

        # Close the main window in this widget
        self.area_window.close()

        # Close the projection viewer
        super().closeEvent(*args, **kwargs)

    # This function returns the default positions of dock widgets and toolbars
    def get_default_dock_positions(self):
        """
        Returns the default positions of all dock widgets connected to the
        viewing area.

        """

        # Make dict including the default docking positions
        default_pos = {'Tools': QC.Qt.TopToolBarArea}

        # Return it
        return (default_pos)

    # This function sets dock widgets and toolbars to their default position
    @QC.Slot()
    @e13.docstring_substitute(qt_slot=qt_slot_doc)
    def set_default_dock_positions(self):
        """
        Sets the postions of all dock widgets connected to the viewing area to
        their default positions.

        %(qt_slot)s

        """

        # Set the dock widgets and toolbars to their default positions
        # TOOLS TOOLBAR
        self.proj_toolbar.setVisible(True)
        self.area_window.addToolBar(self.default_pos['Tools'],
                                    self.proj_toolbar)

    # This function creates the toolbar of the projection viewing area
    def create_projection_toolbar(self):
        """
        Creates the top-level toolbar of the viewing area, primarily used for
        manipulating the area subwindows.

        """

        # Create toolbar for projection viewer
        self.proj_toolbar = QW_QToolBar(self, "Tools")

        # Create an action for enabling/disabling the toolbar
        proj_toolbar_act = self.proj_toolbar.toggleViewAction()
        proj_toolbar_act.setText("Tools toolbar")
        proj_toolbar_act.setStatusTip("Enable/disable the 'Tools' toolbar")
        self.main.toolbars_menu.addAction(proj_toolbar_act)

        # Add action for cascading all subwindows
        cascade_act = QW_QAction(self,
                                 "&Cascade",
                                 shortcut=QC.Qt.CTRL + QC.Qt.SHIFT +
                                 QC.Qt.Key_C,
                                 statustip="Cascade all subwindows",
                                 triggered=self.proj_area.cascadeSubWindows)
        self.proj_toolbar.addAction(cascade_act)

        # Add action for tiling all subwindows
        tile_act = QW_QAction(self,
                              "&Tile",
                              shortcut=QC.Qt.CTRL + QC.Qt.SHIFT + QC.Qt.Key_T,
                              statustip="Tile all subwindows",
                              triggered=self.proj_area.tileSubWindows)
        self.proj_toolbar.addAction(tile_act)

        # Add action for closing all subwindows
        close_act = QW_QAction(self,
                               "Close all",
                               shortcut=QC.Qt.CTRL + QC.Qt.SHIFT + QC.Qt.Key_X,
                               statustip="Close all subwindows",
                               triggered=self.proj_area.closeAllSubWindows)
        self.proj_toolbar.addAction(close_act)