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'))
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)
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)
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)
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)
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])
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)
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)
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 © 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()
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()
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)