def test_drag_drop(self): """Test the drag and drop of the :class:`psyplot_gui.plot_creator.ArrayTable`""" self.pc.show() # XXX Try to use directly the dropEvent method by setting the source of # the event! point = QtCore.QPoint(0, 0) data = QtCore.QMimeData() event = QtGui.QDropEvent(point, Qt.MoveAction, data, Qt.LeftButton, Qt.NoModifier, QtCore.QEvent.Drop) # open dataset fname = self.get_file('test-t2m-u-v.nc') ds = psy.open_dataset(fname) self.pc.bt_get_ds.get_from_shell(ds) # add data arrays QTest.mouseClick(self.pc.bt_add_all, Qt.LeftButton) # move rows atab = self.pc.array_table old = list(atab.arr_names_dict.items()) atab.selectRow(2) atab.dropOn(event) resorted = [old[i] for i in [2, 0, 1] + list(range(3, len(old)))] self.assertEqual(list(atab.arr_names_dict.items()), resorted, msg="Rows not moved correctly!") ds.close()
def fetch_more(self, rows=False, columns=False): if self.can_fetch_more(rows=rows): reminder = self.total_rows - self.rows_loaded items_to_fetch = min(reminder, self.ROWS_TO_LOAD) self.beginInsertRows(QtCore.QModelIndex(), self.rows_loaded, self.rows_loaded + items_to_fetch - 1) self.rows_loaded += items_to_fetch self.endInsertRows() if self.can_fetch_more(columns=columns): reminder = self.total_cols - self.cols_loaded items_to_fetch = min(reminder, self.COLS_TO_LOAD) self.beginInsertColumns(QtCore.QModelIndex(), self.cols_loaded, self.cols_loaded + items_to_fetch - 1) self.cols_loaded += items_to_fetch self.endInsertColumns()
def __init__(self, names=[], N=10, *args, **kwargs): """ Parameters ---------- %(ColormapModel.parameters)s Other Parameters ---------------- ``*args, **kwargs`` Anything else that is passed to the ColormapDialog """ super(QDialog, self).__init__(*args, **kwargs) vbox = QVBoxLayout() self.table = ColormapTable(names=names, N=N) vbox.addWidget(self.table) self.setLayout(vbox) col_width = self.table.columnWidth(0) header_width = self.table.verticalHeader().width() row_height = self.table.rowHeight(0) available = QDesktopWidget().availableGeometry() height = int(min(row_height * (self.table.rowCount() + 1), 2. * available.height() / 3.)) width = int(min(header_width + col_width * N + 0.5 * col_width, 2. * available.width() / 3.)) self.resize(QtCore.QSize(width, height))
def load_new_marks(self, mark): new = [mark.y, mark] self.marks.append(new) self.sort_marks() idx = self.marks.index(new) self.beginInsertRows(QtCore.QModelIndex(), idx, idx) self.endInsertRows() mark.moved.connect(self.update_after_move) self.update_lines()
def columnCount(self, index=QtCore.QModelIndex()): """DataFrame column number""" # This is done to implement series if len(self.df.shape) == 1: return 2 elif self.total_cols <= self.cols_loaded: return self.total_cols + 1 else: return self.cols_loaded + 1
def __init__(self, valid, sep=',', *args, **kwargs): """ Parameters ---------- valid: list of str The possible choices sep: str, optional The separation pattern ``*args,**kwargs`` Determined by PyQt5.QtGui.QValidator """ patt = QtCore.QRegExp('^((%s)(;;)?)+$' % '|'.join(valid)) super(QRegExpValidator, self).__init__(patt, *args, **kwargs)
class EnableButton(QPushButton): """A `QPushButton` that emits a signal when enabled""" #: A signal that is emitted with a boolean whether if the button is #: enabled or disabled enabled = QtCore.pyqtSignal(bool) def setEnabled(self, b): """Reimplemented to emit the :attr:`enabled` signal""" if b is self.isEnabled(): return super(EnableButton, self).setEnabled(b) self.enabled.emit(b)
def delRow(self, irow): mark = self.marks[irow][1] try: mark.moved.disconnect(self.update_after_move) except ValueError: pass try: self._remove_mark(mark) except ValueError: pass del self.marks[irow] self.beginRemoveRows(QtCore.QModelIndex(), irow, irow) self.endRemoveRows() self.update_lines()
class ConfigPage(object): """An abstract base class for configuration pages""" #: A signal that shall be emitted if the validation state changes validChanged = QtCore.pyqtSignal(bool) #: A signal that is emitted if changes are propsed. The signal should be #: emitted with the instance of the page itself propose_changes = QtCore.pyqtSignal(object) #: The title for the config page title = None #: The icon of the page icon = None #: :class:`bool` that is True, if the changes in this ConfigPage are set #: immediately auto_updates = False @property def is_valid(self): """Check whether the page is valid""" raise NotImplementedError @property def changed(self): """Check whether the preferences will change""" raise NotImplementedError def initialize(self): """Initialize the page""" raise NotImplementedError def apply_changes(self): """Apply the planned changes""" raise NotImplementedError
def remove_mark(self, mark): found = False for i, (y, m) in enumerate(self.marks): if m is mark: found = True break if found: try: mark.moved.disconnect(self.update_after_move) except ValueError: pass del self.marks[i] self.beginRemoveRows(QtCore.QModelIndex(), i, i) self.endRemoveRows() self.update_lines()
def insertRows(self, irow, nrows=1): """Insert a row into the :attr:`df` Parameters ---------- irow: int The row index. If `irow` is equal to the length of the :attr:`df`, the rows will be appended. nrows: int The number of rows to insert""" df = self.df if not irow: if not len(df): idx = 0 else: idx = df.index.values[0] else: try: idx = df.index.values[irow-1:irow+1].mean() except TypeError: idx = df.index.values[min(irow, len(df) - 1)] else: idx = df.index.values[min(irow, len(df) - 1)].__class__(idx) # reset the index to sort it correctly idx_name = df.index.name dtype = df.index.dtype df.reset_index(inplace=True) new_idx_name = df.columns[0] current_len = len(df) for i in range(nrows): df.loc[current_len + i, new_idx_name] = idx df[new_idx_name] = df[new_idx_name].astype(dtype) if irow < current_len: changed = df.index.values.astype(float) changed[current_len:] = irow - 0.5 df.index = changed df.sort_index(inplace=True) df.set_index(new_idx_name, inplace=True, drop=True) df.index.name = idx_name self.update_df_index() self.beginInsertRows(QtCore.QModelIndex(), self.rows_loaded, self.rows_loaded + nrows - 1) self.total_rows += nrows self.rows_loaded += nrows self.endInsertRows() self._parent.rows_inserted.emit(irow, nrows)
def __init__(self, versions, *args, **kwargs): """ Parameters ---------- %(DependenciesTree.parameters)s """ super(DependenciesDialog, self).__init__(*args, **kwargs) self.setWindowTitle('Dependencies') self.versions = versions self.vbox = layout = QVBoxLayout() self.label = QLabel(""" psyplot and the plugins depend on several python libraries. The tree widget below lists the versions of the plugins and the requirements. You can select the items in the tree and copy them to clipboard.""", parent=self) layout.addWidget(self.label) self.tree = DependenciesTree(versions, parent=self) self.tree.setSelectionMode(QAbstractItemView.MultiSelection) layout.addWidget(self.tree) # copy button self.bt_copy = QPushButton('Copy selection to clipboard') self.bt_copy.setToolTip( 'Copy the selected packages in the above table to the clipboard.') self.bt_copy.clicked.connect(lambda: self.copy_selected()) self.bbox = QDialogButtonBox(QDialogButtonBox.Ok) self.bbox.accepted.connect(self.accept) hbox = QHBoxLayout() hbox.addWidget(self.bt_copy) hbox.addStretch(1) hbox.addWidget(self.bbox) layout.addLayout(hbox) #: A label for simple status update self.info_label = QLabel('', self) layout.addWidget(self.info_label) self.timer = QtCore.QTimer(self) self.timer.timeout.connect(self.clear_label) self.setLayout(layout)
def load_new_marks(self, mark): """Add a new mark into the table after they have been added by the user Parameters ---------- mark: straditize.cross_mark.CrossMarks The added mark""" self._new_marks.append(mark) mark.moved.connect(self.update_after_move) if len(self._new_marks) == self.columnCount() - 1: new = (self._new_marks[0].y, self._new_marks) self.marks.append(new) self._new_marks = [] self.sort_marks() idx = self.marks.index(new) self.beginInsertRows(QtCore.QModelIndex(), idx, idx) self.endInsertRows() self.update_lines()
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 remove_mark(self, mark): """Remove a mark from the table after it has been removed by the user Parameters ---------- mark: straditize.cross_mark.CrossMarks The removed mark""" found = False for i, (y, marks) in enumerate(self.marks): if mark in marks: found = True break if found: for m in self.marks[i][1]: try: m.moved.disconnect(self.update_after_move) except ValueError: pass del self.marks[i] self.beginRemoveRows(QtCore.QModelIndex(), i, i) self.endRemoveRows() self.update_lines()
def insertRow(self, irow, xa=None, ya=None): """Insert a row into the table Parameters ---------- irow: int The row index. If `irow` is equal to the length of the :attr:`marks`, the rows will be appended""" if xa is None or ya is None: mark = self.marks[min(irow, len(self.marks) - 1)][1] new = self._new_mark(mark.xa[0], mark.ya[0])[0] else: new = self._new_mark(xa + self._bounds[:, 0], ya + self._y0)[0] new.set_pos((xa + self._bounds[:, 0], ya + self._y0)) y = new.y new.moved.connect(self.update_after_move) if irow == len(self.marks): self.marks.append((y, new)) else: self.marks.insert(irow, (y, new)) self.beginInsertRows(QtCore.QModelIndex(), irow, irow) self.endInsertRows() self.update_lines()
def add_page(self, widget): """Add a new page to the preferences dialog Parameters ---------- widget: ConfigPage The page to add""" widget.validChanged.connect(self.bt_apply.setEnabled) widget.validChanged.connect( self.bbox.button(QDialogButtonBox.Ok).setEnabled) scrollarea = QScrollArea(self) scrollarea.setWidgetResizable(True) scrollarea.setWidget(widget) self.pages_widget.addWidget(scrollarea) item = QListWidgetItem(self.contents_widget) try: item.setIcon(widget.icon) except TypeError: pass item.setText(widget.title) item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) item.setSizeHint(QtCore.QSize(0, 25)) widget.propose_changes.connect(self.check_changes)
class SphinxThread(QtCore.QThread): """A thread to render sphinx documentation in a separate process""" #: A signal to be emitted when the rendering finished. The url is the #: file location html_ready = QtCore.pyqtSignal(str) html_error = QtCore.pyqtSignal(str) def __init__(self, outdir, html_text_no_doc=''): super(SphinxThread, self).__init__() self.doc = None self.name = None self.html_text_no_doc = html_text_no_doc self.outdir = outdir self.index_file = osp.join(self.outdir, 'psyplot.rst') self.confdir = osp.join(get_module_path(__name__), 'sphinx_supp') shutil.copyfile(osp.join(self.confdir, 'psyplot.rst'), osp.join(self.outdir, 'psyplot.rst')) self.build_dir = osp.join(self.outdir, '_build', 'html') def render(self, doc, name): """Render the given rst string and save the file as ``name + '.rst'`` Parameters ---------- doc: str The rst docstring name: str the name to use for the file""" if self.wait(): self.doc = doc self.name = name # start rendering in separate process if rcParams['help_explorer.render_docs_parallel']: self.start() else: self.run() def run(self): """Create the html file. When called the first time, it may take a while because the :class:`sphinx.application.Sphinx` app is build, potentially with intersphinx When finished, the html_ready signal is emitted""" if not hasattr(self, 'app'): from IPython.core.history import HistoryAccessor # to avoid history access conflicts between different threads, # we disable the ipython history HistoryAccessor.enabled.default_value = False self.app = Sphinx(self.outdir, self.confdir, self.build_dir, osp.join(self.outdir, '_build', 'doctrees'), 'html', status=StreamToLogger(logger, logging.DEBUG), warning=StreamToLogger(logger, logging.DEBUG)) if self.name is not None: docfile = osp.abspath(osp.join(self.outdir, self.name + '.rst')) if docfile == self.index_file: self.name += '1' docfile = osp.abspath(osp.join(self.outdir, self.name + '.rst')) html_file = osp.abspath( osp.join(self.outdir, '_build', 'html', self.name + '.html')) if not osp.exists(docfile): with open(self.index_file, 'a') as f: f.write('\n ' + self.name) with open(docfile, 'w') as f: f.write(self.doc) else: html_file = osp.abspath( osp.join(self.outdir, '_build', 'html', 'psyplot.html')) try: self.app.build(None, []) except Exception: msg = 'Error while building sphinx document %s' % (self.name) self.html_error.emit('<b>' + msg + '</b>') logger.debug(msg) else: self.html_ready.emit(file2html(html_file))
class PlotterList(QListWidget): """QListWidget showing multiple ArrayItems of one Plotter class""" #: str. The name of the attribute of the :class:`psyplot.project.Project` #: class project_attribute = None #: boolean. True if the current project does not contain any arrays in the #: attribute identified by the :attr:`project_attribute` is_empty = True _no_project_update = _TempBool() updated_from_project = QtCore.pyqtSignal(QListWidget) # Determine whether the plotter could be loaded can_import_plotter = True @property def arrays(self): """List of The InteractiveBase instances in this list""" return ArrayList([ getattr(item.arr(), 'arr', item.arr()) for item in self.array_items ]) @property def array_items(self): """Iterable of :class:`ArrayItem` items in this list""" return filter(lambda i: i is not None, map(self.item, range(self.count()))) def __init__(self, plotter_type=None, *args, **kwargs): """ Parameters ---------- plotter_type: str or None If str, it mus be an attribute name of the :class:`psyplot.project.Project` class. Otherwise the full project is used ``*args,**kwargs`` Are determined by the parent class Notes ----- When initialized, the content of the list is determined by ``gcp(True)`` and ``gcp()``""" super(PlotterList, self).__init__(*args, **kwargs) self.project_attribute = plotter_type self.setSelectionMode(QAbstractItemView.MultiSelection) self.itemSelectionChanged.connect(self.update_cp) self.update_from_project(gcp(True)) self.update_from_project(gcp()) def update_from_project(self, project): """Update the content from the given Project Parameters ---------- project: psyplot.project.Project If the project is a main project, new items will be added. Otherwise only the current selection changes""" if self._no_project_update: return if not self.can_import_plotter: # remove the current items self.disconnect_items() return attr = self.project_attribute # stop if the module of the plotter has not yet been imported if attr and Project._registered_plotters[attr][0] not in sys.modules: return try: arrays = project if not attr else getattr(project, attr) mp = gcp(True) if project is None else project.main main_arrays = mp if not attr else getattr(mp, attr) except ImportError: # plotter could not be loaded self.is_empty = True self.can_import_plotter = False return self.is_empty = not bool(main_arrays) with self._no_project_update: if project is None: for item in self.array_items: item.setSelected(False) elif project.is_main: old_arrays = self.arrays # remove outdated items i = 0 for arr in old_arrays: if arr not in arrays: item = self.takeItem(i) item.disconnect_from_array() else: i += 1 # add new items for arr in arrays: if arr not in old_arrays: item = ArrayItem(weakref.ref(arr.psy), parent=self) self.addItem(item) # resort to match the project for arr in reversed(main_arrays): for i, item in enumerate(self.array_items): if item.arr() is arr.psy: self.insertItem(0, self.takeItem(i)) cp = gcp() for item in self.array_items: item.setSelected(getattr(item.arr(), 'arr', item.arr()) in cp) self.updated_from_project.emit(self) def update_cp(self, *args, **kwargs): """Update the current project from what is selected in this list""" if not self._no_project_update: mp = gcp(True) sp = gcp() selected = [item.arr().arr_name for item in self.selectedItems()] arrays = self.arrays other_selected = [ arr.psy.arr_name for arr in sp if arr not in arrays ] with self._no_project_update: scp(mp(arr_name=selected + other_selected)) def disconnect_items(self): """Disconnect the items in this list from the arrays""" for item in list(self.array_items): item.disconnect_from_array() self.takeItem(self.indexFromItem(item).row()) self.is_empty = True
def rowCount(self, index=QtCore.QModelIndex()): """DataFrame row number""" if self.total_rows <= self.rows_loaded: return self.total_rows else: return self.rows_loaded
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 columnCount(self, index=QtCore.QModelIndex()): return len(self._column_names) + 1
def columnCount(self, index=QtCore.QModelIndex()): """The number of rows in the table""" return len(self.axes) + 1
def rowCount(self, index=QtCore.QModelIndex()): """The number of rows in the table""" return len(self.marks)
class LoadFromConsoleButton(QToolButton): """A toolbutton to load an object from the console""" #: The signal that is emitted when an object has been loaded. The first #: argument is the object name, the second the object itself object_loaded = QtCore.pyqtSignal(str, object) @property def instances2check_str(self): return ', '.join('%s.%s' % (cls.__module__, cls.__name__) for cls in self._instances2check) @property def potential_object_names(self): from ipykernel.inprocess.ipkernel import InProcessInteractiveShell shell = InProcessInteractiveShell.instance() return sorted(name for name, obj in shell.user_global_ns.items() if not name.startswith('_') and self.check(obj)) def __init__(self, instances=None, *args, **kwargs): """ Parameters ---------- instances: class or tuple of classes The classes that should be used for an instance check """ super(LoadFromConsoleButton, self).__init__(*args, **kwargs) self.setIcon(QIcon(get_icon('console-go.png'))) if instances is not None and inspect.isclass(instances): instances = (instances, ) self._instances2check = instances self.error_msg = PyErrorMessage(self) self.clicked.connect(partial(self.get_from_shell, None)) def check(self, obj): return True if not self._instances2check else isinstance( obj, self._instances2check) def get_from_shell(self, oname=None): """Open an input dialog, receive an object and emit the :attr:`object_loaded` signal""" if oname is None: oname, ok = QInputDialog.getItem( self, 'Select variable', 'Select a variable to import from the console', self.potential_object_names) if not ok: return if self.check(oname) and (self._instances2check or not isinstance(oname, six.string_types)): obj = oname oname = 'object' else: found, obj = self.get_obj(oname.strip()) if found: if not self.check(obj): self.error_msg.showMessage( 'Object must be an instance of %r, not %r' % (self.instances2check_str, '%s.%s' % (type(obj).__module__, type(obj).__name__))) return else: if not oname.strip(): msg = 'The variable name must not be empty!' else: msg = 'Could not find object ' + oname self.error_msg.showMessage(msg) return self.object_loaded.emit(oname, obj) def get_obj(self, oname): """Load an object from the current shell""" from psyplot_gui.main import mainwindow return mainwindow.console.get_obj(oname)
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)
def sizeHint(self): """Reimplemented to use the rowHeight as height""" s = super(ColorLabel, self).sizeHint() return QtCore.QSize(s.width(), self.rowHeight(0) * self.rowCount())
def sizeHint(self): header = self.horizontalHeader().sizeHint().height() s = super(PlotControlTable, self).sizeHint() return QtCore.QSize(s.width(), self.rowHeight(0) * self.rowCount() + header)
class ColorLabel(QTableWidget): """A QTableWidget with one cell and no headers to just display a color""" #: a signal that is emitted with an rgba color if the chosen color changes color_changed = QtCore.pyqtSignal(QtGui.QColor) #: QtCore.QColor. The current color that is displayed color = None def __init__(self, color='w', *args, **kwargs): """The color to display Parameters ---------- color: object Either a QtGui.QColor object or a color that can be converted to RGBA using the :func:`matplotlib.colors.to_rgba` function""" super(ColorLabel, self).__init__(*args, **kwargs) self.setColumnCount(1) self.setRowCount(1) self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.horizontalHeader().setHidden(True) self.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) self.verticalHeader().setHidden(True) self.verticalHeader().setSectionResizeMode(QHeaderView.Stretch) self.setEditTriggers(QTableWidget.NoEditTriggers) self.setSelectionMode(QTableWidget.NoSelection) self.itemClicked.connect(self.select_color) self.color_item = QTableWidgetItem() self.setItem(0, 0, self.color_item) self.adjust_height() self.set_color(color) def select_color(self, *args): """Select a color using :meth:`PyQt5.QtWidgets.QColorDialog.getColor` """ self.set_color( QColorDialog.getColor(self.color_item.background().color())) def set_color(self, color): """Set the color of the label This method sets the given `color` as background color for the cell and emits the :attr:`color_changed` signal Parameters ---------- color: object Either a QtGui.QColor object or a color that can be converted to RGBA using the :func:`matplotlib.colors.to_rgba` function""" color = self._set_color(color) self.color_changed.emit(color) def _set_color(self, color): if not isinstance(color, QtGui.QColor): color = QtGui.QColor(*map(int, np.round(mcol.to_rgba(color)) * 255)) self.color_item.setBackground(color) self.color = color return color def adjust_height(self): """Adjust the height to match the row height""" h = self.rowHeight(0) * self.rowCount() self.setMaximumHeight(h) self.setMinimumHeight(h) def sizeHint(self): """Reimplemented to use the rowHeight as height""" s = super(ColorLabel, self).sizeHint() return QtCore.QSize(s.width(), self.rowHeight(0) * self.rowCount())
class RcParamsTree(QTreeWidget): """A QTreeWidget that can be used to display a RcParams instance This widget is populated by a :class:`psyplot.config.rcsetup.RcParams` instance and displays whether the values are valid or not""" #: A signal that shall be emitted if the validation state changes validChanged = QtCore.pyqtSignal(bool) #: A signal that is emitted if changes are propsed. It is either emitted #: with the parent of this instance (if this is not None) or with the #: instance itself propose_changes = QtCore.pyqtSignal(object) #: The :class:`~psyplot.config.rcsetup.RcParams` to display rc = None #: list of :class:`bool`. A boolean for each rcParams key that states #: whether the proposed value is valid or not valid = [] value_col = 2 def __init__(self, rcParams, validators, descriptions, *args, **kwargs): """ Parameters ---------- rcParams: dict The dictionary that contains the rcParams validators: dict A mapping from the `rcParams` key to the validation function for the corresponding value descriptions: dict A mapping from the `rcParams` key to it's description See Also -------- psyplot.config.rcsetup.RcParams psyplot.config.rcsetup.RcParams.validate psyplot.config.rcsetup.RcParams.descriptions """ super(RcParamsTree, self).__init__(*args, **kwargs) self.rc = rcParams self.validators = validators self.descriptions = descriptions self.setContextMenuPolicy(Qt.CustomContextMenu) self.customContextMenuRequested.connect(self.open_menu) self.setColumnCount(self.value_col + 1) self.setHeaderLabels(['RcParams key', '', 'Value']) @property def is_valid(self): """True if all the proposed values in this tree are valid""" return all(self.valid) @property def top_level_items(self): """An iterator over the topLevelItems in this tree""" return map(self.topLevelItem, range(self.topLevelItemCount())) def initialize(self): """Fill the items of the :attr:`rc` into the tree""" rcParams = self.rc descriptions = self.descriptions self.valid = [True] * len(rcParams) validators = self.validators vcol = self.value_col for i, (key, val) in enumerate(sorted(rcParams.items())): item = QTreeWidgetItem(0) item.setText(0, key) item.setToolTip(0, key) item.setIcon(1, QIcon(get_icon('valid.png'))) desc = descriptions.get(key) if desc: item.setText(vcol, desc) item.setToolTip(vcol, desc) child = QTreeWidgetItem(0) item.addChild(child) self.addTopLevelItem(item) editor = QTextEdit(self) # set maximal height of the editor to 3 rows editor.setMaximumHeight(4 * QtGui.QFontMetrics(editor.font()).height()) editor.setPlainText(yaml.dump(val)) self.setItemWidget(child, vcol, editor) editor.textChanged.connect( self.set_icon_func(i, item, validators[key])) self.resizeColumnToContents(0) self.resizeColumnToContents(1) def set_icon_func(self, i, item, validator): """Create a function to change the icon of one topLevelItem This method creates a function that can be called when the value of an item changes to display it's valid state. The returned function changes the icon of the given topLevelItem depending on whether the proposed changes are valid or not and it modifies the :attr:`valid` attribute accordingly Parameters ---------- i: int The index of the topLevelItem item: QTreeWidgetItem The topLevelItem validator: func The validation function Returns ------- function The function that can be called to set the correct icon""" def func(): editor = self.itemWidget(item.child(0), self.value_col) s = asstring(editor.toPlainText()) try: val = yaml.load(s, Loader=yaml.Loader) except Exception as e: item.setIcon(1, QIcon(get_icon('warning.png'))) item.setToolTip(1, "Could not parse yaml code: %s" % e) self.set_valid(i, False) return try: validator(val) except Exception as e: item.setIcon(1, QIcon(get_icon('invalid.png'))) item.setToolTip(1, "Wrong value: %s" % e) self.set_valid(i, False) else: item.setIcon(1, QIcon(get_icon('valid.png'))) self.set_valid(i, True) self.propose_changes.emit(self.parent() or self) return func def set_valid(self, i, b): """Set the validation status If the validation status changed compared to the old one, the :attr:`validChanged` signal is emitted Parameters ---------- i: int The index of the topLevelItem b: bool The valid state of the item """ old = self.is_valid self.valid[i] = b new = self.is_valid if new is not old: self.validChanged.emit(new) def open_menu(self, position): """Open a menu to expand and collapse all items in the tree Parameters ---------- position: QPosition The position where to open the menu""" menu = QMenu() expand_all_action = QAction('Expand all', self) expand_all_action.triggered.connect(self.expandAll) menu.addAction(expand_all_action) collapse_all_action = QAction('Collapse all', self) collapse_all_action.triggered.connect(self.collapseAll) menu.addAction(collapse_all_action) menu.exec_(self.viewport().mapToGlobal(position)) def changed_rc(self, use_items=False): """Iterate over the changed rcParams Parameters ---------- use_items: bool If True, the topLevelItems are used instead of the keys Yields ------ QTreeWidgetItem or str The item identifier object The proposed value""" def equals(item, key, val, orig): return val != orig for t in self._get_rc(equals): yield t[0 if use_items else 1], t[2] def selected_rc(self, use_items=False): """Iterate over the selected rcParams Parameters ---------- use_items: bool If True, the topLevelItems are used instead of the keys Yields ------ QTreeWidgetItem or str The item identifier object The proposed value""" def is_selected(item, key, val, orig): return item.isSelected() for t in self._get_rc(is_selected): yield t[0 if use_items else 1], t[2] def _get_rc(self, filter_func=None): """Iterate over the rcParams This function applies the given `filter_func` to check whether the item should be included or not Parameters ---------- filter_func: function A function that accepts the following arguments: item The QTreeWidgetItem key The rcParams key val The proposed value orig The current value Yields ------ QTreeWidgetItem The corresponding topLevelItem str The rcParams key object The proposed value object The current value """ def no_check(item, key, val, orig): return True rc = self.rc filter_func = filter_func or no_check for item in self.top_level_items: key = asstring(item.text(0)) editor = self.itemWidget(item.child(0), self.value_col) val = yaml.load(asstring(editor.toPlainText()), Loader=yaml.Loader) try: val = rc.validate[key](val) except: pass try: include = filter_func(item, key, val, rc[key]) except: warn('Could not check state for %s key' % key, RuntimeWarning) else: if include: yield (item, key, val, rc[key]) def apply_changes(self): """Update the :attr:`rc` with the proposed changes""" new = dict(self.changed_rc()) if new != self.rc: self.rc.update(new) def select_changes(self): """Select all the items that changed comparing to the current rcParams """ for item, val in self.changed_rc(True): item.setSelected(True)