Source code for prism._gui.widgets.preferences.kwargs_dicts

# -*- coding: utf-8 -*-

"""
GUI Kwargs Dicts Options
========================
Provides a custom :class:`~PyQt5.QtWidgets.QDialog` subclass that allows for
the projection keyword argument dicts to be modified properly in the Projection
GUI preferences.

"""


# %% IMPORTS
# Package imports
import e13tools as e13
from matplotlib import rcParams
from matplotlib.lines import lineMarkers, lineStyles
from qtpy import QtCore as QC, QtGui as QG, QtWidgets as QW
from sortedcontainers import SortedDict as sdict, SortedSet as sset

# PRISM imports
from prism._docstrings import create_type_doc, kwargs_doc, qt_slot_doc
from prism._gui.widgets import (
    BaseBox, QW_QComboBox, QW_QDoubleSpinBox, QW_QEditableComboBox, QW_QLabel,
    QW_QSpinBox, get_box_value, set_box_value)
from prism._gui.widgets.preferences.custom_boxes import (
    ColorBox, ColorMapBox, DefaultBox)

# All declaration
__all__ = ['KwargsDictBoxLayout', 'KwargsDictDialog', 'KwargsDictDialogPage']


# %% CLASS DEFINITIONS
# Make a subclass that allows for kwargs dicts to be saved and edited
[docs]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
[docs] @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
[docs] 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
[docs] 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)
# Make a subclass that shows the kwargs dict entries window
[docs]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. """
[docs] @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
[docs] @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
[docs] 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
[docs] @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)
# Make a class for describing a kwargs dict page
[docs]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. """
[docs] @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
[docs] 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
[docs] 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
[docs] 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
[docs] @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
[docs] @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
[docs] @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
[docs] @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
[docs] @e13.docstring_append(create_type_doc.format('cmap')) def create_type_cmap(self): return(ColorMapBox())
# This function creates a color picker box
[docs] @e13.docstring_append(create_type_doc.format('color')) def create_type_color(self): return(ColorBox())
# This function creates a dpi box
[docs] @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
[docs] @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
[docs] @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
[docs] @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
[docs] @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
[docs] @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
[docs] @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
[docs] @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
[docs] @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
[docs] @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
[docs] @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
[docs] 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
[docs] @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
[docs] @e13.docstring_append(create_type_doc.format('yscale')) def create_type_yscale(self): return(self.create_type_scale('y'))