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 UrlHelp(UrlBrowser, HelpMixin): """Class to convert rst docstrings to html and show browsers""" #: Object containing the necessary fields to describe an object given to #: the help widget. The descriptor is set up by the :meth:`describe_object` #: method and contains an additional objtype attribute object_descriptor = namedtuple('ObjectDescriptor', ['obj', 'name', 'objtype']) can_document_object = with_sphinx can_show_rst = with_sphinx #: menu button with different urls bt_url_menus = None #: sphinx_thread = None def __init__(self, *args, **kwargs): self._temp_dir = 'sphinx_dir' not in kwargs self.sphinx_dir = kwargs.pop('sphinx_dir', mkdtemp(prefix='psyplot_')) self.build_dir = osp.join(self.sphinx_dir, '_build', 'html') super(UrlHelp, self).__init__(*args, **kwargs) self.error_msg = PyErrorMessage(self) if with_sphinx: self.sphinx_thread = SphinxThread(self.sphinx_dir) self.sphinx_thread.html_ready[str].connect(self.browse) self.sphinx_thread.html_error[str].connect( self.error_msg.showTraceback) self.sphinx_thread.html_error[str].connect(logger.debug) rcParams.connect('help_explorer.render_docs_parallel', self.reset_sphinx) rcParams.connect('help_explorer.use_intersphinx', self.reset_sphinx) rcParams.connect('help_explorer.online', self.reset_sphinx) self.bt_connect_console = QToolButton(self) self.bt_connect_console.setCheckable(True) if rcParams['console.connect_to_help']: self.bt_connect_console.setIcon( QIcon(get_icon('ipython_console.png'))) self.bt_connect_console.click() else: self.bt_connect_console.setIcon( QIcon(get_icon('ipython_console_t.png'))) self.bt_connect_console.clicked.connect(self.toogle_connect_console) rcParams.connect('console.connect_to_help', self.update_connect_console) self.toogle_connect_console() # menu button with different urls self.bt_url_menus = QToolButton(self) self.bt_url_menus.setIcon(QIcon(get_icon('docu_button.png'))) self.bt_url_menus.setToolTip('Browse documentations') self.bt_url_menus.setPopupMode(QToolButton.InstantPopup) docu_menu = QMenu(self) for name, url in six.iteritems(self.doc_urls): def to_url(b, url=url): self.browse(url) action = QAction(name, self) action.triggered.connect(to_url) docu_menu.addAction(action) self.bt_url_menus.setMenu(docu_menu) self.button_box.addWidget(self.bt_connect_console) self.button_box.addWidget(self.bt_url_menus) # toogle the lock again to set the bt_url_menus enabled state self.toogle_url_lock() def update_connect_console(self, connect): if (connect and not self.bt_connect_console.isChecked() or not connect and self.bt_connect_console.isChecked()): self.bt_connect_console.click() def toogle_connect_console(self): """Disable (or enable) the loading of web pages in www""" bt = self.bt_connect_console connect = bt.isChecked() bt.setIcon( QIcon( get_icon('ipython_console.png' if connect else 'ipython_console_t.png'))) bt.setToolTip("%sonnect the console to the help explorer" % ("Don't c" if connect else "C")) if rcParams['console.connect_to_help'] is not connect: rcParams['console.connect_to_help'] = connect def reset_sphinx(self, value): """Method that is called if the configuration changes""" if with_sphinx and hasattr(self.sphinx_thread, 'app'): del self.sphinx_thread.app @docstrings.dedent def show_help(self, obj, oname='', files=None): """ Render the rst docu for the given object with sphinx and show it Parameters ---------- %(HelpMixin.show_help.parameters)s """ if self.bt_lock.isChecked(): return return super(UrlHelp, self).show_help(obj, oname=oname, files=files) @docstrings.dedent def show_intro(self, text=''): """ Show the intro text in the explorer Parameters ---------- %(HelpMixin.show_intro.parameters)s""" if self.sphinx_thread is not None: with open(self.sphinx_thread.index_file, 'a') as f: f.write('\n' + text.strip() + '\n\n' + 'Table of Contents\n' '=================\n\n.. toctree::\n') self.sphinx_thread.render(None, None) def show_rst(self, text, oname='', descriptor=None, files=None): """Render restructured text with sphinx and show it Parameters ---------- %(HelpMixin.show_rst.parameters)s""" if self.bt_lock.isChecked() or self.sphinx_thread is None: return False if not oname and descriptor: oname = descriptor.name for f in files or []: shutil.copyfile(f, osp.join(self.sphinx_dir, osp.basename(f))) self.sphinx_thread.render(text, oname) return True def describe_object(self, obj, oname=''): """Describe an object using additionaly the object type from the :meth:`get_objtype` method Returns ------- instance of :attr:`object_descriptor` The descriptor of the object""" return self.object_descriptor(obj, oname, self.get_objtype(obj)) def browse(self, url): """Reimplemented to add file paths to the url string""" url = asstring(url) html_file = osp.join(self.sphinx_dir, '_build', 'html', url + '.html') if osp.exists(html_file): url = file2html(html_file) super(UrlHelp, self).browse(url) def toogle_url_lock(self): """Disable (or enable) the loading of web pages in www""" super(UrlHelp, self).toogle_url_lock() # enable or disable documentation button bt = self.bt_url_lock offline = bt.isChecked() try: self.bt_url_menus.setEnabled(not offline) except AttributeError: # not yet initialized pass def url_changed(self, url): """Reimplemented to remove file paths from the url string""" try: url = asstring(url.toString()) except AttributeError: pass if url.startswith('file://'): fname = html2file(url) if osp.samefile(self.build_dir, osp.commonprefix([fname, self.build_dir])): url = osp.splitext(osp.basename(fname))[0] super(UrlHelp, self).url_changed(url) def header(self, descriptor, sig): return '%(name)s\n%(bars)s\n\n.. py:%(type)s:: %(name)s%(sig)s\n' % { 'name': descriptor.name, 'bars': '-' * len(descriptor.name), 'type': descriptor.objtype, 'sig': sig } def get_objtype(self, obj): """Get the object type of the given object and determine wheter the object is considered a class, a module, a function, method or data Parameters ---------- obj: object Returns ------- str One out of {'class', 'module', 'function', 'method', 'data'}""" if inspect.isclass(obj): return 'class' if inspect.ismodule(obj): return 'module' if inspect.isfunction(obj) or isinstance(obj, type(all)): return 'function' if inspect.ismethod(obj) or isinstance(obj, type(str.upper)): return 'method' return 'data' def is_importable(self, modname): """Determine whether members of the given module can be documented with sphinx by using the :func:`sphinx.util.get_module_source` function Parameters ---------- modname: str The __name__ attribute of the module to import Returns ------- bool True if sphinx can import the module""" try: get_module_source(modname) return True except Exception: return False def get_doc(self, descriptor): """Reimplemented to (potentially) use the features from sphinx.ext.autodoc""" obj = descriptor.obj if inspect.ismodule(obj): module = obj else: module = inspect.getmodule(obj) if module is not None and (re.match('__.*__', module.__name__) or not self.is_importable(module.__name__)): module = None isclass = inspect.isclass(obj) # If the module is available, we try to use autodoc if module is not None: doc = '.. currentmodule:: ' + module.__name__ + '\n\n' # a module --> use automodule if inspect.ismodule(obj): doc += self.header(descriptor, '') doc += '.. automodule:: ' + obj.__name__ # an importable class --> use autoclass elif isclass and getattr(module, obj.__name__, None) is not None: doc += self.header(descriptor, '') doc += '.. autoclass:: ' + obj.__name__ # an instance and the class can be imported # --> use super get_doc and autoclass for the tyoe elif descriptor.objtype == 'data' and getattr( module, type(obj).__name__, None) is not None: doc += '\n\n'.join([ super(UrlHelp, self).get_doc(descriptor), "Class docstring\n===============", '.. autoclass:: ' + type(obj).__name__ ]) # an instance --> use super get_doc for instance and the type elif descriptor.objtype == 'data': cls_doc = super(UrlHelp, self).get_doc( self.describe_object(type(obj), type(obj).__name__)) doc += '\n\n'.join([ super(UrlHelp, self).get_doc(descriptor), "Class docstring\n===============", cls_doc ]) # a function or method --> use super get_doc else: doc += super(UrlHelp, self).get_doc(descriptor) # otherwise the object has been defined in this session else: # an instance --> use super get_doc for instance and the type if descriptor.objtype == 'data': cls_doc = super(UrlHelp, self).get_doc( self.describe_object(type(obj), type(obj).__name__)) doc = '\n\n'.join([ super(UrlHelp, self).get_doc(descriptor), "Class docstring\n===============", cls_doc ]) # a function or method --> use super get_doc else: doc = super(UrlHelp, self).get_doc(descriptor) return doc.rstrip() + '\n' def process_docstring(self, lines, descriptor): """Process the lines with the napoleon sphinx extension""" lines = list(chain(*(l.splitlines() for l in lines))) lines = NumpyDocstring(lines, what=descriptor.objtype, name=descriptor.name, obj=descriptor.obj).lines() lines = GoogleDocstring(lines, what=descriptor.objtype, name=descriptor.name, obj=descriptor.obj).lines() return indent( super(UrlHelp, self).process_docstring(lines, descriptor)) def close(self, *args, **kwargs): if self.sphinx_thread is not None: try: del self.sphinx_thread.app except AttributeError: pass shutil.rmtree(self.build_dir, ignore_errors=True) if self._temp_dir: shutil.rmtree(self.sphinx_dir, ignore_errors=True) del self.sphinx_thread return super(UrlHelp, self).close(*args, **kwargs)
class StraditizerWidgets(QWidget, DockMixin): """A widget that contains widgets to control the straditization in a GUI This widget is the basis of the straditize GUI and implemented as a plugin into the psyplot gui. The open straditizers are handled in the :attr:`_straditizer` attribute. The central parts of this widget are - The combobox to manage the open straditizers - The QTreeWidget in the :attr:`tree` attribute that contains all the controls to interface the straditizer - the tutorial area - the :guilabel:`Apply` and :guilabel:`Cancel` button""" #: Boolean that is True if all dialogs should be answered with `Yes` always_yes = False #: The QTreeWidget that contains the different widgets for the digitization tree = None #: The apply button apply_button = None #: The cancel button cancel_button = None #: The button to edit the straditizer attributes attrs_button = None #: The button to start a tutorial tutorial_button = None #: An :class:`InfoButton` to display the docs info_button = None #: A QComboBox to select the current straditizer stradi_combo = None #: A button to open a new straditizer btn_open_stradi = None #: A button to close the current straditizer btn_close_stradi = None #: A button to reload the last autosaved state btn_reload_autosaved = None #: The :class:`straditize.widgets.progress_widget.ProgressWidget` to #: display the progress of the straditization progress_widget = None #: The :class:`straditize.widgets.data.DigitizingControl` to interface #: the :straditize.straditizer.Straditizer.data_reader` digitizer = None #: The :class:`straditize.widgets.colnames.ColumnNamesManager` to interface #: the :straditize.straditizer.Straditizer.colnames_reader` colnames_manager = None #: The :class:`straditize.widgets.axes_translations.AxesTranslations` to #: handle the y- and x-axis conversions axes_translations = None #: The :class:`straditize.widgets.image_correction.ImageRescaler` class to #: rescale the image image_rescaler = None #: The :class:`straditize.widgets.image_correction.ImageRotator` class to #: rotate the image image_rotator = None #: The :class:`straditize.widgets.plots.PlotControl` to display additional #: information on the diagram plot_control = None #: The :class:`straditize.widgets.marker_control.MarkerControl` to modify #: the appearance of the :class:`~straditize.straditizer.Straditizer.marks` #: of the current straditizer marker_control = None #: The :class:`straditize.widgets.selection_toolbar.SelectionToolbar` to #: select features in the stratigraphic diagram selection_toolbar = None #: The :class:`straditize.straditizer.Straditizer` instance straditizer = None #: open straditizers _straditizers = [] #: The :class:`straditize.widgets.tutorial.Tutorial` class tutorial = None dock_position = Qt.LeftDockWidgetArea #: Auto-saved straditizers autosaved = [] hidden = True title = 'Stratigraphic diagram digitization' window_layout_action = None open_external = QtCore.pyqtSignal(list) def __init__(self, *args, **kwargs): from straditize.widgets.menu_actions import StraditizerMenuActions from straditize.widgets.progress_widget import ProgressWidget from straditize.widgets.data import DigitizingControl from straditize.widgets.selection_toolbar import SelectionToolbar from straditize.widgets.marker_control import MarkerControl from straditize.widgets.plots import PlotControl from straditize.widgets.axes_translations import AxesTranslations from straditize.widgets.image_correction import (ImageRotator, ImageRescaler) from straditize.widgets.colnames import ColumnNamesManager self._straditizers = [] super(StraditizerWidgets, self).__init__(*args, **kwargs) self.tree = QTreeWidget(parent=self) self.tree.setSelectionMode(QTreeWidget.NoSelection) self.refresh_button = QToolButton(self) self.refresh_button.setIcon(QIcon(get_psy_icon('refresh.png'))) self.refresh_button.setToolTip('Refresh from the straditizer') self.apply_button = EnableButton('Apply', parent=self) self.cancel_button = EnableButton('Cancel', parent=self) self.attrs_button = QPushButton('Attributes', parent=self) self.tutorial_button = QPushButton('Tutorial', parent=self) self.tutorial_button.setCheckable(True) self.error_msg = PyErrorMessage(self) self.stradi_combo = QComboBox() self.btn_open_stradi = QToolButton() self.btn_open_stradi.setIcon(QIcon(get_psy_icon('run_arrow.png'))) self.btn_close_stradi = QToolButton() self.btn_close_stradi.setIcon(QIcon(get_psy_icon('invalid.png'))) self.btn_reload_autosaved = QPushButton("Reload") self.btn_reload_autosaved.setToolTip( "Close the straditizer and reload the last autosaved project") # --------------------------------------------------------------------- # --------------------------- Tree widgets ---------------------------- # --------------------------------------------------------------------- self.tree.setHeaderLabels(['', '']) self.tree.setColumnCount(2) self.progress_item = QTreeWidgetItem(0) self.progress_item.setText(0, 'ToDo list') self.progress_widget = ProgressWidget(self, self.progress_item) self.menu_actions_item = QTreeWidgetItem(0) self.menu_actions_item.setText(0, 'Images import/export') self.tree.addTopLevelItem(self.menu_actions_item) self.menu_actions = StraditizerMenuActions(self) self.digitizer_item = item = QTreeWidgetItem(0) item.setText(0, 'Digitization control') self.digitizer = DigitizingControl(self, item) self.col_names_item = item = QTreeWidgetItem(0) item.setText(0, 'Column names') self.colnames_manager = ColumnNamesManager(self, item) self.add_info_button(item, 'column_names.rst') self.axes_translations_item = item = QTreeWidgetItem(0) item.setText(0, 'Axes translations') self.axes_translations = AxesTranslations(self, item) self.image_transform_item = item = QTreeWidgetItem(0) item.setText(0, 'Transform source image') self.image_rescaler = ImageRescaler(self, item) self.image_rotator_item = item = QTreeWidgetItem(0) item.setText(0, 'Rotate image') self.image_rotator = ImageRotator(self) self.image_transform_item.addChild(item) self.image_rotator.setup_children(item) self.plot_control_item = item = QTreeWidgetItem(0) item.setText(0, 'Plot control') self.plot_control = PlotControl(self, item) self.add_info_button(item, 'plot_control.rst') self.marker_control_item = item = QTreeWidgetItem(0) item.setText(0, 'Marker control') self.marker_control = MarkerControl(self, item) self.add_info_button(item, 'marker_control.rst') # --------------------------------------------------------------------- # ----------------------------- Toolbars ------------------------------ # --------------------------------------------------------------------- self.selection_toolbar = SelectionToolbar(self, 'Selection toolbar') # --------------------------------------------------------------------- # ----------------------------- InfoButton ---------------------------- # --------------------------------------------------------------------- self.info_button = InfoButton(self, get_doc_file('straditize.rst')) # --------------------------------------------------------------------- # --------------------------- Layouts --------------------------------- # --------------------------------------------------------------------- stradi_box = QHBoxLayout() stradi_box.addWidget(self.stradi_combo, 1) stradi_box.addWidget(self.btn_open_stradi) stradi_box.addWidget(self.btn_close_stradi) attrs_box = QHBoxLayout() attrs_box.addWidget(self.attrs_button) attrs_box.addStretch(0) attrs_box.addWidget(self.tutorial_button) btn_box = QHBoxLayout() btn_box.addWidget(self.refresh_button) btn_box.addWidget(self.info_button) btn_box.addStretch(0) btn_box.addWidget(self.apply_button) btn_box.addWidget(self.cancel_button) reload_box = QHBoxLayout() reload_box.addWidget(self.btn_reload_autosaved) reload_box.addStretch(0) vbox = QVBoxLayout() vbox.addLayout(stradi_box) vbox.addWidget(self.tree) vbox.addLayout(attrs_box) vbox.addLayout(btn_box) vbox.addLayout(reload_box) self.setLayout(vbox) self.apply_button.setEnabled(False) self.cancel_button.setEnabled(False) self.tree.expandItem(self.progress_item) self.tree.expandItem(self.digitizer_item) # --------------------------------------------------------------------- # --------------------------- Connections ----------------------------- # --------------------------------------------------------------------- self.stradi_combo.currentIndexChanged.connect(self.set_current_stradi) self.refresh_button.clicked.connect(self.refresh) self.attrs_button.clicked.connect(self.edit_attrs) self.tutorial_button.clicked.connect(self.start_tutorial) self.open_external.connect(self._create_straditizer_from_args) self.btn_open_stradi.clicked.connect( self.menu_actions.open_straditizer) self.btn_close_stradi.clicked.connect(self.close_straditizer) self.btn_reload_autosaved.clicked.connect(self.reload_autosaved) self.refresh() header = self.tree.header() header.setStretchLastSection(False) header.setSectionResizeMode(0, QHeaderView.Stretch) def disable_apply_button(self): """Method that is called when the :attr:`cancel_button` is clicked""" for w in [self.apply_button, self.cancel_button]: try: w.clicked.disconnect() except TypeError: pass w.setEnabled(False) self.apply_button.setText('Apply') self.cancel_button.setText('Cancel') self.refresh_button.setEnabled(True) def switch_to_straditizer_layout(self): """Switch to the straditizer layout This method makes this widget visible and stacks it with the psyplot content widget""" mainwindow = self.dock.parent() mainwindow.figures_tree.hide_plugin() mainwindow.ds_tree.hide_plugin() mainwindow.fmt_widget.hide_plugin() self.show_plugin() mainwindow.tabifyDockWidget(mainwindow.project_content.dock, self.dock) hsize = self.marker_control.sizeHint().width() + 50 self.menu_actions.setup_shortcuts(mainwindow) if with_qt5: mainwindow.resizeDocks([self.dock], [hsize], Qt.Horizontal) self.tree.resizeColumnToContents(0) self.tree.resizeColumnToContents(1) self.info_button.click() def to_dock(self, main, *args, **kwargs): ret = super(StraditizerWidgets, self).to_dock(main, *args, **kwargs) if self.menu_actions.window_layout_action is None: main.window_layouts_menu.addAction(self.window_layout_action) main.callbacks['straditize'] = self.open_external.emit main.addToolBar(self.selection_toolbar) self.dock.toggleViewAction().triggered.connect( self.show_or_hide_toolbar) self.menu_actions.setup_menu_actions(main) self.menu_actions.setup_children(self.menu_actions_item) try: main.open_file_options['Straditize project'] = \ self.create_straditizer_from_args except AttributeError: # psyplot-gui <= 1.1.0 pass return ret def show_or_hide_toolbar(self): """Show or hide the toolbar depending on the visibility of this widget """ self.selection_toolbar.setVisible(self.is_shown) def _create_straditizer_from_args(self, args): """A method that is called when the :attr:`psyplot_gui.main.mainwindow` receives a 'straditize' callback""" self.create_straditizer_from_args(*args) def create_straditizer_from_args(self, fnames, project=None, xlim=None, ylim=None, full=False, reader_type='area'): """Create a straditizer from the given file name This method is called when the :attr:`psyplot_gui.main.mainwindow` receives a 'straditize' callback""" fname = fnames[0] if fname is not None: self.menu_actions.open_straditizer(fname) stradi = self.straditizer if stradi is None: return if xlim is not None: stradi.data_xlim = xlim if ylim is not None: stradi.data_ylim = ylim if xlim is not None or ylim is not None or full: if stradi.data_xlim is None: stradi.data_xlim = [0, np.shape(stradi.image)[1]] if stradi.data_ylim is None: stradi.data_ylim = [0, np.shape(stradi.image)[0]] stradi.init_reader(reader_type) stradi.data_reader.digitize() self.refresh() if not self.is_shown: self.switch_to_straditizer_layout() return fname is not None def start_tutorial(self, state, tutorial_cls=None): """Start or stop the tutorial Parameters ---------- state: bool If False, the tutorial is stopped. Otherwise it is started tutorial_cls: straditize.widgets.tutorial.beginner.Tutorial The tutorial class to use. If None, it will be asked in a QInputDialog""" if self.tutorial is not None or not state: self.tutorial.close() self.tutorial_button.setText('Tutorial') elif state: if tutorial_cls is None: tutorial_cls, ok = QInputDialog.getItem( self, 'Start tutorial', "Select the tutorial type", ["Beginner", "Advanced (Hoya del Castillo)"], editable=False) if not ok: self.tutorial_button.blockSignals(True) self.tutorial_button.setChecked(False) self.tutorial_button.blockSignals(False) return if tutorial_cls == 'Beginner': from straditize.widgets.tutorial import Tutorial else: from straditize.widgets.tutorial import ( HoyaDelCastilloTutorial as Tutorial) else: Tutorial = tutorial_cls self.tutorial = Tutorial(self) self.tutorial_button.setText('Stop tutorial') def edit_attrs(self): """Edit the attributes of the current straditizer This creates a new dataframe editor to edit the :attr:`straditize.straditizer.Straditizer.attrs` meta informations""" def add_attr(key): model = editor.table.model() n = len(attrs) model.insertRow(n) model.setData(model.index(n, 0), key) model.setData(model.index(n, 1), '', change_type=six.text_type) from psyplot_gui.main import mainwindow from straditize.straditizer import common_attributes attrs = self.straditizer.attrs editor = mainwindow.new_data_frame_editor(attrs, 'Straditizer attributes') editor.table.resizeColumnToContents(1) editor.table.horizontalHeader().setVisible(False) editor.table.frozen_table_view.horizontalHeader().setVisible(False) combo = QComboBox() combo.addItems([''] + common_attributes) combo.currentTextChanged.connect(add_attr) hbox = QHBoxLayout() hbox.addWidget(QLabel('Common attributes:')) hbox.addWidget(combo) hbox.addStretch(0) editor.layout().insertLayout(1, hbox) return editor, combo def refresh(self): """Refresh from the straditizer""" for i, stradi in enumerate(self._straditizers): self.stradi_combo.setItemText( i, self.get_attr(stradi, 'project_file') or self.get_attr(stradi, 'image_file') or '') # toggle visibility of close button and attributes button enable = self.straditizer is not None self.btn_close_stradi.setVisible(enable) self.attrs_button.setEnabled(enable) # refresh controls self.menu_actions.refresh() self.progress_widget.refresh() self.digitizer.refresh() self.selection_toolbar.refresh() self.plot_control.refresh() self.marker_control.refresh() self.axes_translations.refresh() if self.tutorial is not None: self.tutorial.refresh() self.image_rotator.refresh() self.image_rescaler.refresh() self.colnames_manager.refresh() self.btn_reload_autosaved.setEnabled(bool(self.autosaved)) def get_attr(self, stradi, attr): try: return stradi.get_attr(attr) except KeyError: pass docstrings.delete_params('InfoButton.parameters', 'parent') @docstrings.get_sectionsf('StraditizerWidgets.add_info_button') @docstrings.with_indent(8) def add_info_button(self, child, fname=None, rst=None, name=None, connections=[]): """Add an infobutton to the :attr:`tree` widget Parameters ---------- child: QTreeWidgetItem The item to which to add the infobutton %(InfoButton.parameters.no_parent)s connections: list of QPushButtons Buttons that should be clicked when the info button is clicked""" button = InfoButton(self, fname=fname, rst=rst, name=name) self.tree.setItemWidget(child, 1, button) for btn in connections: btn.clicked.connect(button.click) return button def raise_figures(self): """Raise the figures of the current straditizer in the GUI""" from psyplot_gui.main import mainwindow if mainwindow.figures and self.straditizer: dock = self.straditizer.ax.figure.canvas.manager.window dock.widget().show_plugin() dock.raise_() if self.straditizer.magni is not None: dock = self.straditizer.magni.ax.figure.canvas.manager.window dock.widget().show_plugin() dock.raise_() def set_current_stradi(self, i): """Set the i-th straditizer to the current one""" if not self._straditizers: return self.straditizer = self._straditizers[i] self.menu_actions.set_stradi_in_console() block = self.stradi_combo.blockSignals(True) self.stradi_combo.setCurrentIndex(i) self.stradi_combo.blockSignals(block) self.raise_figures() self.refresh() self.autosaved.clear() def _close_stradi(self, stradi): """Close the given straditizer and all it's figures""" is_current = stradi is self.straditizer if is_current: self.selection_toolbar.disconnect() stradi.close() try: i = self._straditizers.index(stradi) except ValueError: pass else: del self._straditizers[i] self.stradi_combo.removeItem(i) if is_current and self._straditizers: self.stradi_combo.setCurrentIndex(0) elif not self._straditizers: self.straditizer = None self.refresh() self.digitizer.digitize_item.takeChildren() self.digitizer.btn_digitize.setChecked(False) self.digitizer.btn_digitize.setCheckable(False) self.digitizer.toggle_txt_tolerance('') def close_straditizer(self): """Close the current straditizer""" self._close_stradi(self.straditizer) def close_all_straditizers(self): """Close all straditizers""" self.selection_toolbar.disconnect() for stradi in self._straditizers: stradi.close() self._straditizers.clear() self.straditizer = None self.stradi_combo.clear() self.digitizer.digitize_item.takeChildren() self.digitizer.btn_digitize.setChecked(False) self.digitizer.btn_digitize.setCheckable(False) self.digitizer.toggle_txt_tolerance('') self.refresh() def add_straditizer(self, stradi): """Add a straditizer to the list of open straditizers""" if stradi and stradi not in self._straditizers: self._straditizers.append(stradi) self.stradi_combo.addItem(' ') self.set_current_stradi(len(self._straditizers) - 1) def reset_control(self): """Reset the GUI of straditize""" if getattr(self.selection_toolbar, '_pattern_selection', None): self.selection_toolbar._pattern_selection.remove_plugin() del self.selection_toolbar._pattern_selection if getattr(self.digitizer, '_samples_editor', None): self.digitizer._close_samples_fig() tb = self.selection_toolbar tb.set_label_wand_mode() tb.set_rect_select_mode() tb.new_select_action.setChecked(True) tb.select_action.setChecked(False) tb.wand_action.setChecked(False) self.disable_apply_button() self.close_all_straditizers() self.colnames_manager.reset_control() def autosave(self): """Autosave the current straditizer""" self.autosaved = [self.straditizer.to_dataset().copy(True)] + \ self.autosaved[:4] def reload_autosaved(self): """Reload the autosaved straditizer and close the old one""" from straditize.straditizer import Straditizer if not self.autosaved: return answer = QMessageBox.question( self, 'Reload autosave', 'Shall I reload the last autosaved stage? This will close the ' 'current figures.') if answer == QMessageBox.Yes: self.close_straditizer() stradi = Straditizer.from_dataset(self.autosaved.pop(0)) self.menu_actions.finish_loading(stradi)
class DataFrameEditor(DockMixin, QWidget): """An editor for data frames""" dock_cls = DataFrameDock #: A signal that is emitted, if the table is cleared cleared = QtCore.pyqtSignal() #: A signal that is emitted when a cell has been changed. The argument #: is a tuple of two integers and one float: #: the row index, the column index and the new value cell_edited = QtCore.pyqtSignal(int, int, object, object) #: A signal that is emitted, if rows have been inserted into the dataframe. #: The first value is the integer of the (original) position of the row, #: the second one is the number of rows rows_inserted = QtCore.pyqtSignal(int, int) @property def hidden(self): return not self.table.filled def __init__(self, *args, **kwargs): super(DataFrameEditor, self).__init__(*args, **kwargs) self.error_msg = PyErrorMessage(self) # Label for displaying the DataFrame size self.lbl_size = QLabel() # A Checkbox for enabling and disabling the editability of the index self.cb_index_editable = QCheckBox('Index editable') # A checkbox for enabling and disabling the change of data types self.cb_dtypes_changeable = QCheckBox('Datatypes changeable') # A checkbox for enabling and disabling sorting self.cb_enable_sort = QCheckBox('Enable sorting') # A button to open a dataframe from the file self.btn_open_df = QToolButton(parent=self) self.btn_open_df.setIcon(QIcon(get_icon('run_arrow.png'))) self.btn_open_df.setToolTip('Open a DataFrame from your disk') self.btn_from_console = LoadFromConsoleButton(pd.DataFrame) self.btn_from_console.setToolTip('Show a DataFrame from the console') # The table to display the DataFrame self.table = DataFrameView(pd.DataFrame(), self) # format line edit self.format_editor = QLineEdit() self.format_editor.setText(self.table.model()._format) # format update button self.btn_change_format = QPushButton('Update') self.btn_change_format.setEnabled(False) # table clearing button self.btn_clear = QPushButton('Clear') self.btn_clear.setToolTip( 'Clear the table and disconnect from the DataFrame') # refresh button self.btn_refresh = QToolButton() self.btn_refresh.setIcon(QIcon(get_icon('refresh.png'))) self.btn_refresh.setToolTip('Refresh the table') # close button self.btn_close = QPushButton('Close') self.btn_close.setToolTip('Close this widget permanentely') # --------------------------------------------------------------------- # ------------------------ layout -------------------------------- # --------------------------------------------------------------------- vbox = QVBoxLayout() self.top_hbox = hbox = QHBoxLayout() hbox.addWidget(self.cb_index_editable) hbox.addWidget(self.cb_dtypes_changeable) hbox.addWidget(self.cb_enable_sort) hbox.addWidget(self.lbl_size) hbox.addStretch(0) hbox.addWidget(self.btn_open_df) hbox.addWidget(self.btn_from_console) vbox.addLayout(hbox) vbox.addWidget(self.table) self.bottom_hbox = hbox = QHBoxLayout() hbox.addWidget(self.format_editor) hbox.addWidget(self.btn_change_format) hbox.addStretch(0) hbox.addWidget(self.btn_clear) hbox.addWidget(self.btn_close) hbox.addWidget(self.btn_refresh) vbox.addLayout(hbox) self.setLayout(vbox) # --------------------------------------------------------------------- # ------------------------ Connections -------------------------------- # --------------------------------------------------------------------- self.cb_dtypes_changeable.stateChanged.connect( self.set_dtypes_changeable) self.cb_index_editable.stateChanged.connect(self.set_index_editable) self.btn_from_console.object_loaded.connect(self._open_ds_from_console) self.rows_inserted.connect(lambda i, n: self.set_lbl_size_text()) self.format_editor.textChanged.connect(self.toggle_fmt_button) self.btn_change_format.clicked.connect(self.update_format) self.btn_clear.clicked.connect(self.clear_table) self.btn_close.clicked.connect(self.clear_table) self.btn_close.clicked.connect(lambda: self.close()) self.btn_refresh.clicked.connect(self.table.reset_model) self.btn_open_df.clicked.connect(self._open_dataframe) self.table.set_index_action.triggered.connect( self.update_index_editable) self.table.append_index_action.triggered.connect( self.update_index_editable) self.cb_enable_sort.stateChanged.connect( self.table.setSortingEnabled) def update_index_editable(self): model = self.table.model() if len(model.df.index.names) > 1: model.index_editable = False self.cb_index_editable.setEnabled(False) self.cb_index_editable.setChecked(model.index_editable) def set_lbl_size_text(self, nrows=None, ncols=None): """Set the text of the :attr:`lbl_size` label to display the size""" model = self.table.model() nrows = nrows if nrows is not None else model.rowCount() ncols = ncols if ncols is not None else model.columnCount() if not nrows and not ncols: self.lbl_size.setText('') else: self.lbl_size.setText('Rows: %i, Columns: %i' % (nrows, ncols)) def clear_table(self): """Clear the table and emit the :attr:`cleared` signal""" df = pd.DataFrame() self.set_df(df, show=False) def _open_ds_from_console(self, oname, df): self.set_df(df) @docstrings.dedent def set_df(self, df, *args, **kwargs): """ Fill the table from a :class:`~pandas.DataFrame` Parameters ---------- %(DataFrameModel.parameters.no_parent)s show: bool If True (default), show and raise_ the editor """ show = kwargs.pop('show', True) self.table.set_df(df, *args, **kwargs) self.set_lbl_size_text(*df.shape) model = self.table.model() self.cb_dtypes_changeable.setChecked(model.dtypes_changeable) if len(model.df.index.names) > 1: model.index_editable = False self.cb_index_editable.setEnabled(False) else: self.cb_index_editable.setEnabled(True) self.cb_index_editable.setChecked(model.index_editable) self.cleared.emit() if show: self.show_plugin() self.dock.raise_() def set_index_editable(self, state): """Set the :attr:`DataFrameModel.index_editable` attribute""" self.table.model().index_editable = state == Qt.Checked def set_dtypes_changeable(self, state): """Set the :attr:`DataFrameModel.dtypes_changeable` attribute""" self.table.model().dtypes_changeable = state == Qt.Checked def toggle_fmt_button(self, text): try: text % 1.1 except (TypeError, ValueError): self.btn_change_format.setEnabled(False) else: self.btn_change_format.setEnabled( text.strip() != self.table.model()._format) def update_format(self): """Update the format of the table""" self.table.model().set_format(self.format_editor.text().strip()) def to_dock(self, main, *args, **kwargs): connect = self.dock is None super(DataFrameEditor, self).to_dock(main, *args, **kwargs) if connect: self.dock.toggleViewAction().triggered.connect(self.maybe_tabify) 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 _open_dataframe(self): self.open_dataframe() def open_dataframe(self, fname=None, *args, **kwargs): """Opens a file dialog and the dataset that has been inserted""" if fname is None: fname = QFileDialog.getOpenFileName( self, 'Open dataset', os.getcwd(), 'Comma separated files (*.csv);;' 'Excel files (*.xls *.xlsx);;' 'JSON files (*.json);;' 'All files (*)' ) if with_qt5: # the filter is passed as well fname = fname[0] if isinstance(fname, pd.DataFrame): self.set_df(fname) elif not fname: return else: ext = osp.splitext(fname)[1] open_funcs = { '.xls': pd.read_excel, '.xlsx': pd.read_excel, '.json': pd.read_json, '.tab': partial(pd.read_csv, delimiter='\t'), '.dat': partial(pd.read_csv, delim_whitespace=True), } open_func = open_funcs.get(ext, pd.read_csv) try: df = open_func(fname) except Exception: self.error_msg.showTraceback( '<b>Could not open DataFrame %s with %s</b>' % ( fname, open_func)) return self.set_df(df) def close(self, *args, **kwargs): if self.dock is not None: self.dock.close(*args, **kwargs) # removes the dock window del self.dock return super(DataFrameEditor, self).close(*args, **kwargs)
def add_item(self, what, get_artists, plot_func=None, remove_func=None, can_be_plotted=None): """Add a plot object to the table Parameters ---------- what: str The description of the plot object get_artists: function A function that takes no arguments and returns the artists plot_func: function, optional A function that takes no arguments and makes the plot. remove_func: function, optional A function that takes no arguments and removes the plot. can_be_plotted: function, optional A function that takes no argument and returns True if the plot can be made. """ def hide_or_show(checked): checked = checked is True or checked == Qt.Checked artist = None for artist in get_artists(): artist.set_visible(checked) if artist is not None: self.draw_figs(get_artists()) def trigger_plot_btn(): a = next(iter(get_artists()), None) if a is None: if can_be_plotted is None or can_be_plotted(): plot_func() cb.setChecked(True) btn.setIcon(QIcon(get_icon('invalid.png'))) btn.setToolTip('Remove ' + what) self.draw_figs(get_artists()) cb.setEnabled(True) else: fig = a.axes.figure figs = {a.axes.figure for a in get_artists()} remove_func() btn.setIcon(QIcon(get_icon('valid.png'))) btn.setToolTip('Show ' + what) for fig in figs: fig.canvas.draw_idle() cb.setEnabled(False) self.get_artists_funcs[what] = get_artists a = next(iter(get_artists()), None) cb = QCheckBox() cb.label = what self.hide_funcs[what] = hide_or_show cb.setChecked( Qt.Checked if a is not None and a.get_visible() else Qt.Unchecked) cb.stateChanged.connect(hide_or_show) row = self.rowCount() self.setRowCount(row + 1) self.setVerticalHeaderLabels(list(self.get_artists_funcs)) self.setCellWidget(row, 0, cb) if plot_func is not None: btn = QToolButton() btn.setIcon( QIcon(get_icon(('in' if a is None else '') + 'valid.png'))) btn.clicked.connect(trigger_plot_btn) btn.setEnabled(can_be_plotted is None or can_be_plotted()) btn.setToolTip(('Remove ' if a else 'Show ') + what) self.can_be_plotted_funcs[what] = can_be_plotted btn.label = what self.setCellWidget(row, 1, btn)
class ExportDfDialog(QDialog): """A QDialog to export a :class:`pandas.DataFrame` to Excel or CSV""" @docstrings.get_sectionsf('ExportDfDialog') def __init__(self, df, straditizer, fname=None, *args, **kwargs): """ Parameters ---------- df: pandas.DataFrame The DataFrame to be exported straditizer: straditize.straditizer.Straditizer The source straditizer fname: str The file name to export to """ super().__init__(*args, **kwargs) self.df = df self.stradi = straditizer self.txt_fname = QLineEdit() self.bt_open_file = QToolButton() self.bt_open_file.setIcon(QIcon(get_icon('run_arrow.png'))) self.bt_open_file.setToolTip('Select the export file on your drive') self.cb_include_meta = QCheckBox('Include meta data') self.cb_include_meta.setChecked(True) self.bbox = bbox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) # --------------------------------------------------------------------- # --------------------------- Layouts --------------------------------- # --------------------------------------------------------------------- vbox = QVBoxLayout() hbox = QHBoxLayout() hbox.addWidget(QLabel('Export to:')) hbox.addWidget(self.txt_fname) hbox.addWidget(self.bt_open_file) vbox.addLayout(hbox) vbox.addWidget(self.cb_include_meta) vbox.addWidget(bbox) self.setLayout(vbox) # --------------------------------------------------------------------- # --------------------------- Connections ----------------------------- # --------------------------------------------------------------------- bbox.accepted.connect(self._export) bbox.rejected.connect(self.reject) self.bt_open_file.clicked.connect(self.get_open_file_name) if fname is not None: self.txt_fname.setText(fname) self._export() def get_open_file_name(self): """Ask the user for a filename for saving the data frame""" def check_current(): dirname = osp.dirname(current) if osp.exists(dirname) and osp.isdir(dirname): return dirname current = self.txt_fname.text().strip() start = None if current: start = check_current() if start is None: for attr in 'project_file', 'image_file': try: current = self.stradi.get_attr(attr) except KeyError: pass else: start = check_current() if start is not None: break if start is None: start = os.getcwd() fname = QFileDialog.getSaveFileName( self, 'DataFrame file destination', start, 'Excel files (*.xlsx *.xls);;' 'csv files (*.csv);;' 'All files (*)') if with_qt5: # the filter is passed as well fname = fname[0] if not fname: return self.txt_fname.setText(fname) def _export(self): fname = self.txt_fname.text() ending = osp.splitext(fname)[1] self.stradi.set_attr('exported', str(dt.datetime.now())) meta = self.stradi.valid_attrs if ending in ['.xls', '.xlsx']: with pd.ExcelWriter(fname) as writer: self.df.to_excel(writer, 'Data') if self.cb_include_meta.isChecked() and len(meta): meta.to_excel(writer, 'Metadata', header=False) else: with open(fname, 'w') as f: if self.cb_include_meta.isChecked(): for t in meta.iloc[:, 0].items(): f.write('# %s: %s\n' % t) self.df.to_csv(fname, mode='a') self.accept() def cancel(self): del self.stradi, self.df super().cancel() def accept(self): del self.stradi, self.df super().accept() @classmethod @docstrings.dedent def export_df(cls, parent, df, straditizer, fname=None, exec_=True): """Open a dialog for exporting a DataFrame Parameters ---------- parent: QWidget The parent widget %(ExportDfDialog.parameters)s""" dialog = cls(df, straditizer, fname, parent=parent) if fname is None: available_width = QDesktopWidget().availableGeometry().width() / 3. width = dialog.sizeHint().width() height = dialog.sizeHint().height() # The plot creator window should cover at least one third of the # screen dialog.resize(max(available_width, width), height) if exec_: dialog.exec_() else: return dialog
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 DataFrameEditor(DockMixin, QWidget): """An editor for data frames""" dock_cls = DataFrameDock #: A signal that is emitted, if the table is cleared cleared = QtCore.pyqtSignal() #: A signal that is emitted when a cell has been changed. The argument #: is a tuple of two integers and one float: #: the row index, the column index and the new value cell_edited = QtCore.pyqtSignal(int, int, object, object) #: A signal that is emitted, if rows have been inserted into the dataframe. #: The first value is the integer of the (original) position of the row, #: the second one is the number of rows rows_inserted = QtCore.pyqtSignal(int, int) @property def hidden(self): return not self.table.filled def __init__(self, *args, **kwargs): super(DataFrameEditor, self).__init__(*args, **kwargs) self.error_msg = PyErrorMessage(self) # Label for displaying the DataFrame size self.lbl_size = QLabel() # A Checkbox for enabling and disabling the editability of the index self.cb_index_editable = QCheckBox('Index editable') # A checkbox for enabling and disabling the change of data types self.cb_dtypes_changeable = QCheckBox('Datatypes changeable') # A checkbox for enabling and disabling sorting self.cb_enable_sort = QCheckBox('Enable sorting') # A button to open a dataframe from the file self.btn_open_df = QToolButton(parent=self) self.btn_open_df.setIcon(QIcon(get_icon('run_arrow.png'))) self.btn_open_df.setToolTip('Open a DataFrame from your disk') self.btn_from_console = LoadFromConsoleButton(pd.DataFrame) self.btn_from_console.setToolTip('Show a DataFrame from the console') # The table to display the DataFrame self.table = DataFrameView(pd.DataFrame(), self) # format line edit self.format_editor = QLineEdit() self.format_editor.setText(self.table.model()._format) # format update button self.btn_change_format = QPushButton('Update') self.btn_change_format.setEnabled(False) # table clearing button self.btn_clear = QPushButton('Clear') self.btn_clear.setToolTip( 'Clear the table and disconnect from the DataFrame') # refresh button self.btn_refresh = QToolButton() self.btn_refresh.setIcon(QIcon(get_icon('refresh.png'))) self.btn_refresh.setToolTip('Refresh the table') # close button self.btn_close = QPushButton('Close') self.btn_close.setToolTip('Close this widget permanentely') # --------------------------------------------------------------------- # ------------------------ layout -------------------------------- # --------------------------------------------------------------------- vbox = QVBoxLayout() self.top_hbox = hbox = QHBoxLayout() hbox.addWidget(self.cb_index_editable) hbox.addWidget(self.cb_dtypes_changeable) hbox.addWidget(self.cb_enable_sort) hbox.addWidget(self.lbl_size) hbox.addStretch(0) hbox.addWidget(self.btn_open_df) hbox.addWidget(self.btn_from_console) vbox.addLayout(hbox) vbox.addWidget(self.table) self.bottom_hbox = hbox = QHBoxLayout() hbox.addWidget(self.format_editor) hbox.addWidget(self.btn_change_format) hbox.addStretch(0) hbox.addWidget(self.btn_clear) hbox.addWidget(self.btn_close) hbox.addWidget(self.btn_refresh) vbox.addLayout(hbox) self.setLayout(vbox) # --------------------------------------------------------------------- # ------------------------ Connections -------------------------------- # --------------------------------------------------------------------- self.cb_dtypes_changeable.stateChanged.connect( self.set_dtypes_changeable) self.cb_index_editable.stateChanged.connect(self.set_index_editable) self.btn_from_console.object_loaded.connect(self._open_ds_from_console) self.rows_inserted.connect(lambda i, n: self.set_lbl_size_text()) self.format_editor.textChanged.connect(self.toggle_fmt_button) self.btn_change_format.clicked.connect(self.update_format) self.btn_clear.clicked.connect(self.clear_table) self.btn_close.clicked.connect(self.clear_table) self.btn_close.clicked.connect(lambda: self.close()) self.btn_refresh.clicked.connect(self.table.reset_model) self.btn_open_df.clicked.connect(self._open_dataframe) self.table.set_index_action.triggered.connect( self.update_index_editable) self.table.append_index_action.triggered.connect( self.update_index_editable) self.cb_enable_sort.stateChanged.connect( self.table.setSortingEnabled) def update_index_editable(self): model = self.table.model() if len(model.df.index.names) > 1: model.index_editable = False self.cb_index_editable.setEnabled(False) self.cb_index_editable.setChecked(model.index_editable) def set_lbl_size_text(self, nrows=None, ncols=None): """Set the text of the :attr:`lbl_size` label to display the size""" model = self.table.model() nrows = nrows if nrows is not None else model.rowCount() ncols = ncols if ncols is not None else model.columnCount() if not nrows and not ncols: self.lbl_size.setText('') else: self.lbl_size.setText('Rows: %i, Columns: %i' % (nrows, ncols)) def clear_table(self): """Clear the table and emit the :attr:`cleared` signal""" df = pd.DataFrame() self.set_df(df, show=False) def _open_ds_from_console(self, oname, df): self.set_df(df) @docstrings.dedent def set_df(self, df, *args, **kwargs): """ Fill the table from a :class:`~pandas.DataFrame` Parameters ---------- %(DataFrameModel.parameters.no_parent)s show: bool If True (default), show and raise_ the editor """ show = kwargs.pop('show', True) self.table.set_df(df, *args, **kwargs) self.set_lbl_size_text(*df.shape) model = self.table.model() self.cb_dtypes_changeable.setChecked(model.dtypes_changeable) if len(model.df.index.names) > 1: model.index_editable = False self.cb_index_editable.setEnabled(False) else: self.cb_index_editable.setEnabled(True) self.cb_index_editable.setChecked(model.index_editable) self.cleared.emit() if show: self.show_plugin() self.dock.raise_() def set_index_editable(self, state): """Set the :attr:`DataFrameModel.index_editable` attribute""" self.table.model().index_editable = state == Qt.Checked def set_dtypes_changeable(self, state): """Set the :attr:`DataFrameModel.dtypes_changeable` attribute""" self.table.model().dtypes_changeable = state == Qt.Checked def toggle_fmt_button(self, text): try: text % 1.1 except (TypeError, ValueError): self.btn_change_format.setEnabled(False) else: self.btn_change_format.setEnabled( text.strip() != self.table.model()._format) def update_format(self): """Update the format of the table""" self.table.model().set_format(self.format_editor.text().strip()) def to_dock(self, main, *args, **kwargs): connect = self.dock is None super(DataFrameEditor, self).to_dock(main, *args, **kwargs) if connect: self.dock.toggleViewAction().triggered.connect(self.maybe_tabify) 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 _open_dataframe(self): self.open_dataframe() def open_dataframe(self, fname=None, *args, **kwargs): """Opens a file dialog and the dataset that has been inserted""" if fname is None: fname = QFileDialog.getOpenFileName( self, 'Open dataset', os.getcwd(), 'Comma separated files (*.csv);;' 'Excel files (*.xls *.xlsx);;' 'JSON files (*.json);;' 'All files (*)' ) if with_qt5: # the filter is passed as well fname = fname[0] if isinstance(fname, pd.DataFrame): self.set_df(fname) elif not fname: return else: ext = osp.splitext(fname)[1] open_funcs = { '.xls': pd.read_excel, '.xlsx': pd.read_excel, '.json': pd.read_json, '.tab': partial(pd.read_csv, delimiter='\t'), '.dat': partial(pd.read_csv, delim_whitespace=True), } open_func = open_funcs.get(ext, pd.read_csv) try: df = open_func(fname) except Exception: self.error_msg.showTraceback( '<b>Could not open DataFrame %s with %s</b>' % ( fname, open_func)) return self.set_df(df) def close(self, *args, **kwargs): if self.dock is not None: self.dock.close(*args, **kwargs) # removes the dock window del self.dock return super(DataFrameEditor, self).close(*args, **kwargs)
class UrlHelp(UrlBrowser, HelpMixin): """Class to convert rst docstrings to html and show browsers""" #: Object containing the necessary fields to describe an object given to #: the help widget. The descriptor is set up by the :meth:`describe_object` #: method and contains an additional objtype attribute object_descriptor = namedtuple( 'ObjectDescriptor', ['obj', 'name', 'objtype']) can_document_object = with_sphinx can_show_rst = with_sphinx #: menu button with different urls bt_url_menus = None #: sphinx_thread = None def __init__(self, *args, **kwargs): self._temp_dir = 'sphinx_dir' not in kwargs self.sphinx_dir = kwargs.pop('sphinx_dir', mkdtemp(prefix='psyplot_')) self.build_dir = osp.join(self.sphinx_dir, '_build', 'html') super(UrlHelp, self).__init__(*args, **kwargs) self.error_msg = PyErrorMessage(self) if with_sphinx: self.sphinx_thread = SphinxThread(self.sphinx_dir) self.sphinx_thread.html_ready[str].connect(self.browse) self.sphinx_thread.html_error[str].connect( self.error_msg.showTraceback) self.sphinx_thread.html_error[str].connect(logger.debug) rcParams.connect('help_explorer.render_docs_parallel', self.reset_sphinx) rcParams.connect('help_explorer.use_intersphinx', self.reset_sphinx) rcParams.connect('help_explorer.online', self.reset_sphinx) self.bt_connect_console = QToolButton(self) self.bt_connect_console.setCheckable(True) if rcParams['console.connect_to_help']: self.bt_connect_console.setIcon(QIcon(get_icon( 'ipython_console.png'))) self.bt_connect_console.click() else: self.bt_connect_console.setIcon(QIcon(get_icon( 'ipython_console_t.png'))) self.bt_connect_console.clicked.connect(self.toogle_connect_console) rcParams.connect('console.connect_to_help', self.update_connect_console) self.toogle_connect_console() # menu button with different urls self.bt_url_menus = QToolButton(self) self.bt_url_menus.setIcon(QIcon(get_icon('docu_button.png'))) self.bt_url_menus.setToolTip('Browse documentations') self.bt_url_menus.setPopupMode(QToolButton.InstantPopup) docu_menu = QMenu(self) for name, url in six.iteritems(self.doc_urls): def to_url(b, url=url): self.browse(url) action = QAction(name, self) action.triggered.connect(to_url) docu_menu.addAction(action) self.bt_url_menus.setMenu(docu_menu) self.button_box.addWidget(self.bt_connect_console) self.button_box.addWidget(self.bt_url_menus) # toogle the lock again to set the bt_url_menus enabled state self.toogle_url_lock() def update_connect_console(self, connect): if (connect and not self.bt_connect_console.isChecked() or not connect and self.bt_connect_console.isChecked()): self.bt_connect_console.click() def toogle_connect_console(self): """Disable (or enable) the loading of web pages in www""" bt = self.bt_connect_console connect = bt.isChecked() bt.setIcon(QIcon(get_icon( 'ipython_console.png' if connect else 'ipython_console_t.png'))) bt.setToolTip("%sonnect the console to the help explorer" % ( "Don't c" if connect else "C")) if rcParams['console.connect_to_help'] is not connect: rcParams['console.connect_to_help'] = connect def reset_sphinx(self, value): """Method that is called if the configuration changes""" if with_sphinx and hasattr(self.sphinx_thread, 'app'): del self.sphinx_thread.app @docstrings.dedent def show_help(self, obj, oname='', files=None): """ Render the rst docu for the given object with sphinx and show it Parameters ---------- %(HelpMixin.show_help.parameters)s """ if self.bt_lock.isChecked(): return return super(UrlHelp, self).show_help(obj, oname=oname, files=files) @docstrings.dedent def show_intro(self, text=''): """ Show the intro text in the explorer Parameters ---------- %(HelpMixin.show_intro.parameters)s""" if self.sphinx_thread is not None: with open(self.sphinx_thread.index_file, 'a') as f: f.write('\n' + text.strip() + '\n\n' + 'Table of Contents\n' '=================\n\n.. toctree::\n') self.sphinx_thread.render(None, None) def show_rst(self, text, oname='', descriptor=None, files=None): """Render restructured text with sphinx and show it Parameters ---------- %(HelpMixin.show_rst.parameters)s""" if self.bt_lock.isChecked() or self.sphinx_thread is None: return False if not oname and descriptor: oname = descriptor.name for f in files or []: shutil.copyfile(f, osp.join(self.sphinx_dir, osp.basename(f))) self.sphinx_thread.render(text, oname) return True def describe_object(self, obj, oname=''): """Describe an object using additionaly the object type from the :meth:`get_objtype` method Returns ------- instance of :attr:`object_descriptor` The descriptor of the object""" return self.object_descriptor(obj, oname, self.get_objtype(obj)) def browse(self, url): """Reimplemented to add file paths to the url string""" url = asstring(url) html_file = osp.join(self.sphinx_dir, '_build', 'html', url + '.html') if osp.exists(html_file): url = file2html(html_file) super(UrlHelp, self).browse(url) def toogle_url_lock(self): """Disable (or enable) the loading of web pages in www""" super(UrlHelp, self).toogle_url_lock() # enable or disable documentation button bt = self.bt_url_lock offline = bt.isChecked() try: self.bt_url_menus.setEnabled(not offline) except AttributeError: # not yet initialized pass def url_changed(self, url): """Reimplemented to remove file paths from the url string""" try: url = asstring(url.toString()) except AttributeError: pass if url.startswith('file://'): fname = html2file(url) if osp.samefile(self.build_dir, osp.commonprefix([ fname, self.build_dir])): url = osp.splitext(osp.basename(fname))[0] super(UrlHelp, self).url_changed(url) def header(self, descriptor, sig): return '%(name)s\n%(bars)s\n\n.. py:%(type)s:: %(name)s%(sig)s\n' % { 'name': descriptor.name, 'bars': '-' * len(descriptor.name), 'type': descriptor.objtype, 'sig': sig} def get_objtype(self, obj): """Get the object type of the given object and determine wheter the object is considered a class, a module, a function, method or data Parameters ---------- obj: object Returns ------- str One out of {'class', 'module', 'function', 'method', 'data'}""" if inspect.isclass(obj): return 'class' if inspect.ismodule(obj): return 'module' if inspect.isfunction(obj) or isinstance(obj, type(all)): return 'function' if inspect.ismethod(obj) or isinstance(obj, type(str.upper)): return 'method' return 'data' def is_importable(self, modname): """Determine whether members of the given module can be documented with sphinx by using the :func:`sphinx.util.get_module_source` function Parameters ---------- modname: str The __name__ attribute of the module to import Returns ------- bool True if sphinx can import the module""" try: get_module_source(modname) return True except Exception: return False def get_doc(self, descriptor): """Reimplemented to (potentially) use the features from sphinx.ext.autodoc""" obj = descriptor.obj if inspect.ismodule(obj): module = obj else: module = inspect.getmodule(obj) if module is not None and (re.match('__.*__', module.__name__) or not self.is_importable(module.__name__)): module = None isclass = inspect.isclass(obj) # If the module is available, we try to use autodoc if module is not None: doc = '.. currentmodule:: ' + module.__name__ + '\n\n' # a module --> use automodule if inspect.ismodule(obj): doc += self.header(descriptor, '') doc += '.. automodule:: ' + obj.__name__ # an importable class --> use autoclass elif isclass and getattr(module, obj.__name__, None) is not None: doc += self.header(descriptor, '') doc += '.. autoclass:: ' + obj.__name__ # an instance and the class can be imported # --> use super get_doc and autoclass for the tyoe elif descriptor.objtype == 'data' and getattr( module, type(obj).__name__, None) is not None: doc += '\n\n'.join([ super(UrlHelp, self).get_doc(descriptor), "Class docstring\n===============", '.. autoclass:: ' + type(obj).__name__]) # an instance --> use super get_doc for instance and the type elif descriptor.objtype == 'data': cls_doc = super(UrlHelp, self).get_doc(self.describe_object( type(obj), type(obj).__name__)) doc += '\n\n'.join([ super(UrlHelp, self).get_doc(descriptor), "Class docstring\n===============", cls_doc]) # a function or method --> use super get_doc else: doc += super(UrlHelp, self).get_doc(descriptor) # otherwise the object has been defined in this session else: # an instance --> use super get_doc for instance and the type if descriptor.objtype == 'data': cls_doc = super(UrlHelp, self).get_doc(self.describe_object( type(obj), type(obj).__name__)) doc = '\n\n'.join([ super(UrlHelp, self).get_doc(descriptor), "Class docstring\n===============", cls_doc]) # a function or method --> use super get_doc else: doc = super(UrlHelp, self).get_doc(descriptor) return doc.rstrip() + '\n' def process_docstring(self, lines, descriptor): """Process the lines with the napoleon sphinx extension""" lines = list(chain(*(l.splitlines() for l in lines))) lines = NumpyDocstring( lines, what=descriptor.objtype, name=descriptor.name, obj=descriptor.obj).lines() lines = GoogleDocstring( lines, what=descriptor.objtype, name=descriptor.name, obj=descriptor.obj).lines() return indent(super(UrlHelp, self).process_docstring( lines, descriptor)) def close(self, *args, **kwargs): if self.sphinx_thread is not None: try: del self.sphinx_thread.app except AttributeError: pass shutil.rmtree(self.build_dir, ignore_errors=True) if self._temp_dir: shutil.rmtree(self.sphinx_dir, ignore_errors=True) del self.sphinx_thread return super(UrlHelp, self).close(*args, **kwargs)
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 UrlHelp(UrlBrowser, HelpMixin): """Class to convert rst docstrings to html and show browsers""" #: Object containing the necessary fields to describe an object given to #: the help widget. The descriptor is set up by the :meth:`describe_object` #: method and contains an additional objtype attribute object_descriptor = namedtuple( 'ObjectDescriptor', ['obj', 'name', 'objtype']) can_document_object = with_sphinx can_show_rst = with_sphinx def __init__(self, *args, **kwargs): self.sphinx_dir = kwargs.pop('sphinx_dir', mkdtemp()) self.build_dir = osp.join(self.sphinx_dir, '_build', 'html') super(UrlHelp, self).__init__(*args, **kwargs) self.error_msg = PyErrorMessage(self) if with_sphinx: self.sphinx_thread = SphinxThread(self.sphinx_dir) self.sphinx_thread.html_ready[str].connect(self.browse) self.sphinx_thread.html_error[str].connect( self.error_msg.showTraceback) self.sphinx_thread.html_error[str].connect(logger.debug) else: self.sphinx_thread = None #: menu button with different urls self.bt_url_menus = QToolButton(self) self.bt_url_menus.setIcon(QIcon(get_icon('docu_button.png'))) self.bt_url_menus.setToolTip('Browse documentations') self.bt_url_menus.setPopupMode(QToolButton.InstantPopup) docu_menu = QMenu(self) for name, url in six.iteritems(self.doc_urls): def to_url(b, url=url): self.browse(url) action = QAction(name, self) action.triggered.connect(to_url) docu_menu.addAction(action) self.bt_url_menus.setMenu(docu_menu) self.button_box.addWidget(self.bt_url_menus) @docstrings.dedent def show_intro(self, text=''): """ Show the intro text in the explorer Parameters ---------- %(HelpMixin.show_intro.parameters)s""" if self.sphinx_thread is not None: with open(self.sphinx_thread.index_file, 'a') as f: f.write(text) self.sphinx_thread.render(None, None) def show_rst(self, text, oname='', descriptor=None): """Render restructured text with sphinx and show it Parameters ---------- %(HelpMixin.show_rst.parameters)s""" if not oname and descriptor: oname = descriptor.name self.sphinx_thread.render(text, oname) def describe_object(self, obj, oname=''): """Describe an object using additionaly the object type from the :meth:`get_objtype` method Returns ------- instance of :attr:`object_descriptor` The descriptor of the object""" return self.object_descriptor(obj, oname, self.get_objtype(obj)) def browse(self, url): """Reimplemented to add file paths to the url string""" html_file = osp.join(self.sphinx_dir, '_build', 'html', url + '.html') if osp.exists(html_file): url = 'file://' + html_file super(UrlHelp, self).browse(url) def url_changed(self, url): """Reimplemented to remove file paths from the url string""" try: url = url.toString() except AttributeError: pass if url.startswith('file://'): fname = url[7:] if osp.samefile(self.build_dir, osp.commonprefix([ fname, self.build_dir])): url = osp.splitext(osp.basename(fname))[0] super(UrlHelp, self).url_changed(url) def header(self, descriptor, sig): return '%(name)s\n%(bars)s\n\n.. py:%(type)s:: %(name)s%(sig)s\n' % { 'name': descriptor.name, 'bars': '-' * len(descriptor.name), 'type': descriptor.objtype, 'sig': sig} def get_objtype(self, obj): """Get the object type of the given object and determine wheter the object is considered a class, a module, a function, method or data Parameters ---------- obj: object Returns ------- str One out of {'class', 'module', 'function', 'method', 'data'}""" if inspect.isclass(obj): return 'class' if inspect.ismodule(obj): return 'module' if inspect.isfunction(obj) or isinstance(obj, type(all)): return 'function' if inspect.ismethod(obj) or isinstance(obj, type(str.upper)): return 'method' return 'data' def is_importable(self, modname): """Determine whether members of the given module can be documented with sphinx by using the :func:`sphinx.util.get_module_source` function Parameters ---------- modname: str The __name__ attribute of the module to import Returns ------- bool True if sphinx can import the module""" try: get_module_source(modname) return True except: return False def get_doc(self, descriptor): """Reimplemented to (potentially) use the features from sphinx.ext.autodoc""" obj = descriptor.obj if inspect.ismodule(obj): module = obj else: module = inspect.getmodule(obj) if module is not None and (re.match('__.*__', module.__name__) or not self.is_importable(module.__name__)): module = None isclass = inspect.isclass(obj) # If the module is available, we try to use autodoc if module is not None: doc = '.. currentmodule:: ' + module.__name__ + '\n\n' # a module --> use automodule if inspect.ismodule(obj): doc += self.header(descriptor, '') doc += '.. automodule:: ' + obj.__name__ # an importable class --> use autoclass elif isclass and getattr(module, obj.__name__, None) is not None: doc += self.header(descriptor, '') doc += '.. autoclass:: ' + obj.__name__ # an instance and the class can be imported # --> use super get_doc and autoclass for the tyoe elif descriptor.objtype == 'data' and getattr( module, type(obj).__name__, None) is not None: doc += '\n\n'.join([ super(UrlHelp, self).get_doc(descriptor), "Class docstring\n===============", '.. autoclass:: ' + type(obj).__name__]) # an instance --> use super get_doc for instance and the type elif descriptor.objtype == 'data': cls_doc = super(UrlHelp, self).get_doc(self.describe_object( type(obj), type(obj).__name__)) doc += '\n\n'.join([ super(UrlHelp, self).get_doc(descriptor), "Class docstring\n===============", cls_doc]) # a function or method --> use super get_doc else: doc += super(UrlHelp, self).get_doc(descriptor) # otherwise the object has been defined in this session else: # an instance --> use super get_doc for instance and the type if descriptor.objtype == 'data': cls_doc = super(UrlHelp, self).get_doc(self.describe_object( type(obj), type(obj).__name__)) doc = '\n\n'.join([ super(UrlHelp, self).get_doc(descriptor), "Class docstring\n===============", cls_doc]) # a function or method --> use super get_doc else: doc = super(UrlHelp, self).get_doc(descriptor) return doc.rstrip() + '\n' def process_docstring(self, lines, descriptor): """Process the lines with the napoleon sphinx extension""" lines = list(chain(*(l.splitlines() for l in lines))) lines = NumpyDocstring( lines, what=descriptor.objtype, name=descriptor.name, obj=descriptor.obj).lines() lines = GoogleDocstring( lines, what=descriptor.objtype, name=descriptor.name, obj=descriptor.obj).lines() return indent(super(UrlHelp, self).process_docstring( lines, descriptor))
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)}) 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) 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).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())
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())