class ColumnNamesManager(StraditizerControlBase, DockMixin, QtWidgets.QSplitter): """Manage the column names of the reader""" refreshing = _temp_bool_prop('refreshing', doc="True if the widget is refreshing") #: The matplotlib image of the #: :attr:`straditize.colnames.ColNamesReader.rotated_image` im_rotated = None #: The rectangle to highlight a column (see :meth:`highlight_selected_col`) rect = None #: The canvas to display the :attr:`im_rotated` main_canvas = None #: The :class:`matplotlib.axes.Axes` to display the :attr:`im_rotated` main_ax = None #: The original width of the :attr:`main_canvas` fig_w = None #: The original height of the :attr:`main_canvas` fig_h = None #: The matplotlib image of the :attr:`colpic` colpic_im = None #: The canvas to display the :attr:`colpic_im` colpic_canvas = None #: The :class:`matplotlib.axes.Axes` to display the :attr:`colpic_im` colpic_ax = None #: The extents of the :attr:`colpic` in the :attr:`im_rotated` colpic_extents = None #: A QTableWidget to display the column names colnames_table = None #: The :class:`matplotlib.widgets.RectangleSelector` to select the #: :attr:`colpic` selector = None #: The :class:`PIL.Image.Image` of the column name (see also #: :attr:`straditize.colnames.ColNamesReader.colpics`) colpic = None #: A QPushButton to load the highres image btn_load_image = None #: A QPushButton to find column names in the visible part of the #: :attr:`im_rotated` btn_find = None #: A QPushButton to recognize text in the :attr:`colpic` btn_recognize = None #: A checkable QPushButton to initialize a :attr:`selector` to select the #: :attr:`colpic` btn_select_colpic = None #: The QPushButton in the :class:`straditize.widgets.StraditizerWidgets` #: to toggle the column names dialog btn_select_names = None #: A QCheckBox to find the column names (see :attr:`btn_find`) for all #: columns and not just the one selected in the :attr:`colnames_table` cb_find_all_cols = None #: A QCheckBox to ignore the part within the #: :attr:`straditize.colnames.ColNamesReader.data_ylim` cb_ignore_data_part = None #: A QLineEdit to set the :attr:`straditize.colnames.ColNamesReader.rotate` txt_rotate = None #: A QCheckBox to set the :attr:`straditize.colnames.ColNamesReader.mirror` cb_fliph = None #: A QCheckBox to set the :attr:`straditize.colnames.ColNamesReader.flip` cb_flipv = None NAVIGATION_LABEL = ("Use left-click of your mouse to move the image below " "and right-click to zoom in and out.") SELECT_LABEL = "Left-click and hold on the image to select the column name" @property def current_col(self): """The currently selected column""" indexes = self.colnames_table.selectedIndexes() if len(indexes): return indexes[0].row() @property def colnames_reader(self): """The :attr:`straditize.straditizer.Straditizer.colnames_reader` of the current straditizer""" return self.straditizer.colnames_reader @docstrings.dedent def __init__(self, straditizer_widgets, item=None, *args, **kwargs): """ Parameters ---------- %(StraditizerControlBase.init_straditizercontrol.parameters)s """ # Create the button for the straditizer_widgets tree self.btn_select_names = QtWidgets.QPushButton('Edit column names') self.btn_select_names.setCheckable(True) self.btn_select_colpic = QtWidgets.QPushButton('Select column name') self.btn_select_colpic.setCheckable(True) self.btn_select_colpic.setEnabled(False) self.btn_cancel_colpic_selection = QtWidgets.QPushButton('Cancel') self.btn_cancel_colpic_selection.setVisible(False) self.btn_load_image = QtWidgets.QPushButton('Load HR image') self.btn_load_image.setToolTip( 'Select a version of this image with a higher resolution to ' 'improve the text recognition') self.btn_load_image.setCheckable(True) self.btn_recognize = QtWidgets.QPushButton('Recognize') self.btn_recognize.setToolTip('Use tesserocr to recognize the column ' 'name in the given image') self.btn_find = QtWidgets.QPushButton('Find column names') self.btn_find.setToolTip( 'Find the columns names automatically in the image above using ' 'tesserocr') self.cb_find_all_cols = QtWidgets.QCheckBox("all columns") self.cb_find_all_cols.setToolTip( "Find the column names in all columns or only in the selected one") self.cb_find_all_cols.setChecked(True) self.cb_ignore_data_part = QtWidgets.QCheckBox("ignore data part") self.cb_ignore_data_part.setToolTip("ignore everything from the top " "to the bottom of the data part") super().__init__(Qt.Horizontal) # centers of the image self.xc = self.yc = None self.txt_rotate = QtWidgets.QLineEdit() self.txt_rotate.setValidator(QtGui.QDoubleValidator(0., 90., 3)) self.txt_rotate.setPlaceholderText('0˚..90˚') self.cb_fliph = QtWidgets.QCheckBox('Flip horizontally') self.cb_flipv = QtWidgets.QCheckBox('Flip vertically') self.info_label = QtWidgets.QLabel() self.info_label.setWordWrap(True) self.info_label.setStyleSheet('border: 1px solid black') self.main_canvas = EmbededMplCanvas() self.main_ax = self.main_canvas.figure.add_axes([0, 0, 1, 1]) self.main_toolbar = DummyNavigationToolbar2(self.main_canvas) self.main_toolbar.pan() left_widget = QtWidgets.QWidget() layout = QtWidgets.QFormLayout() layout.addRow(self.btn_load_image) layout.addRow(QtWidgets.QLabel('Rotate:'), self.txt_rotate) layout.addRow(self.cb_fliph) layout.addRow(self.cb_flipv) layout.addRow(self.cb_ignore_data_part) layout.addRow(self.info_label) layout.addRow(self.main_canvas) hbox = QtWidgets.QHBoxLayout() hbox.addWidget(self.btn_select_colpic) hbox.addWidget(self.btn_cancel_colpic_selection) layout.addRow(hbox) hbox = QtWidgets.QHBoxLayout() hbox.addWidget(self.btn_find) hbox.addWidget(self.cb_find_all_cols) layout.addRow(hbox) left_widget.setLayout(layout) self.colpic_canvas = EmbededMplCanvas() self.colpic_ax = self.colpic_canvas.figure.add_subplot(111) self.colpic_ax.axis("off") self.colpic_ax.margins(0) self.colpic_canvas.figure.subplots_adjust(bottom=0.3) self.colnames_table = QtWidgets.QTableWidget() self.colnames_table.setColumnCount(1) self.colnames_table.horizontalHeader().setHidden(True) self.colnames_table.setSelectionMode( QtWidgets.QTableView.SingleSelection) self.colnames_table.horizontalHeader().setSectionResizeMode( QtWidgets.QHeaderView.Stretch) self.vsplit = QtWidgets.QSplitter(Qt.Vertical) self.addWidget(left_widget) self.addWidget(self.vsplit) self.vsplit.addWidget(self.colnames_table) w = QtWidgets.QWidget() vbox = QtWidgets.QVBoxLayout() vbox.addWidget(self.colpic_canvas) vbox.addWidget(self.btn_recognize) w.setLayout(vbox) self.vsplit.addWidget(w) self.init_straditizercontrol(straditizer_widgets, item) self.widgets2disable = [ self.btn_select_names, self.btn_find, self.btn_load_image, self.btn_select_colpic ] self.btn_select_names.clicked.connect(self.toggle_dialog) self.btn_select_colpic.clicked.connect(self.toggle_colpic_selection) self.btn_cancel_colpic_selection.clicked.connect( self.cancel_colpic_selection) self.txt_rotate.textChanged.connect(self.rotate) self.cb_fliph.stateChanged.connect(self.mirror) self.cb_flipv.stateChanged.connect(self.flip) self.colnames_table.itemSelectionChanged.connect( self.highlight_selected_col) self.colnames_table.cellChanged.connect(self.colname_changed) self.main_canvas.mpl_connect('resize_event', self.adjust_lims_after_resize) self.btn_load_image.clicked.connect(self.load_image) self.btn_recognize.clicked.connect(self.read_colpic) self.btn_find.clicked.connect(self._find_colnames) self.cb_find_all_cols.stateChanged.connect( self.enable_or_disable_btn_find) self.cb_ignore_data_part.stateChanged.connect( self.change_ignore_data_part) def colname_changed(self, row, column): """Update the column name in the :attr:`colnames_reader` This method is called when a cell in the :attr:`colnames_table` has been changed and updates the corresponding name in the :attr:`colnames_reader` Parameters ---------- row: int The row of the cell in the :attr:`colnames_table` that changed column: int The column of the cell in the :attr:`colnames_table` that changed """ self.colnames_reader._column_names[row] = self.colnames_table.item( row, column).text() def read_colpic(self): """Recognize the text in the :attr:`colpic` See Also -------- straditize.colnames.ColNamesReader.recognize_text""" text = self.colnames_reader.recognize_text(self.colpic) self.colnames_table.item(self.current_col, 0).setText(text) self.colnames_reader._column_names[self.current_col] = text return text def load_image(self): """Load a high resolution image""" if self.btn_load_image.isChecked(): fname = QtWidgets.QFileDialog.getOpenFileName( self.straditizer_widgets, 'Straditizer project', self.straditizer_widgets.menu_actions._start_directory, 'Projects and images ' '(*.nc *.nc4 *.pkl *.jpeg *.jpg *.pdf *.png *.raw *.rgba *.tif' ' *.tiff);;' 'NetCDF files (*.nc *.nc4);;' 'Pickle files (*.pkl);;' 'All images ' '(*.jpeg *.jpg *.pdf *.png *.raw *.rgba *.tif *.tiff);;' 'Joint Photographic Experts Group (*.jpeg *.jpg);;' 'Portable Document Format (*.pdf);;' 'Portable Network Graphics (*.png);;' 'Raw RGBA bitmap (*.raw *.rbga);;' 'Tagged Image File Format(*.tif *.tiff);;' 'All files (*)') fname = fname[0] if fname: from PIL import Image with Image.open(fname) as _image: image = Image.fromarray(np.array(_image.convert('RGBA')), 'RGBA') self.colnames_reader.highres_image = image else: self.colnames_reader.highres_image = None self.refresh() def cancel_colpic_selection(self): """Stop the colpic selection in the :attr:`im_rotated`""" self.colnames_reader._colpics = self._colpics_save if self.current_col is not None: self.colpic = self.colnames_reader.colpics[self.current_col] self.btn_select_colpic.setChecked(False) self.toggle_colpic_selection() def toggle_colpic_selection(self): """Enable or disable the colpic selection""" if (not self.btn_select_colpic.isChecked() and self.selector is not None): self.remove_selector() self.btn_select_colpic.setText('Select column name') if self.current_col is not None: self.colnames_reader._colpics[self.current_col] = self.colpic if self.colpic is None and self.colpic_im is not None: self.colpic_im.remove() del self.colpic_im self.colpic_canvas.draw() self.btn_cancel_colpic_selection.setVisible(False) self.main_canvas.toolbar.pan() self._colpics_save.clear() self.info_label.setText(self.NAVIGATION_LABEL) else: self.create_selector() self.btn_select_colpic.setText('Cancel') self.info_label.setText(self.SELECT_LABEL) self.main_canvas.toolbar.pan() self._colpics_save = list(self.colnames_reader.colpics) self.cb_find_all_cols.setChecked(False) self.main_canvas.draw() def remove_selector(self): """Remove and disconnect the :attr:`selector`""" self.selector.disconnect_events() for a in self.selector.artists: try: a.remove() except ValueError: pass self.main_canvas.draw() del self.selector self.main_canvas.mpl_disconnect(self.key_press_cid) def reset_control(self): """Reset the dialog""" if self.is_shown: self.hide_plugin() self.btn_select_names.setChecked(False) self.remove_images() self.cb_find_all_cols.setChecked(False) self.btn_select_colpic.setChecked(False) self.btn_cancel_colpic_selection.setVisible(False) if self.selector is not None: self.remove_selector() self.cb_fliph.setChecked(False) self.cb_flipv.setChecked(False) self.txt_rotate.blockSignals(True) self.txt_rotate.setText('0') self.txt_rotate.blockSignals(False) def create_selector(self): """Create the :attr:`selector` to enable :attr:`colpic` selection""" self.selector = RectangleSelector(self.main_ax, self.update_image, interactive=True) if self.colpic_extents is not None: self.selector.extents = self.colpic_extents self.key_press_cid = self.main_canvas.mpl_connect( 'key_press_event', self.update_image) def plot_colpic(self): """Plot the :attr:`colpic` in the :attr:`colpic_ax`""" try: self.colpic_im.remove() except (AttributeError, ValueError): pass self.colpic_im = self.colpic_ax.imshow(self.colpic) self.colpic_canvas.draw() def update_image(self, *args, **kwargs): """Update the :attr:`colpic` with the extents of the :attr:`selector` ``*args`` and ``**kwargs`` are ignored """ self.colpic_extents = np.round(self.selector.extents).astype(int) x, y = self.colpic_extents.reshape((2, 2)) x0, x1 = sorted(x) y0, y1 = sorted(y) self.colpic = self.colnames_reader._colpics[self.current_col] = \ self.colnames_reader.get_colpic(x0, y0, x1, y1) self.plot_colpic() self.btn_select_colpic.setText('Apply') self.btn_cancel_colpic_selection.setVisible(True) self.btn_recognize.setEnabled( self.should_be_enabled(self.btn_recognize)) def highlight_selected_col(self): """Highlight the column selected in the :attr:`colnames_tables` See Also -------- straditize.colnames.ColNamesReader.highlight_column""" draw = False if self.rect is not None: self.rect.remove() draw = True del self.rect col = self.current_col if col is not None: reader = self.straditizer.colnames_reader self.rect = reader.highlight_column(col, self.main_ax) reader.navigate_to_col(col, self.main_ax) self.btn_select_colpic.setEnabled(True) if self.colpic_im is not None: self.colpic_im.remove() del self.colpic_im self.colpic = colpic = self.colnames_reader.colpics[col] if colpic is not None: self.colpic_im = self.colpic_ax.imshow(colpic) self.colpic_canvas.draw() self.btn_recognize.setEnabled( self.should_be_enabled(self.btn_recognize)) draw = True else: self.btn_select_colpic.setEnabled(False) if draw: self.main_canvas.draw() self.enable_or_disable_btn_find() def enable_or_disable_btn_find(self, *args, **kwargs): self.btn_find.setEnabled(self.should_be_enabled(self.btn_find)) def setup_children(self, item): child = QtWidgets.QTreeWidgetItem(0) item.addChild(child) self.straditizer_widgets.tree.setItemWidget(child, 0, self.btn_select_names) def should_be_enabled(self, w): ret = self.straditizer is not None and getattr( self.straditizer.data_reader, '_column_starts', None) is not None if ret and w is self.btn_find: from straditize.colnames import tesserocr ret = tesserocr is not None and (self.cb_find_all_cols.isChecked() or self.current_col is not None) elif ret and w is self.btn_recognize: from straditize.colnames import tesserocr ret = tesserocr is not None and self.colpic is not None return ret def toggle_dialog(self): """Close the dialog when the :attr:`btn_select_names` button is clicked """ from psyplot_gui.main import mainwindow if not self.refreshing: if not self.btn_select_names.isChecked() or (self.dock is not None and self.is_shown): self.hide_plugin() if self.btn_select_colpic.isChecked(): self.btn_select_colpic.setChecked(False) self.toggle_colpic_selection() elif self.btn_select_names.isEnabled(): self.straditizer_widgets.tree.itemWidget( self.straditizer_widgets.col_names_item, 1).show_docs() self.to_dock(mainwindow, 'Straditizer column names') self.info_label.setText(self.NAVIGATION_LABEL) self.show_plugin() self.dock.raise_() self.widget(0).layout().update() self.refresh() def _maybe_check_btn_select_names(self): if self.dock is None: return self.btn_select_names.blockSignals(True) self.btn_select_names.setChecked( self.dock.toggleViewAction().isChecked()) self.btn_select_names.blockSignals(False) def refresh(self): with self.refreshing: super().refresh() self.btn_select_names.setChecked(self.btn_select_names.isEnabled() and self.dock is not None and self.is_shown) if self.btn_select_names.isEnabled(): names = self.straditizer.colnames_reader.column_names self.colnames_table.setRowCount(len(names)) for i, name in enumerate(names): self.colnames_table.setItem(i, 0, QtWidgets.QTableWidgetItem(name)) self.colnames_table.setVerticalHeaderLabels( list(map(str, range(len(names))))) self.replot_figure() reader = self.colnames_reader self.txt_rotate.setText(str(reader.rotate)) self.cb_fliph.setChecked(reader.mirror) self.cb_flipv.setChecked(reader.flip) self.cb_ignore_data_part.setChecked(reader.ignore_data_part) image = reader._highres_image if image is reader.image: image = None if image is not None: self.btn_load_image.setText('HR image with size {}'.format( image.size)) self.btn_load_image.setToolTip( 'Remove and ignore the high resolution image') checked = True else: self.btn_load_image.setText('Load HR image') self.btn_load_image.setToolTip( 'Select a version of this image with a higher resolution ' 'to improve the text recognition') checked = False self.btn_load_image.blockSignals(True) self.btn_load_image.setChecked(checked) self.btn_load_image.blockSignals(False) self.btn_recognize.setEnabled( self.should_be_enabled(self.btn_recognize)) else: self.colnames_table.setRowCount(0) self.remove_images() def remove_images(self): """Remove the :attr:`im_rotated` and the :attr:`colpic_im`""" try: self.im_rotated.remove() except (AttributeError, ValueError): pass try: self.colpic_im.remove() except (AttributeError, ValueError): pass self.im_rotated = self.colpic_im = self.xc = self.yc = None def set_xc_yc(self): """Set the x- and y-center before rotating or flipping""" xc = np.mean(self.main_ax.get_xlim()) yc = np.mean(self.main_ax.get_ylim()) self.xc, self.yc = self.colnames_reader.transform_point(xc, yc, True) def flip(self, checked): """TFlip the image""" self.set_xc_yc() self.colnames_reader.flip = checked == Qt.Checked self.replot_figure() def mirror(self, checked): """Mirror the image""" self.set_xc_yc() self.colnames_reader.mirror = checked == Qt.Checked self.replot_figure() def change_ignore_data_part(self, checked): """Change :attr:`straditize.colnames.ColNamesReader.ignore_data_part` """ self.colnames_reader.ignore_data_part = checked == Qt.Checked def rotate(self, val): """Rotate the image Parameters ---------- float The angle for the rotation""" if not str(val).strip(): return try: val = float(val) except (ValueError, TypeError): val = 0 self.set_xc_yc() self.colnames_reader.rotate = val self.replot_figure() def replot_figure(self): """Remove and replot the :attr:`im_rotated`""" adjust_lims = self.im_rotated is None ax = self.main_ax if not self.is_shown: return elif self.im_rotated: rotated = self.straditizer.colnames_reader.rotated_image if np.all(self.im_rotated.get_array() == np.asarray(rotated)): return else: try: self.im_rotated.remove() except ValueError: pass else: rotated = self.straditizer.colnames_reader.rotated_image self.im_rotated = ax.imshow(rotated) if self.xc is not None: dx = np.diff(ax.get_xlim()) / 2. dy = np.diff(ax.get_ylim()) / 2. xc, yc = self.colnames_reader.transform_point(self.xc, self.yc) ax.set_xlim(xc - dx, xc + dx) ax.set_ylim(yc - dy, yc + dy) self.highlight_selected_col() self.xc = self.yc = None if adjust_lims: self.adjust_lims() def adjust_lims(self): """Adjust the limits of the :attr:`main_ax` to fill the entire figure """ size = xs, ys = np.array(self.im_rotated.get_size()) ax = self.main_ax figw, figh = ax.figure.get_figwidth(), ax.figure.get_figheight() woh = figw / figh # width over height how = figh / figw # height over width limits = np.array([[xs, xs * how], [xs * woh, xs], [ys, ys * how], [ys * woh, ys]]) x, y = min(filter(lambda a: (a >= size).all(), limits), key=lambda a: (a - size).max()) ax.set_xlim(0, x) ax.set_ylim(y, 0) ax.axis('off') ax.margins(0) ax.set_position([0, 0, 1, 1]) def to_dock(self, main, title=None, position=None, *args, **kwargs): if position is None: if main.centralWidget() is not main.help_explorer: position = main.dockWidgetArea(main.help_explorer.dock) else: position = Qt.RightDockWidgetArea connect = self.dock is None ret = super(ColumnNamesManager, self).to_dock(main, title, position, *args, **kwargs) if connect: action = self.dock.toggleViewAction() action.triggered.connect(self.maybe_tabify) action.triggered.connect(self._maybe_check_btn_select_names) return ret def maybe_tabify(self): main = self.dock.parent() if self.is_shown and main.dockWidgetArea( main.help_explorer.dock) == main.dockWidgetArea(self.dock): main.tabifyDockWidget(main.help_explorer.dock, self.dock) def adjust_lims_after_resize(self, event): """Adjust the limits of the :attr:`main_ax` after resize of the figure """ h = event.height w = event.width if self.fig_w is None: self.fig_w = w self.fig_h = h self.adjust_lims() return ax = self.main_ax dx = np.diff(ax.get_xlim())[0] dy = np.diff(ax.get_ylim())[0] new_dx = dx * w / self.fig_w new_dy = dy * h / self.fig_h xc = np.mean(ax.get_xlim()) yc = np.mean(ax.get_ylim()) ax.set_xlim(xc - new_dx / 2, xc + new_dx / 2) ax.set_ylim(yc - new_dy / 2, yc + new_dy / 2) self.fig_w = w self.fig_h = h def _find_colnames(self): return self.find_colnames() def find_colnames(self, warn=True, full_image=False, all_cols=None): """Find the column names automatically See Also -------- straditize.colnames.ColNamesReader.find_colnames""" ys, xs = self.im_rotated.get_size() x0, x1 = self.main_ax.get_xlim() if not full_image else (0, xs) y0, y1 = sorted(self.main_ax.get_ylim()) if not full_image else (0, ys) x0 = max(x0, 0) y0 = max(y0, 0) x1 = min(x1, xs) y1 = min(y1, ys) reader = self.colnames_reader texts, images, boxes = reader.find_colnames([x0, y0, x1, y1]) # make sure we have the exact length reader.column_names reader.colpics all_cols = all_cols or (all_cols is None and self.cb_find_all_cols.isChecked()) if not all_cols and self.current_col not in texts: if self.current_col is not None: msg = ("Could not find a column name of column %i in the " "selected image!" % self.current_col) if warn: QtWidgets.QMessageBox.warning( self.straditizer_widgets, 'Could not find column name', msg) return msg elif not texts: msg = "Could not find any column name in the selected image!" if warn: QtWidgets.QMessageBox.warning(self.straditizer_widgets, 'Could not find column name', msg) return msg elif not all_cols: texts = {self.current_col: texts[self.current_col]} for col, text in texts.items(): self.colnames_table.setItem(col, 0, QtWidgets.QTableWidgetItem(text)) self.colnames_reader._colpics[col] = images[col] if self.current_col is not None: self.colpic = self.colnames_reader._colpics[self.current_col] if self.selector is not None: box = boxes[self.current_col] self.colpic_extents = np.round(box.extents).astype(int) self.remove_selector() self.create_selector() self.main_canvas.draw() self.plot_colpic()
class UrlBrowser(QFrame): """Very simple browser with session history and autocompletion based upon the :class:`PyQt5.QtWebEngineWidgets.QWebEngineView` class Warnings -------- This class is known to crash under PyQt4 when new web page domains are loaded. Hence it should be handled with care""" completed = _temp_bool_prop( 'completed', "Boolean whether the html page loading is completed.", default=True) url_like_re = re.compile('^\w+://') doc_urls = OrderedDict([ ('startpage', 'https://startpage.com/'), ('psyplot', 'http://psyplot.readthedocs.org/en/latest/'), ('pyplot', 'http://matplotlib.org/api/pyplot_api.html'), ('seaborn', 'http://stanford.edu/~mwaskom/software/seaborn/api.html'), ('cartopy', 'http://scitools.org.uk/cartopy/docs/latest/index.html'), ('xarray', 'http://xarray.pydata.org/en/stable/'), ('pandas', 'http://pandas.pydata.org/pandas-docs/stable/'), ('numpy', 'https://docs.scipy.org/doc/numpy/reference/routines.html'), ]) #: The initial url showed in the webview. If None, nothing will be #: displayed default_url = None #: adress line tb_url = None #: button to go to previous url bt_back = None #: button to go to next url bt_ahead = None #: refresh the current url bt_refresh = None #: button to go lock to the current url bt_lock = None #: button to disable browsing in www bt_url_lock = None #: The upper part of the browser containing all the buttons button_box = None #: The upper most layout aranging the button box and the html widget vbox = None def __init__(self, *args, **kwargs): super(UrlBrowser, self).__init__(*args, **kwargs) # --------------------------------------------------------------------- # ---------------------------- upper buttons -------------------------- # --------------------------------------------------------------------- # adress line self.tb_url = UrlCombo(self) # button to go to previous url self.bt_back = QToolButton(self) # button to go to next url self.bt_ahead = QToolButton(self) # refresh the current url self.bt_refresh = QToolButton(self) # button to go lock to the current url self.bt_lock = QToolButton(self) # button to disable browsing in www self.bt_url_lock = QToolButton(self) # ---------------------------- buttons settings ----------------------- self.bt_back.setIcon(QIcon(get_icon('previous.png'))) self.bt_back.setToolTip('Go back one page') self.bt_ahead.setIcon(QIcon(get_icon('next.png'))) self.bt_back.setToolTip('Go forward one page') self.bt_refresh.setIcon(QIcon(get_icon('refresh.png'))) self.bt_refresh.setToolTip('Refresh the current page') self.bt_lock.setCheckable(True) self.bt_url_lock.setCheckable(True) if not with_qt5 and rcParams['help_explorer.online'] is None: # We now that the browser can crash with Qt4, therefore we disable # the browing in the internet self.bt_url_lock.click() rcParams['help_explorer.online'] = False elif rcParams['help_explorer.online'] is False: self.bt_url_lock.click() elif rcParams['help_explorer.online'] is None: rcParams['help_explorer.online'] = True rcParams.connect('help_explorer.online', self.update_url_lock_from_rc) self.bt_url_lock.clicked.connect(self.toogle_url_lock) self.bt_lock.clicked.connect(self.toogle_lock) # tooltip and icons of lock and url_lock are set in toogle_lock and # toogle_url_lock self.toogle_lock() self.toogle_url_lock() # --------------------------------------------------------------------- # --------- initialization and connection of the web view ------------- # --------------------------------------------------------------------- #: The actual widget showing the html content self.html = QWebEngineView(parent=self) self.html.loadStarted.connect(self.completed) self.html.loadFinished.connect(self.completed) self.tb_url.currentIndexChanged[str].connect(self.browse) self.bt_back.clicked.connect(self.html.back) self.bt_ahead.clicked.connect(self.html.forward) self.bt_refresh.clicked.connect(self.html.reload) self.html.urlChanged.connect(self.url_changed) # --------------------------------------------------------------------- # ---------------------------- layouts -------------------------------- # --------------------------------------------------------------------- # The upper part of the browser containing all the buttons self.button_box = button_box = QHBoxLayout() button_box.addWidget(self.bt_back) button_box.addWidget(self.bt_ahead) button_box.addWidget(self.tb_url) button_box.addWidget(self.bt_refresh) button_box.addWidget(self.bt_lock) button_box.addWidget(self.bt_url_lock) # The upper most layout aranging the button box and the html widget self.vbox = vbox = QVBoxLayout() self.vbox.setContentsMargins(0, 0, 0, 0) vbox.addLayout(button_box) vbox.addWidget(self.html) self.setLayout(vbox) if self.default_url is not None: self.tb_url.addItem(self.default_url) def browse(self, url): """Make a web browse on the given url and show the page on the Webview widget. """ if self.bt_lock.isChecked(): return if not self.url_like_re.match(url): url = 'https://' + url if self.bt_url_lock.isChecked() and url.startswith('http'): return if not self.completed: logger.debug('Stopping current load...') self.html.stop() self.completed = True logger.debug('Loading %s', url) # we use :meth:`PyQt5.QtWebEngineWidgets.QWebEngineView.setUrl` instead # of :meth:`PyQt5.QtWebEngineWidgets.QWebEngineView.load` because that # changes the url directly and is more useful for unittests self.html.setUrl(QtCore.QUrl(url)) def url_changed(self, url): """Triggered when the url is changed to update the adress line""" try: url = url.toString() except AttributeError: pass logger.debug('url changed to %s', url) try: self.tb_url.setCurrentText(url) except AttributeError: # Qt4 self.tb_url.setEditText(url) self.tb_url.add_text_on_top(url, block=True) def update_url_lock_from_rc(self, online): if (online and self.bt_url_lock.isChecked() or not online and not self.bt_url_lock.isChecked()): self.bt_url_lock.click() def toogle_url_lock(self): """Disable (or enable) the loading of web pages in www""" bt = self.bt_url_lock offline = bt.isChecked() bt.setIcon(QIcon( get_icon('world_red.png' if offline else 'world.png'))) online_message = "Go online" if not with_qt5: online_message += ("\nWARNING: This mode is unstable under Qt4 " "and might result in a complete program crash!") bt.setToolTip(online_message if offline else "Offline mode") if rcParams['help_explorer.online'] is offline: rcParams['help_explorer.online'] = not offline def toogle_lock(self): """Disable (or enable) the changing of the current webpage""" bt = self.bt_lock bt.setIcon( QIcon(get_icon('lock.png' if bt.isChecked() else 'lock_open.png'))) bt.setToolTip("Unlock" if bt.isChecked() else "Lock to current page")
class Test(object): test = utils._temp_bool_prop('test')
class FormatoptionWidget(QWidget, DockMixin): """ Widget to update the formatoptions of the current project This widget, mainly made out of a combobox for the formatoption group, a combobox for the formatoption, and a one-line text editor, is designed for updating the selected formatoptions for the current subproject. The widget is connected to the :attr:`psyplot.project.Project.oncpchange` signal and refills the comboboxes if the current subproject changes. The one-line text editor accepts python code that will be executed in side the given `shell`. """ no_fmtos_update = _temp_bool_prop('no_fmtos_update', """update the fmto combo box or not""") #: The combobox for the formatoption groups group_combo = None #: The combobox for the formatoptions fmt_combo = None #: The help_explorer to display the documentation of the formatoptions help_explorer = None #: The shell to execute the update of the formatoptions in the current #: project shell = None def __init__(self, *args, **kwargs): """ Parameters ---------- help_explorer: psyplot_gui.help_explorer.HelpExplorer The help explorer to show the documentation of one formatoption shell: IPython.core.interactiveshell.InteractiveShell The shell that can be used to update the current subproject via:: psy.gcp().update(**kwargs) where ``**kwargs`` is defined through the selected formatoption in the :attr:`fmt_combo` combobox and the value in the :attr:`line_edit` editor ``*args, **kwargs`` Any other keyword for the QWidget class """ help_explorer = kwargs.pop('help_explorer', None) shell = kwargs.pop('shell', None) super(FormatoptionWidget, self).__init__(*args, **kwargs) self.help_explorer = help_explorer self.shell = shell # --------------------------------------------------------------------- # -------------------------- Child widgets ---------------------------- # --------------------------------------------------------------------- self.group_combo = QComboBox(parent=self) self.fmt_combo = QComboBox(parent=self) self.line_edit = QLineEdit(parent=self) self.run_button = QToolButton(parent=self) self.keys_button = QPushButton('Formatoption keys', parent=self) self.summaries_button = QPushButton('Summaries', parent=self) self.docs_button = QPushButton('Docs', parent=self) self.grouped_cb = QCheckBox('grouped', parent=self) self.all_groups_cb = QCheckBox('all groups', parent=self) self.include_links_cb = QCheckBox('include links', parent=self) # --------------------------------------------------------------------- # -------------------------- Descriptions ----------------------------- # --------------------------------------------------------------------- self.group_combo.setToolTip('Select the formatoption group') self.fmt_combo.setToolTip('Select the formatoption to update') self.line_edit.setToolTip( 'Insert the value which what you want to update the selected ' 'formatoption and hit right button. The code is executed in the ' 'main console.') self.run_button.setIcon(QIcon(get_icon('run_arrow.png'))) self.run_button.setToolTip('Update the selected formatoption') self.keys_button.setToolTip( 'Show the formatoption keys in this group (or in all ' 'groups) in the help explorer') self.summaries_button.setToolTip( 'Show the formatoption summaries in this group (or in all ' 'groups) in the help explorer') self.docs_button.setToolTip( 'Show the formatoption documentations in this group (or in all ' 'groups) in the help explorer') self.grouped_cb.setToolTip( 'Group the formatoptions before displaying them in the help ' 'explorer') self.all_groups_cb.setToolTip('Use all groups when displaying the ' 'keys, docs or summaries') self.include_links_cb.setToolTip( 'Include links to remote documentations when showing the ' 'keys, docs and summaries in the help explorer (requires ' 'intersphinx)') # --------------------------------------------------------------------- # -------------------------- Connections ------------------------------ # --------------------------------------------------------------------- self.group_combo.currentIndexChanged[int].connect(self.fill_fmt_combo) self.fmt_combo.currentIndexChanged[int].connect(self.show_fmt_info) self.run_button.clicked.connect(self.run_code) self.line_edit.returnPressed.connect(self.run_button.click) self.keys_button.clicked.connect( partial(self.show_all_fmt_info, 'keys')) self.summaries_button.clicked.connect( partial(self.show_all_fmt_info, 'summaries')) self.docs_button.clicked.connect( partial(self.show_all_fmt_info, 'docs')) # --------------------------------------------------------------------- # ------------------------------ Layouts ------------------------------ # --------------------------------------------------------------------- self.combos = QHBoxLayout() self.combos.addWidget(self.group_combo) self.combos.addWidget(self.fmt_combo) self.execs = QHBoxLayout() self.execs.addWidget(self.line_edit) self.execs.addWidget(self.run_button) self.info_box = QHBoxLayout() self.info_box.addStretch(0) for w in [ self.keys_button, self.summaries_button, self.docs_button, self.all_groups_cb, self.grouped_cb, self.include_links_cb ]: self.info_box.addWidget(w) self.vbox = QVBoxLayout() self.vbox.addLayout(self.combos) self.vbox.addLayout(self.execs) self.vbox.addLayout(self.info_box) self.setLayout(self.vbox) # fill with content self.fill_combos_from_project(psy.gcp()) psy.Project.oncpchange.connect(self.fill_combos_from_project) def fill_combos_from_project(self, project): """Fill :attr:`group_combo` and :attr:`fmt_combo` from a project Parameters ---------- project: psyplot.project.Project The project to use""" current_text = self.group_combo.currentText() with self.no_fmtos_update: self.group_combo.clear() if project is None or project.is_main or not len(project.plotters): self.fmt_combo.clear() self.groups = [] self.fmtos = [] self.line_edit.setEnabled(False) return self.line_edit.setEnabled(True) # get dimensions coords = sorted(project.coords_intersect) coords_name = [COORDSGROUP] if coords else [] coords_verbose = ['Dimensions'] if coords else [] coords = [coords] if coords else [] # get formatoptions and group them alphabetically grouped_fmts = defaultdict(list) for fmto in project._fmtos: grouped_fmts[fmto.group].append(fmto) for val in six.itervalues(grouped_fmts): val.sort(key=self.get_name) grouped_fmts = OrderedDict( sorted(six.iteritems(grouped_fmts), key=lambda t: psyp.groups.get(t[0], t[0]))) fmt_groups = list(grouped_fmts.keys()) # save original names self.groups = coords_name + [ALLGROUP] + fmt_groups # save verbose group names (which are used in the combo box) self.groupnames = coords_verbose + ['All formatoptions'] + list( map(lambda s: psyp.groups.get(s, s), fmt_groups)) # save formatoptions fmtos = list(grouped_fmts.values()) self.fmtos = coords + [sorted(chain(*fmtos), key=self.get_name) ] + fmtos self.group_combo.addItems(self.groupnames) ind = self.group_combo.findText(current_text) self.group_combo.setCurrentIndex(ind if ind >= 0 else 0) self.fill_fmt_combo(self.group_combo.currentIndex()) def get_name(self, fmto): """Get the name of a :class:`psyplot.plotter.Formatoption` instance""" if isinstance(fmto, six.string_types): return fmto return '%s (%s)' % (fmto.name, fmto.key) if fmto.name else fmto.key def fill_fmt_combo(self, i): """Fill the :attr:`fmt_combo` combobox based on the current group name """ if not self.no_fmtos_update: with self.no_fmtos_update: current_text = self.fmt_combo.currentText() self.fmt_combo.clear() self.fmt_combo.addItems(list(map(self.get_name, self.fmtos[i]))) ind = self.fmt_combo.findText(current_text) self.fmt_combo.setCurrentIndex(ind if ind >= 0 else 0) self.show_fmt_info(self.fmt_combo.currentIndex()) def show_fmt_info(self, i): """Show the documentation of the formatoption in the help explorer """ group_ind = self.group_combo.currentIndex() if (not self.no_fmtos_update and self.groups[group_ind] != COORDSGROUP): fmto = self.fmtos[self.group_combo.currentIndex()][i] fmto.plotter.show_docs( fmto.key, include_links=self.include_links_cb.isChecked()) def run_code(self): """Run the update of the project inside the :attr:`shell`""" text = str(self.line_edit.text()) if not text or not self.fmtos: return group_ind = self.group_combo.currentIndex() if self.groups[group_ind] == COORDSGROUP: key = self.fmtos[group_ind][self.fmt_combo.currentIndex()] param = 'dims' else: key = self.fmtos[group_ind][self.fmt_combo.currentIndex()].key param = 'fmt' e = ExecutionResult() self.shell.run_code( "psy.gcp().update(%s={'%s': %s})" % (param, key, text), e) e.raise_error() def show_all_fmt_info(self, what): """Show the keys, summaries or docs of the formatoptions Calling this function let's the help browser show the documentation etc. of all docs or only the selected group determined by the state of the :attr:`grouped_cb` and :attr:`all_groups_cb` checkboxes Parameters ---------- what: {'keys', 'summaries', 'docs'} Determines what to show""" if not self.fmtos: return if self.all_groups_cb.isChecked(): fmtos = list( chain(*(fmto_group for i, fmto_group in enumerate(self.fmtos) if not self.groups[i] in [ALLGROUP, COORDSGROUP]))) else: if self.groups[self.group_combo.currentIndex()] == COORDSGROUP: return fmtos = self.fmtos[self.group_combo.currentIndex()] plotter = fmtos[0].plotter getattr(plotter, 'show_' + what)([fmto.key for fmto in fmtos], grouped=self.grouped_cb.isChecked(), include_links=self.include_links_cb.isChecked())
class CrossMarks(object): """ A set of draggable marks in a matplotlib axes """ @property def fig(self): """The :class:`matplotlib.figure.Figure` that this mark plots on""" return self.ax.figure @property def y(self): """The y-position of the mark""" return self.ya[self._i_hline] @y.setter def y(self, value): """The y-position of the mark""" self.ya[self._i_hline] = value @property def x(self): """The x-position of the mark""" return self.xa[self._i_vline] @x.setter def x(self, value): """The x-position of the mark""" self.xa[self._i_vline] = value @property def hline(self): """The current horizontal line""" return self.hlines[self._i_hline] @property def vline(self): """The current vertical line""" return self.vlines[self._i_vline] @property def pos(self): """The position of the current line""" return np.array([self.xa[self._i_vline], self.ya[self._i_hline]]) @pos.setter def pos(self, value): """The position of the current line""" self.xa[self._i_vline] = value[0] if np.ndim(value) else value self.ya[self._i_hline] = value[1] if np.ndim(value) else value @property def points(self): """The x-y-coordinates of the points as a (N, 2)-shaped array""" return np.array(list(product(self.xa, self.ya))) @property def line_connections(self): """The line connections to the current position""" return self._all_line_connections[self._i_hline][self._i_vline] @line_connections.setter def line_connections(self, value): """The line connections to the current position""" self._all_line_connections[self._i_hline][self._i_vline] = value @property def other_connections(self): """All other connections to the current position""" return self._all_other_connections[self._i_hline][self._i_vline] @other_connections.setter def other_connections(self, value): """All other connections to the current position""" self._all_other_connections[self._i_hline][self._i_vline] = value @property def idx_h(self): """The index for vertical lines""" return None if not self._idx_h else self._idx_h[self._i_vline] @idx_h.setter def idx_h(self, value): """The index for vertical lines""" if self._idx_h is None: self._idx_h = [None] * len(self.xa) self._idx_h[self._i_vline] = value @property def idx_v(self): """The index for horizontal lines""" return None if not self._idx_v else self._idx_v[self._i_hline] @idx_v.setter def idx_v(self, value): """The index for horizontal lines""" if self._idx_v is None: self._idx_v = [None] * len(self.ya) self._idx_v[self._i_hline] = value #: Boolean to control whether the vertical lines should be hidden hide_vertical = False #: Boolean to control whether the horizontal lines should be hidden hide_horizontal = False #: A signal that is emitted when the mark is moved. Connected function are #: expected to accept two arguments. One tuple with the old position and #: the CrossMarks instance itself moved = Signal('_moved') block_signals = _temp_bool_prop( 'block_signals', "Block the emitting of signals of this instance") #: The index of the selected hline _i_hline = 0 #: The index of the selected vline _i_vline = 0 #: Boolean that is True, if the animated property of the lines should be #: used _animated = True #: The matplotlib axes to plot on ax = None #: The x-limits of the :attr:`hlines` xlim = None #: The x-limits of the :attr:`vlines` ylim = None #: Class attribute that is set to a :class:`CrossMark` instance to lock the #: selection of marks lock = None #: A boolean to control whether the connected artists should be shown #: at all show_connected_artists = True #: a list of :class:`matplotlib.artist.Artist` whose colors are changed #: when this mark is selected connected_artists = [] #: The default properties of the unselected mark, complementing the #: :attr:`_select_props` _unselect_props = {} #: the list of horizontal lines hlines = [] #: the list of vertical lines vlines = [] @docstrings.get_sectionsf('CrossMarks') @docstrings.dedent def __init__(self, pos=(0, 0), ax=None, selectable=['h', 'v'], draggable=['h', 'v'], idx_h=None, idx_v=None, xlim=None, ylim=None, select_props={'c': 'r'}, auto_hide=False, connected_artists=[], lock=True, draw_lines=True, hide_vertical=None, hide_horizontal=None, **kwargs): """ Parameters ---------- pos: tuple of 2 arrays The initial positions of the crosses. The first item marks the x-coordinates of the points, the second the y-coordinates ax: matplotlib.axes.Axes The axes object to draw to. If not specified and draw_lines is True, the current axes object is used selectable: list of {'x', 'y'} Determine whether only the x-, y-, or both lines should be selectable draggable: list of {'x', 'y'} Determine whether only the x-, y-, or both lines should be draggable idx_h: pandas.Index The index for the horizontal coordinates. If not provided, we use a continuous movement along x. idx_v: pandas.Index The index for the vertical coordinates. If not provided, we use a continuous movement along y. xlim: tuple of floats (xmin, xmax) The minimum and maximum x value for the lines ylim: tuple for floats (ymin, ymax) The minimum and maximum y value for the lines select_props: color The line properties for selected marks auto_hide: bool If True, the lines are hidden if they are not selected. connected_artists: list of artists List of artists whose properties should be changed to `select_props` when this marks is selected lock: bool If True, at most one mark can be selected at a time draw_lines: bool If True, the cross mark lines are drawn. Otherwise, you must call the `draw_lines` method explicitly hide_vertical: bool Boolean to control whether the vertical lines should be hidden. If None, the default class attribute is used hide_horizontal: bool Boolean to control whether the horizontal lines should be hidden. If None, the default class attribute is used ``**kwargs`` Any other keyword argument that is passed to the :func:`matplotlib.pyplot.plot` function""" self.xa = np.asarray([pos[0]] if not np.ndim(pos[0]) else pos[0], dtype=float) self.ya = np.asarray([pos[1]] if not np.ndim(pos[1]) else pos[1], dtype=float) self._xa0 = self.xa.copy() self._ya0 = self.ya.copy() self._constant_dist_x = [] self._constant_dist_x_marks = [] self._constant_dist_y = [] self._constant_dist_y_marks = [] self.selectable = list(selectable) self.draggable = list(draggable) if hide_horizontal is not None: self.hide_horizontal = hide_horizontal if hide_vertical is not None: self.hide_vertical = hide_vertical self._select_props = select_props.copy() self.press = None if idx_h is not None: try: idx_h[0][0] except IndexError: idx_h = [idx_h] * len(self.xa) if idx_v is not None and np.ndim(idx_v) != 2: try: idx_v[0][0] except IndexError: idx_v = [idx_v] * len(self.ya) self._idx_h = idx_h self._idx_v = idx_v self.xlim = xlim self.ylim = ylim self.other_marks = [] self._connection_visible = [] self._all_line_connections = [[[] for _ in range(len(self.xa))] for _ in range(len(self.ya))] self._all_other_connections = [[[] for _ in range(len(self.xa))] for _ in range(len(self.ya))] self._lock_mark = lock kwargs.setdefault('marker', '+') self.auto_hide = auto_hide self._line_kwargs = kwargs self.set_connected_artists(list(connected_artists)) if draw_lines: self.ax = ax self.draw_lines() self.connect() elif ax is not None: self.ax = ax def set_connected_artists(self, artists): """Set the connected artists Parameters ---------- artists: matplotlib.artist.Artist The artists (e.g. other lines) that should be connected and highlighted if this mark is selected""" self.connected_artists = artists self._connected_artists_props = [{ key: getattr(a, 'get_' + key)() for key in self._select_props } for a in artists] def draw_lines(self, **kwargs): """Draw the vertical and horizontal lines Parameters ---------- ``**kwargs`` An keyword that is passed to the :func:`matplotlib.pyplot.plot` function""" if kwargs: self._line_kwargs = kwargs else: kwargs = self._line_kwargs if self.ax is None: import matplotlib.pyplot as plt self.ax = plt.gca() if self.ylim is None: self.ylim = ylim = self.ax.get_ylim() else: ylim = self.ylim if self.xlim is None: self.xlim = xlim = self.ax.get_xlim() else: xlim = self.xlim xmin = min(xlim) xmax = max(xlim) ymin = min(ylim) ymax = max(ylim) xy = zip(repeat(self.xa), self.ya) x, y = next(xy) # we plot the first separate line to get the correct color line = self.ax.plot(np.r_[[xmin], x, [xmax]], [y] * (len(x) + 2), markevery=slice(1, len(x) + 1), label='cross_mark_hline', visible=not self.hide_horizontal, **kwargs)[0] if 'color' not in kwargs and 'c' not in kwargs: kwargs['c'] = line.get_c() # now the rest of the horizontal lines self.hlines = [line] + [ self.ax.plot(np.r_[[xmin], x, [xmax]], [y] * (len(x) + 2), markevery=slice(1, len(x) + 1), label='cross_mark_hline', visible=not self.hide_horizontal, **kwargs)[0] for x, y in xy ] # and the vertical lines self.vlines = [ self.ax.plot([x] * (len(y) + 2), np.r_[[ymin], y, [ymax]], markevery=slice(1, len(y) + 1), label='cross_mark_vline', visible=not self.hide_vertical, **kwargs)[0] for x, y in zip(self.xa, repeat(self.ya)) ] for h, v in zip(self.hlines, self.vlines): visible = v.get_visible() v.update_from(h) v.set_visible(visible) line = self.hlines[0] props = self._select_props if 'lw' not in props and 'linewidth' not in props: props.setdefault('lw', line.get_lw()) # copy the current attributes from the lines self._unselect_props = { key: getattr(line, 'get_' + key)() for key in props } if self.auto_hide: for l in chain(self.hlines, self.vlines, self.line_connections): l.set_lw(0) def set_visible(self, b): """Set the visibility of the mark Parameters ---------- b: bool If False, hide all horizontal and vertical lines, and the :attr:`connected_artists`""" for l in self.hlines: l.set_visible(b and not self.hide_horizontal) for l in self.vlines: l.set_visible(b and not self.hide_vertical) show_connected = self.show_connected_artists and b for l in self.connected_artists: l.set_visible(show_connected) def __reduce__(self): return ( self.__class__, ( (self.xa, self.ya), # pos None, # ax -- do not make a plot self.selectable, # selectable self.draggable, # draggable self._idx_h, # idx_h self._idx_v, # idx_v self.xlim, # xlim self.ylim, # ylim self._select_props, # select_props self.auto_hide, # auto_hide [], # connected_artists self._lock_mark, # lock False, # draw_lines -- do not draw the lines ), { '_line_kwargs': self._line_kwargs, 'hide_horizontal': self.hide_horizontal, 'hide_vertical': self.hide_vertical, '_unselect_props': self._unselect_props, 'xa': self.xa, 'ya': self.ya }) @staticmethod def maintain_y(marks): """Connect marks and maintain a constant vertical distance between them Parameters ---------- marks: list of CrossMarks A list of marks. If one of the marks is moved vertically, the others are, too""" for mark in marks: mark._maintain_y([m for m in marks if m is not mark]) def _maintain_y(self, marks): """Connect to marks and maintain a constant vertical distance Parameters ---------- marks: list of CrossMarks A list of other marks. If this mark is moved vertically, the others are, too""" y = self.y self._constant_dist_y.extend(m.y - y for m in marks) self._constant_dist_y_marks.extend(marks) @staticmethod def maintain_x(marks): """Connect marks and maintain a constant horizontal distance Parameters ---------- marks: list of CrossMarks A list of marks. If one of the marks is moved horizontally, the others are, too""" for mark in marks: mark._maintain_x([m for m in marks if m is not mark]) def _maintain_x(self, marks): """Connect to marks and maintain a constant horizontal distance Parameters ---------- marks: list of CrossMarks A list of other marks. If this mark is moved horizontally, the others are, too""" x = self.x self._constant_dist_x.extend(m.x - x for m in marks) self._constant_dist_x_marks.extend(marks) def connect_to_marks(self, marks, visible=False, append=True): """Append other marks that should be considered for aligning the lines Parameters ---------- marks: list of CrossMarks A list of other marks visible: bool If True, the marks are connected through visible lines append: bool If True, the marks are appended. This is important if the mark will be moved by the `set_pos` method Notes ----- This method can only be used to connect other marks with this mark. If you want to connect multiple marks within each other, use the :meth:`connect_marks` static method """ if append: self.other_marks.extend(marks) self._connection_visible.extend([visible] * len(marks)) if visible: ya = self.ya xa = self.xa ax = self.ax for m in marks: for i1, j1 in product(range(len(xa)), range(len(ya))): self.set_current_point(i1, j1) pos = self.pos for i2, j2 in product(range(len(m.xa)), range(len(m.ya))): m.set_current_point(i2, j2) line = ax.plot([pos[0], m.pos[0]], [pos[1], m.pos[1]], label='cross_mark_connection', **self._unselect_props)[0] if self.auto_hide: line.set_lw(0) self.line_connections.append(line) m.other_connections.append(line) @staticmethod def connect_marks(marks, visible=False): """Connect multiple marks to each other Parameters ---------- marks: list of CrossMarks A list of marks visible: bool If True, the marks are connected through visible lines Notes ----- Different from the :meth:`connect_to_marks` method, this static function connects each of the marks to the others. """ for mark in marks: mark.connect_to_marks([m for m in marks if m is not mark], visible) def connect(self): """Connect the marks matplotlib events""" fig = self.fig self.cidpress = fig.canvas.mpl_connect('button_press_event', self.on_press) self.cidrelease = fig.canvas.mpl_connect('button_release_event', self.on_release) self.cidmotion = fig.canvas.mpl_connect('motion_notify_event', self.on_motion) def is_selected_by(self, event, buttons=[1]): """Test if the given `event` selects the mark Parameters ---------- event: matplotlib.backend_bases.MouseEvent The matplotlib event button: list of int Possible buttons to select this mark Returns ------- bool True, if it is selected""" return not (self.lock is not None or event.inaxes != self.ax or event.button not in buttons or self.fig.canvas.manager.toolbar.mode != '' or not self.contains(event)) def set_current_point(self, x, y, nearest=False): """Set the current point that is selected Parameters ---------- x: int The index of the x-value in the :attr:`xa` attribute y: int The index of the y-value in the :attr:`ya` attribute nearest: bool If not None, `x` and `y` are interpreted as x- and y-values and we select the closest one """ if nearest: x = np.abs(self.xa - x).argmin() y = np.abs(self.ya - y).argmin() self._i_vline = x self._i_hline = y def on_press(self, event, force=False, connected=True): """Select the mark Parameters ---------- event: matplotlib.backend_bases.MouseEvent The mouseevent that selects the mark force: bool If True, the mark is selected although it does not contain the `event` connected: bool If True, connected marks that should maintain a constant x- and y-distance are selected, too""" if not force and not self.is_selected_by(event): return self.set_current_point(event.xdata, event.ydata, True) # use only the upper most CrossMarks if self._lock_mark and connected: CrossMarks.lock = self if self._animated: self.hline.set_animated(True) self.vline.set_animated(True) self.background = self.fig.canvas.copy_from_bbox(self.ax.bbox) self.hline.update(self._select_props) self.vline.update(self._select_props) # toggle line connections artist_props = self._select_props.copy() for a in chain(self.line_connections, self.other_connections): a.update(artist_props) # toggle connected artists artist_props['visible'] = (self.show_connected_artists and artist_props.get('visible', True)) for a in self.connected_artists: a.update(artist_props) self.press = self.pos[0], self.pos[1], event.xdata, event.ydata # select the connected marks that should maintain the distance if connected: for m in set( chain(self._constant_dist_y_marks, self._constant_dist_x_marks)): m._i_vline = self._i_vline m._i_hline = self._i_hline event.xdata, event.ydata = m.pos m.on_press(event, True, False) event.xdata, event.ydata = self.press[2:] for l in chain(self.other_connections, self.line_connections, self.connected_artists): self.ax.draw_artist(l) self.ax.draw_artist(self.hline) self.ax.draw_artist(self.vline) if self._animated: self.fig.canvas.blit(self.ax.bbox) def contains(self, event): """Test if the mark is selected by the given `event` Parameters ---------- event: ButtonPressEvent The ButtonPressEvent that has been triggered""" contains = None if 'h' in self.selectable: contains = any(l.contains(event)[0] for l in self.hlines) if not contains and 'v' in self.selectable: contains = any(l.contains(event)[0] for l in self.vlines) return contains def on_motion(self, event, force=False, move_connected=True, restore=True): """Move the lines of this mark Parameters ---------- event: matplotlib.backend_bases.MouseEvent The mouseevent that moves the mark force: bool If True, the mark is moved although it does not contain the `event` move_connected: bool If True, connected marks that should maintain a constant x- and y-distance are moved, too restore: bool If True, the axes background is restored""" if self.press is None or (not force and self._lock_mark and self.lock is not self): return if not force and event.inaxes != self.ax: return x0, y0, xpress, ypress = self.press dx = event.xdata - xpress dy = event.ydata - ypress canvas = self.fig.canvas if dy and 'h' in self.draggable: y1 = y0 + dy one_percent = np.abs(0.01 * np.diff(self.ax.get_ylim())[0]) for mark in filter(lambda m: m.ax is self.ax, self.other_marks): if np.abs(mark.pos[1] - y1) < one_percent: y1 = mark.pos[1] break if self.idx_v is not None: y1 = self.idx_v[self.idx_v.get_loc(y1, method='nearest')] self.hline.set_ydata([y1] * len(self.hline.get_ydata())) self.y = y1 # first we move the horizontal line that is associated with this # mark ydata = self.vline.get_ydata()[:] ydata[self._i_hline + 1] = y1 for l in self.vlines: l.set_ydata(ydata) # now we move all connections that are connected to this horizontal # layer for l in chain.from_iterable( self._all_line_connections[self._i_hline]): l.set_ydata([y1, l.get_ydata()[1]]) for l in chain.from_iterable( self._all_other_connections[self._i_hline]): l.set_ydata([l.get_ydata()[0], y1]) if dx and 'v' in self.draggable: x1 = x0 + dx one_percent = np.abs(0.01 * np.diff(self.ax.get_xlim())[0]) for mark in filter(lambda m: m.ax is self.ax, self.other_marks): if np.abs(mark.pos[0] - x1) < one_percent: x1 = mark.pos[0] break if self.idx_h is not None: x1 = self.idx_h[self.idx_h.get_loc(x1, method='nearest')] self.vline.set_xdata([x1] * len(self.vline.get_xdata())) self.x = x1 # first we move the vertical line that is associated with this mark xdata = self.hline.get_xdata()[:] xdata[self._i_vline + 1] = x1 for l in self.hlines: l.set_xdata(xdata) # now we move all connections that are connected to this vertical # layer for l in chain.from_iterable(l[self._i_vline] for l in self._all_line_connections): l.set_xdata([x1, l.get_xdata()[1]]) for l in chain.from_iterable(l[self._i_vline] for l in self._all_other_connections): l.set_xdata([l.get_xdata()[0], x1]) if restore and self._animated: canvas.restore_region(self.background) for l in chain(self.other_connections, self.line_connections, self.connected_artists): self.ax.draw_artist(l) if restore and self._animated: self.ax.draw_artist(self.hline) self.ax.draw_artist(self.vline) canvas.blit(self.ax.bbox) else: self.ax.figure.canvas.draw_idle() # move the marks that should maintain a constant distance orig_xy = (event.xdata, event.ydata) if move_connected and dy and 'h' in self.draggable: for dist, m in zip(self._constant_dist_y, self._constant_dist_y_marks): event.xdata = m.press[-2] event.ydata = y1 + dist m.on_motion(event, True, False, m.ax is not self.ax) if move_connected and dx and 'v' in self.draggable: for dist, m in zip(self._constant_dist_x, self._constant_dist_x_marks): event.xdata = x1 + dist event.ydata = m.press[-1] m.on_motion(event, True, False, m.ax is not self.ax) event.xdata, event.ydata = orig_xy def set_connected_artists_visible(self, visible): """Set the visibility of the connected artists Parameters ---------- visible: bool True, show the connected artists, else don't""" self.show_connected_artists = visible for a in self.connected_artists: a.set_visible(visible) for d in self._connected_artists_props: d['visible'] = visible def on_release(self, event, force=False, connected=True, draw=True, *args, **kwargs): """Release the mark and unselect it Parameters ---------- event: matplotlib.backend_bases.MouseEvent The mouseevent that releases the mark force: bool If True, the mark is released although it does not contain the `event` connected: bool If True, connected marks that should maintain a constant x- and y-distance are released, too draw: bool If True, the figure is drawn ``*args, **kwargs`` Any other parameter that is passed to the connected lines""" if (not force and self._lock_mark and self.lock is not self or self.press is None): return self.hline.update(self._unselect_props) self.vline.update(self._unselect_props) for d, a in zip_longest(self._connected_artists_props, self.connected_artists, fillvalue=self._unselect_props): a.update(d) for l in chain(self.line_connections, self.other_connections): l.update(self._unselect_props) if self.auto_hide: self.hline.set_lw(0) self.vline.set_lw(0) for l in chain(self.line_connections, self.other_connections): l.set_lw(0) self.xa[self._i_vline] = self.pos[0] self.ya[self._i_hline] = self.pos[1] pos0 = self.press[:2] self.press = None if self._animated: self.hline.set_animated(False) self.vline.set_animated(False) self.background = None if connected: for m in set( chain(self._constant_dist_y_marks, self._constant_dist_x_marks)): m.on_release(event, True, False, m.fig is not self.fig, *args, **kwargs) if self._lock_mark and self.lock is self: CrossMarks.lock = None if draw: self.fig.canvas.draw_idle() self.moved.emit(pos0, self) def disconnect(self): """Disconnect all the stored connection ids""" fig = self.fig fig.canvas.mpl_disconnect(self.cidpress) fig.canvas.mpl_disconnect(self.cidrelease) fig.canvas.mpl_disconnect(self.cidmotion) def remove(self, artists=True): """Remove all lines and disconnect the mark Parameters ---------- artists: bool If True, the :attr:`connected_artists` list is cleared and the corresponding artists are removed as well""" for l in chain( self.hlines, self.vlines, self.connected_artists if artists else [], chain.from_iterable( chain.from_iterable(self._all_other_connections)), chain.from_iterable( chain.from_iterable(self._all_line_connections))): try: l.remove() except ValueError: pass self.hlines.clear() self.vlines.clear() if artists: self.connected_artists.clear() # Remove the line connections visible_connections = [ m for m, v in zip(self.other_marks, self._connection_visible) if v ] for m in visible_connections: for l in chain.from_iterable( chain.from_iterable(self._all_line_connections)): for i, j in product(range(len(m.ya)), range(len(m.xa))): if l in m._all_other_connections[i][j]: m._all_other_connections[i][j].remove(l) break for l in chain.from_iterable( chain.from_iterable(self._all_other_connections)): for i, j in product(range(len(m.ya)), range(len(m.xa))): if l in m._all_line_connections[i][j]: m._all_line_connections[i][j].remove(l) break self._all_line_connections = [[[] for _ in range(len(self.xa))] for _ in range(len(self.ya))] self._all_other_connections = [[[] for _ in range(len(self.xa))] for _ in range(len(self.ya))] self.disconnect() def set_pos(self, pos): """Move the point(s) to another position Parameters ---------- pos: tuple of 2 arrays The positions of the crosses. The first item marks the x-coordinates of the points, the second the y-coordinates""" self.remove(artists=False) self.xa[:] = pos[0] self.ya[:] = pos[1] self.draw_lines(**self._line_kwargs) self.connect() visible_connections = [ m for m, v in zip(self.other_marks, self._connection_visible) if v ] if visible_connections: self.connect_to_marks(visible_connections, True, append=False)
class ImageRescaler(StraditizerControlBase, QPushButton): """A button to rescale the straditize image""" rescaling = _temp_bool_prop( 'rescaling', "Boolean that is true if one of the axes is rescaling") #: A :class:`matplotlib.widgets.Slider` for specifying the size of the #: rescaled image slider = None #: The matplotlib image for the rescaled diagram im_rescale = None #: The matplotlib image for the original diagram im_orig = None #: The matplotlib axes for the :attr:`im_orig` ax_orig = None #: The matplotlib axes for the :attr:`im_rescale` ax_rescale = None #: The matplotlib figure for the rescaling fig = None def __init__(self, straditizer_widgets, item, *args, **kwargs): super(ImageRescaler, self).__init__('Rescale image') self.init_straditizercontrol(straditizer_widgets, item) self.widgets2disable = [self] self.clicked.connect(self.start_rescaling) def start_rescaling(self): """Create the rescaling figure""" self._create_rescale_figure() def _create_rescale_figure(self): import matplotlib.pyplot as plt from matplotlib.widgets import Slider import matplotlib.colorbar as mcbar self.fig, (self.ax_orig, self.ax_rescale) = plt.subplots( 2, 1, figsize=(8, 12), gridspec_kw=dict(top=1.0, bottom=0.0)) slider_ax, kw = mcbar.make_axes_gridspec( self.ax_rescale, orientation='horizontal', location='bottom') slider_ax.set_aspect('auto') slider_ax._hold = True self.slider = Slider(slider_ax, 'Fraction', 0, 100, valfmt='%1.3g %%') self.slider.set_val(100) self.slider.on_changed(self.rescale_plot) self.im_orig = self.ax_orig.imshow(self.straditizer.image) self.im_rescale = self.ax_rescale.imshow(self.straditizer.image) # connect limits self.ax_orig.callbacks.connect('xlim_changed', self.adjust_rescaled_limits) self.ax_orig.callbacks.connect('ylim_changed', self.adjust_rescaled_limits) self.ax_rescale.callbacks.connect('xlim_changed', self.adjust_orig_limits) self.ax_rescale.callbacks.connect('ylim_changed', self.adjust_orig_limits) self.fig.canvas.mpl_connect('resize_event', self.equalize_axes) self.connect2apply(self.rescale, self.close_figs) self.connect2cancel(self.close_figs) self.raise_figure() self.equalize_axes() def resize_stradi_image(self, percentage): """Resize the straditizer image Parameters ---------- percentage: float A float between 0 and 100 specifying the target size of the :attr:`straditize.straditizer.Straditizer.image` Returns ------- PIL.Image.Image The resized :attr:`~straditize.straditizer.Straditizer.image` of the current straditizer""" w, h = self.straditizer.image.size new_size = (int(round(w * percentage / 100.)), int(round(h * percentage / 100.))) return self.straditizer.image.resize(new_size) def raise_figure(self): """Raise the figure for rescaling""" from psyplot_gui.main import mainwindow if mainwindow.figures: dock = self.fig.canvas.manager.window dock.widget().show_plugin() dock.raise_() def rescale_plot(self, percentage): """Replot :attr:`im_rescale` after adjustments of the :attr:`slider`""" self.im_rescale.remove() self.im_rescale = self.ax_rescale.imshow( self.resize_stradi_image(percentage)) self.adjust_rescaled_limits() def adjust_rescaled_limits(self, *args, **kwargs): """Readjust :attr:`ax_rescale` after changes in :attr:`ax_orig`""" if self.rescaling: return with self.rescaling: x0, x1 = self.ax_orig.get_xlim() y0, y1 = self.ax_orig.get_ylim() fraction = self.slider.val / 100. self.ax_rescale.set_xlim(x0 * fraction, x1 * fraction) self.ax_rescale.set_ylim(y0 * fraction, y1 * fraction) self.draw_figure() def adjust_orig_limits(self, *args, **kwargs): """Readjust :attr:`ax_orig` after changes in :attr:`ax_rescale`""" if self.rescaling: return with self.rescaling: x0, x1 = self.ax_rescale.get_xlim() y0, y1 = self.ax_rescale.get_ylim() fraction = self.slider.val / 100. self.ax_orig.set_xlim(x0 / fraction, x1 / fraction) self.ax_orig.set_ylim(y0 / fraction, y1 / fraction) self.draw_figure() def equalize_axes(self, event=None): """Set both axes to the same size""" rescale_pos = self.ax_rescale.get_position() self.ax_orig.set_position(( rescale_pos.x0, 0.55, rescale_pos.width, rescale_pos.height)) def draw_figure(self): self.fig.canvas.draw() def rescale(self, ask=None): """Rescale and start a new straditizer Parameters ---------- ask: bool Whether to ask with a QMessageBox. If None, it defaults to the :attr:`straditize.widgets.StraditizerWidgers.always_yes`""" if ask is None: ask = not self.straditizer_widgets.always_yes answer = QMessageBox.Yes if not ask else QMessageBox.question( self, 'Restart project?', 'This will close the straditizer and create new figures. ' 'Are you sure, you want to continue?') if answer == QMessageBox.Yes: image = self.resize_stradi_image(self.slider.val) attrs = self.straditizer.attrs self.straditizer_widgets.close_straditizer() self.straditizer_widgets.menu_actions.open_straditizer( image, attrs=attrs) def close_figs(self): """Close the :attr:`fig`""" import matplotlib.pyplot as plt plt.close(self.fig.number) del self.fig, self.ax_orig, self.ax_rescale, self.im_rescale, \ self.im_orig, self.slider def should_be_enabled(self, w): return self.straditizer is not None
class FormatoptionWidget(QWidget, DockMixin): """ Widget to update the formatoptions of the current project This widget, mainly made out of a combobox for the formatoption group, a combobox for the formatoption, and a text editor, is designed for updating the selected formatoptions for the current subproject. The widget is connected to the :attr:`psyplot.project.Project.oncpchange` signal and refills the comboboxes if the current subproject changes. The text editor either accepts python code that will be executed by the given `console`, or yaml code. """ no_fmtos_update = _temp_bool_prop('no_fmtos_update', """update the fmto combo box or not""") #: The combobox for the formatoption groups group_combo = None #: The combobox for the formatoptions fmt_combo = None #: The help_explorer to display the documentation of the formatoptions help_explorer = None #: The formatoption specific widget that is loaded from the formatoption fmt_widget = None #: A line edit for updating the formatoptions line_edit = None #: A multiline text editor for updating the formatoptions text_edit = None #: A button to switch between :attr:`line_edit` and :attr:`text_edit` multiline_button = None @property def shell(self): """The shell to execute the update of the formatoptions in the current project""" return self.console.kernel_manager.kernel.shell def __init__(self, *args, **kwargs): """ Parameters ---------- help_explorer: psyplot_gui.help_explorer.HelpExplorer The help explorer to show the documentation of one formatoption console: psyplot_gui.console.ConsoleWidget The console that can be used to update the current subproject via:: psy.gcp().update(**kwargs) where ``**kwargs`` is defined through the selected formatoption in the :attr:`fmt_combo` combobox and the value in the :attr:`line_edit` editor ``*args, **kwargs`` Any other keyword for the QWidget class """ help_explorer = kwargs.pop('help_explorer', None) console = kwargs.pop('console', None) super(FormatoptionWidget, self).__init__(*args, **kwargs) self.help_explorer = help_explorer self.console = console self.error_msg = PyErrorMessage(self) # --------------------------------------------------------------------- # -------------------------- Child widgets ---------------------------- # --------------------------------------------------------------------- self.group_combo = QComboBox(parent=self) self.fmt_combo = QComboBox(parent=self) self.line_edit = QLineEdit(parent=self) self.text_edit = QTextEdit(parent=self) self.run_button = QToolButton(parent=self) # completer for the fmto widget self.fmt_combo.setEditable(True) self.fmt_combo.setInsertPolicy(QComboBox.NoInsert) self.fmto_completer = completer = QCompleter( ['time', 'lat', 'lon', 'lev']) completer.setCompletionMode(QCompleter.PopupCompletion) completer.activated[str].connect(self.set_fmto) if with_qt5: completer.setFilterMode(Qt.MatchContains) completer.setModel(QStandardItemModel()) self.fmt_combo.setCompleter(completer) self.dim_widget = DimensionsWidget(parent=self) self.dim_widget.setVisible(False) self.multiline_button = QPushButton('Multiline', parent=self) self.multiline_button.setCheckable(True) self.yaml_cb = QCheckBox('Yaml syntax') self.yaml_cb.setChecked(True) self.keys_button = QPushButton('Keys', parent=self) self.summaries_button = QPushButton('Summaries', parent=self) self.docs_button = QPushButton('Docs', parent=self) self.grouped_cb = QCheckBox('grouped', parent=self) self.all_groups_cb = QCheckBox('all groups', parent=self) self.include_links_cb = QCheckBox('include links', parent=self) self.text_edit.setVisible(False) # --------------------------------------------------------------------- # -------------------------- Descriptions ----------------------------- # --------------------------------------------------------------------- self.group_combo.setToolTip('Select the formatoption group') self.fmt_combo.setToolTip('Select the formatoption to update') self.line_edit.setToolTip( 'Insert the value which what you want to update the selected ' 'formatoption and hit right button. The code is executed in the ' 'main console.') self.yaml_cb.setToolTip( "Use the yaml syntax for the values inserted in the above cell. " "Otherwise the content there is evaluated as a python expression " "in the terminal") self.text_edit.setToolTip(self.line_edit.toolTip()) self.run_button.setIcon(QIcon(get_icon('run_arrow.png'))) self.run_button.setToolTip('Update the selected formatoption') self.multiline_button.setToolTip( 'Allow linebreaks in the text editor line above.') self.keys_button.setToolTip( 'Show the formatoption keys in this group (or in all ' 'groups) in the help explorer') self.summaries_button.setToolTip( 'Show the formatoption summaries in this group (or in all ' 'groups) in the help explorer') self.docs_button.setToolTip( 'Show the formatoption documentations in this group (or in all ' 'groups) in the help explorer') self.grouped_cb.setToolTip( 'Group the formatoptions before displaying them in the help ' 'explorer') self.all_groups_cb.setToolTip('Use all groups when displaying the ' 'keys, docs or summaries') self.include_links_cb.setToolTip( 'Include links to remote documentations when showing the ' 'keys, docs and summaries in the help explorer (requires ' 'intersphinx)') # --------------------------------------------------------------------- # -------------------------- Connections ------------------------------ # --------------------------------------------------------------------- self.group_combo.currentIndexChanged[int].connect(self.fill_fmt_combo) self.fmt_combo.currentIndexChanged[int].connect(self.show_fmt_info) self.fmt_combo.currentIndexChanged[int].connect(self.load_fmt_widget) self.fmt_combo.currentIndexChanged[int].connect( self.set_current_fmt_value) self.run_button.clicked.connect(self.run_code) self.line_edit.returnPressed.connect(self.run_button.click) self.multiline_button.clicked.connect(self.toggle_line_edit) self.keys_button.clicked.connect( partial(self.show_all_fmt_info, 'keys')) self.summaries_button.clicked.connect( partial(self.show_all_fmt_info, 'summaries')) self.docs_button.clicked.connect( partial(self.show_all_fmt_info, 'docs')) # --------------------------------------------------------------------- # ------------------------------ Layouts ------------------------------ # --------------------------------------------------------------------- self.combos = QHBoxLayout() self.combos.addWidget(self.group_combo) self.combos.addWidget(self.fmt_combo) self.execs = QHBoxLayout() self.execs.addWidget(self.line_edit) self.execs.addWidget(self.text_edit) self.execs.addWidget(self.run_button) self.info_box = QHBoxLayout() self.info_box.addWidget(self.multiline_button) self.info_box.addWidget(self.yaml_cb) self.info_box.addStretch(0) for w in [ self.keys_button, self.summaries_button, self.docs_button, self.all_groups_cb, self.grouped_cb, self.include_links_cb ]: self.info_box.addWidget(w) self.vbox = QVBoxLayout() self.vbox.addLayout(self.combos) self.vbox.addWidget(self.dim_widget) self.vbox.addLayout(self.execs) self.vbox.addLayout(self.info_box) self.vbox.setSpacing(0) self.setLayout(self.vbox) # fill with content self.fill_combos_from_project(psy.gcp()) psy.Project.oncpchange.connect(self.fill_combos_from_project) rcParams.connect('fmt.sort_by_key', self.refill_from_rc) def refill_from_rc(self, sort_by_key): from psyplot.project import gcp self.fill_combos_from_project(gcp()) def fill_combos_from_project(self, project): """Fill :attr:`group_combo` and :attr:`fmt_combo` from a project Parameters ---------- project: psyplot.project.Project The project to use""" if rcParams['fmt.sort_by_key']: def sorter(fmto): return fmto.key else: sorter = self.get_name current_text = self.group_combo.currentText() with self.no_fmtos_update: self.group_combo.clear() if project is None or project.is_main or not len(project): self.fmt_combo.clear() self.groups = [] self.fmtos = [] self.line_edit.setEnabled(False) return self.line_edit.setEnabled(True) # get dimensions it_vars = chain.from_iterable(arr.psy.iter_base_variables for arr in project.arrays) dims = next(it_vars).dims sdims = set(dims) for var in it_vars: sdims.intersection_update(var.dims) coords = [d for d in dims if d in sdims] coords_name = [COORDSGROUP] if coords else [] coords_verbose = ['Dimensions'] if coords else [] coords = [coords] if coords else [] if len(project.plotters): # get formatoptions and group them alphabetically grouped_fmts = defaultdict(list) for fmto in project._fmtos: grouped_fmts[fmto.group].append(fmto) for val in six.itervalues(grouped_fmts): val.sort(key=sorter) grouped_fmts = OrderedDict( sorted(six.iteritems(grouped_fmts), key=lambda t: psyp.groups.get(t[0], t[0]))) fmt_groups = list(grouped_fmts.keys()) # save original names self.groups = coords_name + [ALLGROUP] + fmt_groups # save verbose group names (which are used in the combo box) self.groupnames = ( coords_verbose + ['All formatoptions'] + list(map(lambda s: psyp.groups.get(s, s), fmt_groups))) # save formatoptions fmtos = list(grouped_fmts.values()) self.fmtos = coords + [sorted(chain(*fmtos), key=sorter) ] + fmtos else: self.groups = coords_name self.groupnames = coords_verbose self.fmtos = coords self.group_combo.addItems(self.groupnames) ind = self.group_combo.findText(current_text) self.group_combo.setCurrentIndex(ind if ind >= 0 else 0) self.fill_fmt_combo(self.group_combo.currentIndex()) def get_name(self, fmto): """Get the name of a :class:`psyplot.plotter.Formatoption` instance""" if isinstance(fmto, six.string_types): return fmto return '%s (%s)' % (fmto.name, fmto.key) if fmto.name else fmto.key @property def fmto(self): return self.fmtos[self.group_combo.currentIndex()][ self.fmt_combo.currentIndex()] @fmto.setter def fmto(self, value): name = self.get_name(value) for i, fmtos in enumerate(self.fmtos): if i == 1: # all formatoptions continue if name in map(self.get_name, fmtos): with self.no_fmtos_update: self.group_combo.setCurrentIndex(i) self.fill_fmt_combo(i, name) return def toggle_line_edit(self): """Switch between the :attr:`line_edit` and :attr:`text_edit` This method is called when the :attr:`multiline_button` is clicked and switches between the single line :attr:``line_edit` and the multiline :attr:`text_edit` """ # switch to multiline text edit if (self.multiline_button.isChecked() and not self.text_edit.isVisible()): self.line_edit.setVisible(False) self.text_edit.setVisible(True) self.text_edit.setPlainText(self.line_edit.text()) elif (not self.multiline_button.isChecked() and not self.line_edit.isVisible()): self.line_edit.setVisible(True) self.text_edit.setVisible(False) self.line_edit.setText(self.text_edit.toPlainText()) def fill_fmt_combo(self, i, current_text=None): """Fill the :attr:`fmt_combo` combobox based on the current group name """ if not self.no_fmtos_update: with self.no_fmtos_update: if current_text is None: current_text = self.fmt_combo.currentText() self.fmt_combo.clear() self.fmt_combo.addItems(list(map(self.get_name, self.fmtos[i]))) ind = self.fmt_combo.findText(current_text) self.fmt_combo.setCurrentIndex(ind if ind >= 0 else 0) # update completer model self.setup_fmt_completion_model() idx = self.fmt_combo.currentIndex() self.show_fmt_info(idx) self.load_fmt_widget(idx) self.set_current_fmt_value(idx) def set_fmto(self, name): self.fmto = name def setup_fmt_completion_model(self): fmtos = list( unique_everseen(map(self.get_name, chain.from_iterable(self.fmtos)))) model = self.fmto_completer.model() model.setRowCount(len(fmtos)) for i, name in enumerate(fmtos): model.setItem(i, QStandardItem(name)) def load_fmt_widget(self, i): """Load the formatoption specific widget This method loads the formatoption specific widget from the :meth:`psyplot.plotter.Formatoption.get_fmt_widget` method and displays it above the :attr:`line_edit` Parameters ---------- i: int The index of the current formatoption""" self.remove_fmt_widget() group_ind = self.group_combo.currentIndex() if not self.no_fmtos_update: from psyplot.project import gcp if self.groups[group_ind] == COORDSGROUP: dim = self.fmtos[group_ind][i] self.fmt_widget = self.dim_widget self.dim_widget.set_dim(dim) self.dim_widget.set_single_selection(dim not in gcp()[0].dims) self.dim_widget.setVisible(True) else: fmto = self.fmtos[group_ind][i] self.fmt_widget = fmto.get_fmt_widget(self, gcp()) if self.fmt_widget is not None: self.vbox.insertWidget(2, self.fmt_widget) def reset_fmt_widget(self): idx = self.fmt_combo.currentIndex() self.load_fmt_widget(idx) self.set_current_fmt_value(idx) def remove_fmt_widget(self): if self.fmt_widget is not None: self.fmt_widget.hide() if self.fmt_widget is self.dim_widget: self.fmt_widget.reset_combobox() else: self.vbox.removeWidget(self.fmt_widget) self.fmt_widget.close() del self.fmt_widget def set_current_fmt_value(self, i): """Add the value of the current formatoption to the line text""" group_ind = self.group_combo.currentIndex() if not self.no_fmtos_update: if self.groups[group_ind] == COORDSGROUP: from psyplot.project import gcp dim = self.fmtos[group_ind][i] self.set_obj(gcp().arrays[0].psy.idims[dim]) else: fmto = self.fmtos[group_ind][i] self.set_obj(fmto.value) def show_fmt_info(self, i): """Show the documentation of the formatoption in the help explorer """ group_ind = self.group_combo.currentIndex() if (not self.no_fmtos_update and self.groups[group_ind] != COORDSGROUP): fmto = self.fmtos[self.group_combo.currentIndex()][i] fmto.plotter.show_docs( fmto.key, include_links=self.include_links_cb.isChecked()) def run_code(self): """Run the update of the project inside the :attr:`shell`""" if self.line_edit.isVisible(): text = str(self.line_edit.text()) else: text = str(self.text_edit.toPlainText()) if not text or not self.fmtos: return group_ind = self.group_combo.currentIndex() if self.groups[group_ind] == COORDSGROUP: key = self.fmtos[group_ind][self.fmt_combo.currentIndex()] param = 'dims' else: key = self.fmtos[group_ind][self.fmt_combo.currentIndex()].key param = 'fmt' if self.yaml_cb.isChecked(): import psyplot.project as psy psy.gcp().update(**{key: yaml.load(text, Loader=yaml.Loader)}) else: code = "psy.gcp().update(%s={'%s': %s})" % (param, key, text) if ExecutionInfo is not None: info = ExecutionInfo(raw_cell=code, store_history=False, silent=True, shell_futures=False) e = ExecutionResult(info) else: e = ExecutionResult() self.console.run_command_in_shell(code, e) try: e.raise_error() except Exception: # reset the console and clear the error message raise finally: self.console.reset() def get_text(self): """Get the current update text""" if self.line_edit.isVisible(): return self.line_edit.text() else: return self.text_edit.toPlainText() def get_obj(self): """Get the current update text""" if self.line_edit.isVisible(): txt = self.line_edit.text() else: txt = self.text_edit.toPlainText() try: obj = yaml.load(txt, Loader=yaml.Loader) except Exception: self.error_msg.showTraceback("Could not load %s" % txt) else: return obj def insert_obj(self, obj): """Add a string to the formatoption widget""" current = self.get_text() use_yaml = self.yaml_cb.isChecked() use_line_edit = self.line_edit.isVisible() # strings are treated separately such that we consider quotation marks # at the borders if isstring(obj) and current: if use_line_edit: pos = self.line_edit.cursorPosition() else: pos = self.text_edit.textCursor().position() if pos not in [0, len(current)]: s = obj else: if current[0] in ['"', "'"]: current = current[1:-1] self.clear_text() if pos == 0: s = '"' + obj + current + '"' else: s = '"' + current + obj + '"' current = '' elif isstring(obj): # add quotation marks s = '"' + obj + '"' elif not use_yaml: s = repr(obj) else: s = yaml.dump(obj, default_flow_style=True).strip() if s.endswith('\n...'): s = s[:-4] if use_line_edit: self.line_edit.insert(s) else: self.text_edit.insertPlainText(s) def clear_text(self): if self.line_edit.isVisible(): self.line_edit.clear() else: self.text_edit.clear() def set_obj(self, obj): self.clear_text() self.insert_obj(obj) def show_all_fmt_info(self, what): """Show the keys, summaries or docs of the formatoptions Calling this function let's the help browser show the documentation etc. of all docs or only the selected group determined by the state of the :attr:`grouped_cb` and :attr:`all_groups_cb` checkboxes Parameters ---------- what: {'keys', 'summaries', 'docs'} Determines what to show""" if not self.fmtos: return if (self.all_groups_cb.isChecked() or self.group_combo.currentIndex() < 2): fmtos = list( chain.from_iterable( fmto_group for i, fmto_group in enumerate(self.fmtos) if self.groups[i] not in [ALLGROUP, COORDSGROUP])) else: fmtos = self.fmtos[self.group_combo.currentIndex()] plotter = fmtos[0].plotter getattr(plotter, 'show_' + what)([fmto.key for fmto in fmtos], grouped=self.grouped_cb.isChecked(), include_links=self.include_links_cb.isChecked())