class PreferencesDialog(QDialog): def __init__(self, parent=None): super().__init__(parent) self.contentsWidget = QListWidget() self.pagesWidget = QStackedWidget() self.plotPref = PlotPreferences(parent) self.dataPref = DataPreferences(parent) self.pagesWidget.addWidget(self.plotPref) self.pagesWidget.addWidget(self.dataPref) self.contentsWidget.addItem("Plot") self.contentsWidget.addItem("Data") self.contentsWidget.currentItemChanged.connect(self.changePage) self.contentsWidget.setCurrentRow(0) self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Apply | QDialogButtonBox.Close) okButton = self.buttonBox.button(QDialogButtonBox.Ok) okButton.clicked.connect(self.ok) applyButton = self.buttonBox.button(QDialogButtonBox.Apply) applyButton.clicked.connect(self.apply) closeButton = self.buttonBox.button(QDialogButtonBox.Close) closeButton.clicked.connect(self.close) horizontalLayout = QHBoxLayout() horizontalLayout.addWidget(self.contentsWidget) horizontalLayout.addWidget(self.pagesWidget) self.contentsWidget.setSizeAdjustPolicy( QAbstractScrollArea.AdjustToContents) mainLayout = QVBoxLayout() mainLayout.addLayout(horizontalLayout) mainLayout.addWidget(self.buttonBox) self.setLayout(mainLayout) self.setWindowTitle("Preferences") def changePage(self, current, previous): if not current: current = previous self.pagesWidget.setCurrentIndex(self.contentsWidget.row(current)) def show(self): for n in range(self.pagesWidget.count()): self.pagesWidget.widget(n).setCurrentValues() super().show() def apply(self): self.pagesWidget.currentWidget().apply() def ok(self): for idx in range(self.pagesWidget.count()): self.pagesWidget.widget(idx).apply() self.accept()
class RunConfigDialog(BaseRunConfigDialog): """Run configuration dialog box: multiple file version""" def __init__(self, parent=None): BaseRunConfigDialog.__init__(self, parent) self.file_to_run = None self.combo = None self.stack = None def run_btn_clicked(self): """Run button was just clicked""" self.file_to_run = to_text_string(self.combo.currentText()) def setup(self, fname): """Setup Run Configuration dialog with filename *fname*""" combo_label = QLabel(_("Select a run configuration:")) self.combo = QComboBox() self.combo.setMaxVisibleItems(20) self.combo.setSizeAdjustPolicy(QComboBox.AdjustToMinimumContentsLength) self.combo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.stack = QStackedWidget() configurations = _get_run_configurations() for index, (filename, options) in enumerate(configurations): if fname == filename: break else: # There is no run configuration for script *fname*: # creating a temporary configuration that will be kept only if # dialog changes are accepted by the user configurations.insert(0, (fname, RunConfiguration(fname).get())) index = 0 for filename, options in configurations: widget = RunConfigOptions(self) widget.set(options) self.combo.addItem(filename) self.stack.addWidget(widget) self.combo.currentIndexChanged.connect(self.stack.setCurrentIndex) self.combo.setCurrentIndex(index) self.add_widgets(combo_label, self.combo, 10, self.stack) self.add_button_box(QDialogButtonBox.Ok|QDialogButtonBox.Cancel) self.setWindowTitle(_("Run configuration per file")) def accept(self): """Reimplement Qt method""" configurations = [] for index in range(self.stack.count()): filename = to_text_string(self.combo.itemText(index)) runconfigoptions = self.stack.widget(index) if index == self.stack.currentIndex() and\ not runconfigoptions.is_valid(): return options = runconfigoptions.get() configurations.append( (filename, options) ) _set_run_configurations(configurations) QDialog.accept(self)
class VariableExplorer(SpyderPluginWidget): """Variable Explorer plugin.""" CONF_SECTION = 'variable_explorer' CONFIGWIDGET_CLASS = VariableExplorerConfigPage DISABLE_ACTIONS_WHEN_HIDDEN = False INITIAL_FREE_MEMORY_TIME_TRIGGER = 60 * 1000 # ms SECONDARY_FREE_MEMORY_TIME_TRIGGER = 180 * 1000 # ms sig_option_changed = Signal(str, object) def __init__(self, parent): SpyderPluginWidget.__init__(self, parent) # Widgets self.stack = QStackedWidget(self) self.shellwidgets = {} # Layout layout = QVBoxLayout() layout.addWidget(self.stack) self.setLayout(layout) # Initialize plugin self.initialize_plugin() def get_settings(self): """ Retrieve all Variable Explorer configuration settings. Specifically, return the settings in CONF_SECTION with keys in REMOTE_SETTINGS, and the setting 'dataframe_format'. Returns: dict: settings """ settings = {} for name in REMOTE_SETTINGS: settings[name] = self.get_option(name) # dataframe_format is stored without percent sign in config # to avoid interference with ConfigParser's interpolation name = 'dataframe_format' settings[name] = '%{0}'.format(self.get_option(name)) return settings @Slot(str, object) def change_option(self, option_name, new_value): """ Change a config option. This function is called if sig_option_changed is received. If the option changed is the dataframe format, then the leading '%' character is stripped (because it can't be stored in the user config). Then, the signal is emitted again, so that the new value is saved in the user config. """ if option_name == 'dataframe_format': assert new_value.startswith('%') new_value = new_value[1:] self.sig_option_changed.emit(option_name, new_value) @Slot() def free_memory(self): """Free memory signal.""" self.main.free_memory() QTimer.singleShot(self.INITIAL_FREE_MEMORY_TIME_TRIGGER, lambda: self.main.free_memory()) QTimer.singleShot(self.SECONDARY_FREE_MEMORY_TIME_TRIGGER, lambda: self.main.free_memory()) # ----- Stack accesors ---------------------------------------------------- def set_current_widget(self, nsb): self.stack.setCurrentWidget(nsb) # We update the actions of the options button (cog menu) and we move # it to the layout of the current widget. self.refresh_actions() nsb.setup_options_button() def current_widget(self): return self.stack.currentWidget() def count(self): return self.stack.count() def remove_widget(self, nsb): self.stack.removeWidget(nsb) def add_widget(self, nsb): self.stack.addWidget(nsb) # ----- Public API -------------------------------------------------------- def add_shellwidget(self, shellwidget): """ Register shell with variable explorer. This function opens a new NamespaceBrowser for browsing the variables in the shell. """ shellwidget_id = id(shellwidget) if shellwidget_id not in self.shellwidgets: self.options_button.setVisible(True) nsb = NamespaceBrowser(self, options_button=self.options_button) nsb.set_shellwidget(shellwidget) nsb.setup(**self.get_settings()) nsb.sig_option_changed.connect(self.change_option) nsb.sig_free_memory.connect(self.free_memory) self.add_widget(nsb) self.shellwidgets[shellwidget_id] = nsb self.set_shellwidget_from_id(shellwidget_id) return nsb def remove_shellwidget(self, shellwidget_id): # If shellwidget_id is not in self.shellwidgets, it simply means # that shell was not a Python-based console (it was a terminal) if shellwidget_id in self.shellwidgets: nsb = self.shellwidgets.pop(shellwidget_id) self.remove_widget(nsb) nsb.close() def set_shellwidget_from_id(self, shellwidget_id): if shellwidget_id in self.shellwidgets: nsb = self.shellwidgets[shellwidget_id] self.set_current_widget(nsb) def import_data(self, fname): """Import data in current namespace""" if self.count(): nsb = self.current_widget() nsb.refresh_table() nsb.import_data(filenames=fname) if self.dockwidget and not self.ismaximized: self.dockwidget.setVisible(True) self.dockwidget.raise_() #------ SpyderPluginWidget API --------------------------------------------- def get_plugin_title(self): """Return widget title""" return _('Variable explorer') def get_plugin_icon(self): """Return plugin icon""" return ima.icon('dictedit') def get_focus_widget(self): """ Return the widget to give focus to when this plugin's dockwidget is raised on top-level """ return self.current_widget() def closing_plugin(self, cancelable=False): """Perform actions before parent main window is closed""" return True def refresh_plugin(self): """Refresh widget""" pass def get_plugin_actions(self): """Return a list of actions related to plugin""" return self.current_widget().actions if self.current_widget() else [] def register_plugin(self): """Register plugin in Spyder's main window""" self.main.add_dockwidget(self) def apply_plugin_settings(self, options): """Apply configuration file's plugin settings""" for nsb in list(self.shellwidgets.values()): nsb.setup(**self.get_settings())
class ConfigDialog(QDialog): def __init__(self): super(ConfigDialog, self).__init__() # Set size and position self.setGeometry(0, 0, 900, 550) frameGm = self.frameGeometry() screen = QApplication.desktop().screenNumber(QApplication.desktop().cursor().pos()) centerPoint = QApplication.desktop().screenGeometry(screen).center() frameGm.moveCenter(centerPoint) self.move(frameGm.topLeft()) self.contentsWidget = QListView() self.contentsWidget.setViewMode(QListView.IconMode) # self.contentsWidget.setIconSize(QSize(96, 84)) self.contentsWidget.setMovement(QListView.Static) self.contentsWidget.setMaximumWidth(174) self.contentsWidget.setSpacing(12) self.contentsWidget.setSelectionMode(QAbstractItemView.SingleSelection) self.contentsModel = QStandardItemModel() self.contentsWidget.setModel(self.contentsModel) self.contentsWidget.selectionModel().currentChanged.connect(self.changePage) self.buttonboxWidget = QDialogButtonBox( QDialogButtonBox.Ok | QDialogButtonBox.Cancel | QDialogButtonBox.Apply# | QDialogButtonBox.Help ) self.buttonboxWidget.button(QDialogButtonBox.Ok).clicked.connect(self.ok) self.buttonboxWidget.button(QDialogButtonBox.Apply).clicked.connect(self.apply) self.buttonboxWidget.button(QDialogButtonBox.Cancel).clicked.connect(self.close) self.pagesWidget = QStackedWidget() horizontalLayout = QHBoxLayout() horizontalLayout.addWidget(self.contentsWidget) horizontalLayout.addWidget(self.pagesWidget, 1) mainLayout = QVBoxLayout() mainLayout.addLayout(horizontalLayout) # mainLayout.addStretch(1) mainLayout.addSpacing(12) mainLayout.addWidget(self.buttonboxWidget) self.setLayout(mainLayout) self.setWindowTitle("Config Dialog") # Set modality self.setModal(True) self.lastwidget = None self.createIcons() self.restore() pluginmanager.attach(self.pluginsChanged) def createIcons(self): self.contentsModel.clear() for pluginInfo in pluginmanager.getPluginsOfCategory("SettingsPlugin"): item = QStandardItem(pluginInfo.plugin_object.icon, pluginInfo.plugin_object.name()) item.widget = pluginInfo.plugin_object.widget item.setTextAlignment(Qt.AlignHCenter) item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) item.setSizeHint(QSize(136, 80)) self.contentsModel.appendRow(item) def show(self): if self.lastwidget: self.pagesWidget.addWidget(self.lastwidget) self.pagesWidget.setCurrentWidget(self.lastwidget) self.restore() super(ConfigDialog, self).show() def changePage(self, current, previous): if not current: current = previous current = self.contentsModel.itemFromIndex(current) self.pagesWidget.addWidget(current.widget) self.pagesWidget.setCurrentWidget(current.widget) self.lastwidget = current.widget def pluginsChanged(self): self.createIcons() def restore(self): for pluginInfo in pluginmanager.getPluginsOfCategory("SettingsPlugin"): pluginInfo.plugin_object.restore() self.apply() def ok(self): self._empty() self.apply() self.accept() def apply(self): for pluginInfo in pluginmanager.getPluginsOfCategory("SettingsPlugin"): pluginInfo.plugin_object.save() def close(self): self._empty() self.restore() self.reject() def _empty(self): """ Disown all widget children (otherwise their c++ objects are force deleted when the dialog closes). Must be run in reverse to avoid index update errors """ for i in reversed(range(self.pagesWidget.count())): self.pagesWidget.widget(i).setParent(None) def closeEvent(self, event): self.close() event.accept() def keyPressEvent(self, e: QKeyEvent): if e.key() != Qt.Key_Escape: super(ConfigDialog, self).keyPressEvent(e) else: self.close()
class ExtendedTabWidget(QFrame): ''' Like QTabWidget, except the tab bar is located elsewhere. Intended for use with ExtendedTabBar. ''' def __init__(self): super(ExtendedTabWidget, self).__init__() self._tab_bar = None self._stack = QStackedWidget() self._main_layout = QBoxLayout(QBoxLayout.LeftToRight) self._main_layout.setContentsMargins(0, 0, 0, 0) self._main_layout.addWidget(self._stack) self.setLayout(self._main_layout) def _move_tab(self, from_, to): ''' Handles tab moves so that the tab bar indices stay aligned with the widget stack indices. ''' displaced_widget = self._stack.widget(from_) moved_widget = self._stack.widget(to) self._stack.removeWidget(moved_widget) self._stack.removeWidget(displaced_widget) self._stack.insertWidget(to, displaced_widget) self._stack.insertWidget(from_, moved_widget) self._stack.setCurrentIndex(self._tab_bar.currentIndex()) def setTabBar(self, tab_bar): ''' Sets the tab bar that will be used to switch between tabs. tab_bar The tab bar to set as the controller of this widget. ''' if self._tab_bar is not None: raise Exception('Tab bar already set.') self._tab_bar = tab_bar tab_bar.currentChanged.connect(self._stack.setCurrentIndex) tab_bar.tabCloseRequested.connect(self.closeTab) tab_bar.tabMoved.connect(self._move_tab) def closeTab(self, index): ''' Closes a tab, removing from this widget, the tab bar, and deleting its widget. index Index of the tab to be closed. ''' self._tab_bar.removeTab(index) widget = self._stack.widget(index) self._stack.removeWidget(widget) widget.deleteLater() self._stack.setCurrentIndex(self._tab_bar.currentIndex()) def addTab(self, widget, label): ''' Adds a tab. widget The widget for the tab contents. label The name of the tab to show in the tab bar. Returns the index of the added tab. ''' index = self._tab_bar.addTab(label) self._stack.insertWidget(index, widget) return index def count(self): ''' Returns the number of widgets. ''' return self._stack.count() def widget(self, index): ''' Returns the widget at the given index. ''' return self._stack.widget(index) def indexOf(self, widget): ''' Returns the index of the given widget. ''' return self._stack.indexOf(widget)
class Plots(SpyderPluginWidget): """Plots plugin.""" CONF_SECTION = 'plots' CONFIGWIDGET_CLASS = PlotsConfigPage DISABLE_ACTIONS_WHEN_HIDDEN = False sig_option_changed = Signal(str, object) def __init__(self, parent): SpyderPluginWidget.__init__(self, parent) # Widgets self.stack = QStackedWidget(self) self.shellwidgets = {} # Layout layout = QGridLayout(self) layout.addWidget(self.stack) # Initialize plugin self.initialize_plugin() def get_settings(self): """Retrieve all Plots configuration settings.""" return { name: self.get_option(name) for name in ['mute_inline_plotting', 'show_plot_outline'] } # ---- Stack accesors def set_current_widget(self, fig_browser): """ Set the currently visible fig_browser in the stack widget, refresh the actions of the cog menu button and move it to the layout of the new fig_browser. """ self.stack.setCurrentWidget(fig_browser) # We update the actions of the options button (cog menu) and # we move it to the layout of the current widget. self.refresh_actions() fig_browser.setup_options_button() def current_widget(self): return self.stack.currentWidget() def count(self): return self.stack.count() def remove_widget(self, fig_browser): self.stack.removeWidget(fig_browser) def add_widget(self, fig_browser): self.stack.addWidget(fig_browser) # ---- Public API def add_shellwidget(self, shellwidget): """ Register shell with figure explorer. This function opens a new FigureBrowser for browsing the figures in the shell. """ shellwidget_id = id(shellwidget) if shellwidget_id not in self.shellwidgets: self.options_button.setVisible(True) fig_browser = FigureBrowser(self, options_button=self.options_button) fig_browser.set_shellwidget(shellwidget) fig_browser.setup(**self.get_settings()) fig_browser.sig_option_changed.connect( self.sig_option_changed.emit) fig_browser.thumbnails_sb.redirect_stdio.connect( self.main.redirect_internalshell_stdio) self.add_widget(fig_browser) self.shellwidgets[shellwidget_id] = fig_browser self.set_shellwidget_from_id(shellwidget_id) return fig_browser def remove_shellwidget(self, shellwidget_id): # If shellwidget_id is not in self.shellwidgets, it simply means # that shell was not a Python-based console (it was a terminal) if shellwidget_id in self.shellwidgets: fig_browser = self.shellwidgets.pop(shellwidget_id) self.remove_widget(fig_browser) fig_browser.close() def set_shellwidget_from_id(self, shellwidget_id): if shellwidget_id in self.shellwidgets: fig_browser = self.shellwidgets[shellwidget_id] self.set_current_widget(fig_browser) # ---- SpyderPluginWidget API def get_plugin_title(self): """Return widget title""" return _('Plots') def get_plugin_icon(self): """Return plugin icon""" return ima.icon('hist') def get_focus_widget(self): """ Return the widget to give focus to when this plugin's dockwidget is raised on top-level """ return self.current_widget() def closing_plugin(self, cancelable=False): """Perform actions before parent main window is closed""" return True def refresh_plugin(self): """Refresh widget""" pass def get_plugin_actions(self): """Return a list of actions related to plugin""" return self.current_widget().actions if self.current_widget() else [] def register_plugin(self): """Register plugin in Spyder's main window""" self.main.add_dockwidget(self) def apply_plugin_settings(self, options): """Apply configuration file's plugin settings""" for fig_browser in list(self.shellwidgets.values()): fig_browser.setup(**self.get_settings())
class Plots(SpyderPluginWidget): """Plots plugin.""" CONF_SECTION = 'plots' CONFIGWIDGET_CLASS = PlotsConfigPage DISABLE_ACTIONS_WHEN_HIDDEN = False sig_option_changed = Signal(str, object) def __init__(self, parent): SpyderPluginWidget.__init__(self, parent) # Widgets self.stack = QStackedWidget(self) self.shellwidgets = {} # Layout layout = QGridLayout(self) layout.addWidget(self.stack) # Initialize plugin self.initialize_plugin() def get_settings(self): """Retrieve all Plots configuration settings.""" return {name: self.get_option(name) for name in ['mute_inline_plotting', 'show_plot_outline']} # ---- Stack accesors def set_current_widget(self, fig_browser): """ Set the currently visible fig_browser in the stack widget, refresh the actions of the cog menu button and move it to the layout of the new fig_browser. """ self.stack.setCurrentWidget(fig_browser) # We update the actions of the options button (cog menu) and # we move it to the layout of the current widget. self.refresh_actions() fig_browser.setup_options_button() def current_widget(self): return self.stack.currentWidget() def count(self): return self.stack.count() def remove_widget(self, fig_browser): self.stack.removeWidget(fig_browser) def add_widget(self, fig_browser): self.stack.addWidget(fig_browser) # ---- Public API def add_shellwidget(self, shellwidget): """ Register shell with figure explorer. This function opens a new FigureBrowser for browsing the figures in the shell. """ shellwidget_id = id(shellwidget) if shellwidget_id not in self.shellwidgets: self.options_button.setVisible(True) fig_browser = FigureBrowser( self, options_button=self.options_button, background_color=MAIN_BG_COLOR) fig_browser.set_shellwidget(shellwidget) fig_browser.setup(**self.get_settings()) fig_browser.sig_option_changed.connect( self.sig_option_changed.emit) fig_browser.thumbnails_sb.redirect_stdio.connect( self.main.redirect_internalshell_stdio) self.add_widget(fig_browser) self.shellwidgets[shellwidget_id] = fig_browser self.set_shellwidget_from_id(shellwidget_id) return fig_browser def remove_shellwidget(self, shellwidget_id): # If shellwidget_id is not in self.shellwidgets, it simply means # that shell was not a Python-based console (it was a terminal) if shellwidget_id in self.shellwidgets: fig_browser = self.shellwidgets.pop(shellwidget_id) self.remove_widget(fig_browser) fig_browser.close() def set_shellwidget_from_id(self, shellwidget_id): if shellwidget_id in self.shellwidgets: fig_browser = self.shellwidgets[shellwidget_id] self.set_current_widget(fig_browser) # ---- SpyderPluginWidget API def get_plugin_title(self): """Return widget title""" return _('Plots') def get_plugin_icon(self): """Return plugin icon""" return ima.icon('hist') def get_focus_widget(self): """ Return the widget to give focus to when this plugin's dockwidget is raised on top-level """ return self.current_widget() def closing_plugin(self, cancelable=False): """Perform actions before parent main window is closed""" return True def refresh_plugin(self): """Refresh widget""" pass def get_plugin_actions(self): """Return a list of actions related to plugin""" return self.current_widget().actions if self.current_widget() else [] def register_plugin(self): """Register plugin in Spyder's main window""" self.main.add_dockwidget(self) def apply_plugin_settings(self, options): """Apply configuration file's plugin settings""" for fig_browser in list(self.shellwidgets.values()): fig_browser.setup(**self.get_settings())
class ConfigDialog(QDialog): """Configuration or preferences dialog box""" # Signals check_settings = Signal() size_change = Signal(QSize) def __init__(self, parent=None, objname=None): QDialog.__init__(self, parent) # If used for data object in tree, the main is the tree widget. self.parent = parent self.objname = objname # Widgets self.pages_widget = QStackedWidget() self.contents_widget = QListWidget() self.button_reset = QPushButton(_('Reset to defaults')) bbox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Apply | QDialogButtonBox.Cancel) self.apply_btn = bbox.button(QDialogButtonBox.Apply) # Widgets setup # Destroying the C++ object right after closing the dialog box, # otherwise it may be garbage-collected in another QThread # (e.g. the editor's analysis thread in Ezcad), thus leading to # a segmentation fault on UNIX or an application crash on Windows self.setAttribute(Qt.WA_DeleteOnClose) if self.objname is None: self.setWindowTitle(_('Preferences')) else: self.setWindowTitle(_('Preferences of ') + self.objname) self.setWindowIcon(ima.icon('configure')) self.contents_widget.setMovement(QListView.Static) self.contents_widget.setSpacing(1) self.contents_widget.setCurrentRow(0) # Layout hsplitter = QSplitter() hsplitter.addWidget(self.contents_widget) hsplitter.addWidget(self.pages_widget) hsplitter.setSizes([150,500]) btnlayout = QHBoxLayout() btnlayout.addWidget(self.button_reset) btnlayout.addStretch(1) btnlayout.addWidget(bbox) vlayout = QVBoxLayout() vlayout.addWidget(hsplitter) vlayout.addLayout(btnlayout) self.setLayout(vlayout) # Signals and slots self.pages_widget.currentChanged.connect(self.current_page_changed) self.contents_widget.currentRowChanged.connect( self.pages_widget.setCurrentIndex) bbox.accepted.connect(self.accept) bbox.rejected.connect(self.reject) bbox.clicked.connect(self.button_clicked) # Ensures that the config is present on ezcad first run CONF.set('main', 'interface_language', load_lang_conf()) def get_current_index(self): """Return current page index""" return self.contents_widget.currentRow() def set_current_index(self, index): """Set current page index""" self.contents_widget.setCurrentRow(index) def get_page(self, index=None): """Return page widget""" if index is None: widget = self.pages_widget.currentWidget() else: widget = self.pages_widget.widget(index) return widget.widget() @Slot() def accept(self): """Reimplement Qt method""" for index in range(self.pages_widget.count()): configpage = self.get_page(index) configpage.apply_changes() QDialog.accept(self) def button_clicked(self, button): if button is self.apply_btn: # Apply button was clicked configpage = self.get_page() configpage.apply_changes() def current_page_changed(self, index): # widget = self.get_page(index) self.apply_btn.setVisible(True) self.apply_btn.setEnabled(True) def add_page(self, widget): scrollarea = QScrollArea(self) scrollarea.setWidgetResizable(True) scrollarea.setWidget(widget) self.pages_widget.addWidget(scrollarea) item = QListWidgetItem(self.contents_widget) try: item.setIcon(widget.get_icon()) except TypeError: pass item.setText(widget.get_name()) item.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled) item.setSizeHint(QSize(0, 25)) def check_all_settings(self): """This method is called to check all configuration page settings after configuration dialog has been shown""" self.check_settings.emit() def resizeEvent(self, event): """ Reimplement Qt method to be able to save the widget's size from the main application """ QDialog.resizeEvent(self, event) self.size_change.emit(self.size())
class MxStackedMixin: """Mixin to Plugin classes to stacked child widgets. The stacked child widgets are of MX_WIDGET_CLASS. Each of the stacked child widgets is connected to each MxShellWidget. """ MX_WIDGET_CLASS = None # To be defined in sub class def __init__(self, parent): self.main = parent # Spyder3 # Widgets self.stack = QStackedWidget(self) self.shellwidgets = {} # Fallback widget when no MxConsole available. # Spyder 4 output internal error message without this. self.blankwidget = QLabel(text="No MxConsole Available") self.blankwidget.setAlignment(Qt.AlignCenter) self.stack.addWidget(self.blankwidget) # On active tab in IPython console change if spyder.version_info > (5, 2): self.main.ipyconsole.get_widget().tabwidget.currentChanged.connect( self.on_ipyconsole_current_changed) else: self.main.ipyconsole.tabwidget.currentChanged.connect( self.on_ipyconsole_current_changed) # ----- Stack accesors ---------------------------------------------------- # Modified from https://github.com/spyder-ide/spyder/blob/v3.3.2/spyder/plugins/variableexplorer.py#L140 def set_current_widget(self, mxwidget): self.stack.setCurrentWidget(mxwidget) # self.refresh_actions() if spyder.version_info < (5,): mxwidget.setup_options_button() def current_widget(self): return self.stack.currentWidget() def count(self): return self.stack.count() def remove_widget(self, mxwidget): self.stack.removeWidget(mxwidget) def add_widget(self, mxwidget): self.stack.addWidget(mxwidget) # ----- Public API -------------------------------------------------------- # Modified from https://github.com/spyder-ide/spyder/blob/v3.3.2/spyder/plugins/variableexplorer.py#L156 def add_shellwidget(self, shellwidget): """ Register shell with variable explorer. This function opens a new NamespaceBrowser for browsing the variables in the shell. """ shellwidget_id = id(shellwidget) if shellwidget_id not in self.shellwidgets: if spyder.version_info < (4,) or spyder.version_info > (5,): mxwidget = self.MX_WIDGET_CLASS(self, options_button=None) else: self.options_button.setVisible(True) mxwidget = self.MX_WIDGET_CLASS( self, options_button=self.options_button) mxwidget.set_shellwidget(shellwidget) # analyzer.sig_option_changed.connect(self.change_option) # analyzer.sig_free_memory.connect(self.free_memory) self.add_widget(mxwidget) self.shellwidgets[shellwidget_id] = mxwidget self.set_shellwidget_from_id(shellwidget_id) return mxwidget def remove_shellwidget(self, shellwidget_id): # If shellwidget_id is not in self.shellwidgets, it simply means # that shell was not a Python-based console (it was a terminal) if shellwidget_id in self.shellwidgets: mxwidget = self.shellwidgets.pop(shellwidget_id) self.remove_widget(mxwidget) mxwidget.close() def set_shellwidget_from_id(self, shellwidget_id): if shellwidget_id in self.shellwidgets: mxwidget = self.shellwidgets[shellwidget_id] self.set_current_widget(mxwidget) def on_ipyconsole_current_changed(self): # Slot like IPythonConsole.reflesh_plugin if spyder.version_info > (5, 2): client = self.main.ipyconsole.get_widget().tabwidget.currentWidget() else: client = self.main.ipyconsole.tabwidget.currentWidget() if client: sw = client.shellwidget self.set_shellwidget_from_id(id(sw))
class ArrayEditor(QDialog): """Array Editor Dialog""" def __init__(self, parent=None): QDialog.__init__(self, parent) # Destroying the C++ object right after closing the dialog box, # otherwise it may be garbage-collected in another QThread # (e.g. the editor's analysis thread in Spyder), thus leading to # a segmentation fault on UNIX or an application crash on Windows self.setAttribute(Qt.WA_DeleteOnClose) self.data = None self.arraywidget = None self.stack = None self.layout = None self.btn_save_and_close = None self.btn_close = None # Values for 3d array editor self.dim_indexes = [{}, {}, {}] self.last_dim = 0 # Adjust this for changing the startup dimension def setup_and_check(self, data, title='', readonly=False, xlabels=None, ylabels=None): """ Setup ArrayEditor: return False if data is not supported, True otherwise """ self.data = data self.data.flags.writeable = True is_record_array = data.dtype.names is not None is_masked_array = isinstance(data, np.ma.MaskedArray) if data.ndim > 3: self.error( _("Arrays with more than 3 dimensions are not " "supported")) return False if xlabels is not None and len(xlabels) != self.data.shape[1]: self.error( _("The 'xlabels' argument length do no match array " "column number")) return False if ylabels is not None and len(ylabels) != self.data.shape[0]: self.error( _("The 'ylabels' argument length do no match array row " "number")) return False if not is_record_array: dtn = data.dtype.name if dtn not in SUPPORTED_FORMATS and not dtn.startswith('str') \ and not dtn.startswith('unicode'): arr = _("%s arrays") % data.dtype.name self.error(_("%s are currently not supported") % arr) return False self.layout = QGridLayout() self.setLayout(self.layout) self.setWindowIcon(ima.icon('arredit')) if title: title = to_text_string(title) + " - " + _("NumPy array") else: title = _("Array editor") if readonly: title += ' (' + _('read only') + ')' self.setWindowTitle(title) self.resize(600, 500) # Stack widget self.stack = QStackedWidget(self) if is_record_array: for name in data.dtype.names: self.stack.addWidget( ArrayEditorWidget(self, data[name], readonly, xlabels, ylabels)) elif is_masked_array: self.stack.addWidget( ArrayEditorWidget(self, data, readonly, xlabels, ylabels)) self.stack.addWidget( ArrayEditorWidget(self, data.data, readonly, xlabels, ylabels)) self.stack.addWidget( ArrayEditorWidget(self, data.mask, readonly, xlabels, ylabels)) elif data.ndim == 3: pass else: self.stack.addWidget( ArrayEditorWidget(self, data, readonly, xlabels, ylabels)) self.arraywidget = self.stack.currentWidget() if self.arraywidget: self.arraywidget.model.dataChanged.connect( self.save_and_close_enable) self.stack.currentChanged.connect(self.current_widget_changed) self.layout.addWidget(self.stack, 1, 0) # Buttons configuration btn_layout = QHBoxLayout() if is_record_array or is_masked_array or data.ndim == 3: if is_record_array: btn_layout.addWidget(QLabel(_("Record array fields:"))) names = [] for name in data.dtype.names: field = data.dtype.fields[name] text = name if len(field) >= 3: title = field[2] if not is_text_string(title): title = repr(title) text += ' - ' + title names.append(text) else: names = [_('Masked data'), _('Data'), _('Mask')] if data.ndim == 3: # QSpinBox self.index_spin = QSpinBox(self, keyboardTracking=False) self.index_spin.valueChanged.connect(self.change_active_widget) # QComboBox names = [str(i) for i in range(3)] ra_combo = QComboBox(self) ra_combo.addItems(names) ra_combo.currentIndexChanged.connect(self.current_dim_changed) # Adding the widgets to layout label = QLabel(_("Axis:")) btn_layout.addWidget(label) btn_layout.addWidget(ra_combo) self.shape_label = QLabel() btn_layout.addWidget(self.shape_label) label = QLabel(_("Index:")) btn_layout.addWidget(label) btn_layout.addWidget(self.index_spin) self.slicing_label = QLabel() btn_layout.addWidget(self.slicing_label) # set the widget to display when launched self.current_dim_changed(self.last_dim) else: ra_combo = QComboBox(self) ra_combo.currentIndexChanged.connect( self.stack.setCurrentIndex) ra_combo.addItems(names) btn_layout.addWidget(ra_combo) if is_masked_array: label = QLabel( _("<u>Warning</u>: changes are applied separately")) label.setToolTip(_("For performance reasons, changes applied "\ "to masked array won't be reflected in "\ "array's data (and vice-versa).")) btn_layout.addWidget(label) btn_layout.addStretch() if not readonly: self.btn_save_and_close = QPushButton(_('Save and Close')) self.btn_save_and_close.setDisabled(True) self.btn_save_and_close.clicked.connect(self.accept) btn_layout.addWidget(self.btn_save_and_close) self.btn_close = QPushButton(_('Close')) self.btn_close.setAutoDefault(True) self.btn_close.setDefault(True) self.btn_close.clicked.connect(self.reject) btn_layout.addWidget(self.btn_close) self.layout.addLayout(btn_layout, 2, 0) self.setMinimumSize(400, 300) # Make the dialog act as a window self.setWindowFlags(Qt.Window) return True @Slot(QModelIndex, QModelIndex) def save_and_close_enable(self, left_top, bottom_right): """Handle the data change event to enable the save and close button.""" if self.btn_save_and_close: self.btn_save_and_close.setEnabled(True) self.btn_save_and_close.setAutoDefault(True) self.btn_save_and_close.setDefault(True) def current_widget_changed(self, index): self.arraywidget = self.stack.widget(index) self.arraywidget.model.dataChanged.connect(self.save_and_close_enable) def change_active_widget(self, index): """ This is implemented for handling negative values in index for 3d arrays, to give the same behavior as slicing """ string_index = [':'] * 3 string_index[self.last_dim] = '<font color=red>%i</font>' self.slicing_label.setText( (r"Slicing: [" + ", ".join(string_index) + "]") % index) if index < 0: data_index = self.data.shape[self.last_dim] + index else: data_index = index slice_index = [slice(None)] * 3 slice_index[self.last_dim] = data_index stack_index = self.dim_indexes[self.last_dim].get(data_index) if stack_index == None: stack_index = self.stack.count() try: self.stack.addWidget( ArrayEditorWidget(self, self.data[slice_index])) except IndexError: # Handle arrays of size 0 in one axis self.stack.addWidget(ArrayEditorWidget(self, self.data)) self.dim_indexes[self.last_dim][data_index] = stack_index self.stack.update() self.stack.setCurrentIndex(stack_index) def current_dim_changed(self, index): """ This change the active axis the array editor is plotting over in 3D """ self.last_dim = index string_size = ['%i'] * 3 string_size[index] = '<font color=red>%i</font>' self.shape_label.setText( ('Shape: (' + ', '.join(string_size) + ') ') % self.data.shape) if self.index_spin.value() != 0: self.index_spin.setValue(0) else: # this is done since if the value is currently 0 it does not emit # currentIndexChanged(int) self.change_active_widget(0) self.index_spin.setRange(-self.data.shape[index], self.data.shape[index] - 1) @Slot() def accept(self): """Reimplement Qt method""" for index in range(self.stack.count()): self.stack.widget(index).accept_changes() QDialog.accept(self) def get_value(self): """Return modified array -- this is *not* a copy""" # It is import to avoid accessing Qt C++ object as it has probably # already been destroyed, due to the Qt.WA_DeleteOnClose attribute return self.data def error(self, message): """An error occured, closing the dialog box""" QMessageBox.critical(self, _("Array editor"), message) self.setAttribute(Qt.WA_DeleteOnClose) self.reject() @Slot() def reject(self): """Reimplement Qt method""" if self.arraywidget is not None: for index in range(self.stack.count()): self.stack.widget(index).reject_changes() QDialog.reject(self)
class PreferencesDialog(QDialog): """Preferences Dialog for Napari user settings.""" ui_schema = { "call_order": { "ui:widget": "plugins" }, "highlight_thickness": { "ui:widget": "highlight" }, "shortcuts": { "ui:widget": "shortcuts" }, "extension2reader": { "ui:widget": "extension2reader" }, } resized = Signal(QSize) def __init__(self, parent=None): from ...settings import get_settings super().__init__(parent) self.setWindowTitle(trans._("Preferences")) self._settings = get_settings() self._stack = QStackedWidget(self) self._list = QListWidget(self) self._list.setObjectName("Preferences") self._list.currentRowChanged.connect(self._stack.setCurrentIndex) # Set up buttons self._button_cancel = QPushButton(trans._("Cancel")) self._button_cancel.clicked.connect(self.reject) self._button_ok = QPushButton(trans._("OK")) self._button_ok.clicked.connect(self.accept) self._button_ok.setDefault(True) self._button_restore = QPushButton(trans._("Restore defaults")) self._button_restore.clicked.connect(self._restore_default_dialog) # Layout left_layout = QVBoxLayout() left_layout.addWidget(self._list) left_layout.addStretch() left_layout.addWidget(self._button_restore) left_layout.addWidget(self._button_cancel) left_layout.addWidget(self._button_ok) self.setLayout(QHBoxLayout()) self.layout().addLayout(left_layout, 1) self.layout().addWidget(self._stack, 3) # Build dialog from settings self._rebuild_dialog() def keyPressEvent(self, e: 'QKeyEvent'): if e.key() == Qt.Key_Escape: # escape key should just close the window # which implies "accept" e.accept() self.accept() return super().keyPressEvent(e) def resizeEvent(self, event): """Override to emit signal.""" self.resized.emit(event.size()) super().resizeEvent(event) def _rebuild_dialog(self): """Removes settings not to be exposed to user and creates dialog pages.""" # FIXME: this dialog should not need to know about the plugin manager from ...plugins import plugin_manager self._starting_pm_order = plugin_manager.call_order() self._starting_values = self._settings.dict(exclude={'schema_version'}) self._list.clear() while self._stack.count(): self._stack.removeWidget(self._stack.currentWidget()) for field in self._settings.__fields__.values(): if isinstance(field.type_, type) and issubclass( field.type_, BaseModel): self._add_page(field) self._list.setCurrentRow(0) def _add_page(self, field: 'ModelField'): """Builds the preferences widget using the json schema builder. Parameters ---------- field : ModelField subfield for which to create a page. """ from ..._vendor.qt_json_builder.qt_jsonschema_form import WidgetBuilder schema, values = self._get_page_dict(field) name = field.field_info.title or field.name form = WidgetBuilder().create_form(schema, self.ui_schema) # set state values for widget form.widget.state = values # make settings follow state of the form widget form.widget.on_changed.connect( lambda d: getattr(self._settings, name.lower()).update(d)) # need to disable async if octree is enabled. # TODO: this shouldn't live here... if there is a coupling/dependency # between these settings, it should be declared in the settings schema if (name.lower() == 'experimental' and values['octree'] and self._settings.env_settings().get( 'experimental', {}).get('async_') not in (None, '0')): form_layout = form.widget.layout() for i in range(form_layout.count()): wdg = form_layout.itemAt(i, form_layout.FieldRole).widget() if getattr(wdg, '_name') == 'async_': wdg.opacity.setOpacity(0.3) wdg.setDisabled(True) break self._list.addItem(field.field_info.title or field.name) self._stack.addWidget(form) def _get_page_dict(self, field: 'ModelField') -> Tuple[dict, dict, dict]: """Provides the schema, set of values for each setting, and the properties for each setting.""" ftype = cast('BaseModel', field.type_) schema = json.loads(ftype.schema_json()) # find enums: for name, subfield in ftype.__fields__.items(): if isinstance(subfield.type_, EnumMeta): enums = [s.value for s in subfield.type_] # type: ignore schema["properties"][name]["enum"] = enums schema["properties"][name]["type"] = "string" # Need to remove certain properties that will not be displayed on the GUI setting = getattr(self._settings, field.name) with setting.enums_as_values(): values = setting.dict() napari_config = getattr(setting, "NapariConfig", None) if hasattr(napari_config, 'preferences_exclude'): for val in napari_config.preferences_exclude: schema['properties'].pop(val, None) values.pop(val, None) return schema, values def _restore_default_dialog(self): """Launches dialog to confirm restore settings choice.""" response = QMessageBox.question( self, trans._("Restore Settings"), trans._("Are you sure you want to restore default settings?"), QMessageBox.RestoreDefaults | QMessageBox.Cancel, QMessageBox.RestoreDefaults, ) if response == QMessageBox.RestoreDefaults: self._settings.reset() self._rebuild_dialog() # TODO: do we need this? def _restart_required_dialog(self): """Displays the dialog informing user a restart is required.""" QMessageBox.information( self, trans._("Restart required"), trans. _("A restart is required for some new settings to have an effect." ), ) def closeEvent(self, event: 'QCloseEvent') -> None: event.accept() self.accept() def reject(self): """Restores the settings in place when dialog was launched.""" self._settings.update(self._starting_values) # FIXME: this dialog should not need to know about the plugin manager if self._starting_pm_order: from ...plugins import plugin_manager plugin_manager.set_call_order(self._starting_pm_order) super().reject()
class PreferencesDialog(QDialog): """Preferences Dialog for Napari user settings.""" resized = Signal(QSize) def __init__(self, parent=None): super().__init__(parent) self._list = QListWidget(self) self._stack = QStackedWidget(self) self._list.setObjectName("Preferences") # Set up buttons self._button_cancel = QPushButton(trans._("Cancel")) self._button_ok = QPushButton(trans._("OK")) self._default_restore = QPushButton(trans._("Restore defaults")) # Setup self.setWindowTitle(trans._("Preferences")) # Layout left_layout = QVBoxLayout() left_layout.addWidget(self._list) left_layout.addStretch() left_layout.addWidget(self._default_restore) left_layout.addWidget(self._button_cancel) left_layout.addWidget(self._button_ok) main_layout = QHBoxLayout() main_layout.addLayout(left_layout, 1) main_layout.addWidget(self._stack, 3) self.setLayout(main_layout) # Signals self._list.currentRowChanged.connect( lambda index: self._stack.setCurrentIndex(index)) self._button_cancel.clicked.connect(self.on_click_cancel) self._button_ok.clicked.connect(self.on_click_ok) self._default_restore.clicked.connect(self.restore_defaults) # Make widget self.make_dialog() self._list.setCurrentRow(0) def resizeEvent(self, event): """Override to emit signal.""" self.resized.emit(event.size()) super().resizeEvent(event) def make_dialog(self): """Removes settings not to be exposed to user and creates dialog pages.""" # Because there are multiple pages, need to keep a list of values sets. self._values_orig_set_list = [] self._values_set_list = [] for _key, setting in SETTINGS.schemas().items(): schema = json.loads(setting['json_schema']) # Need to remove certain properties that will not be displayed on the GUI properties = schema.pop('properties') model = setting['model'] values = model.dict() napari_config = getattr(model, "NapariConfig", None) if napari_config is not None: for val in napari_config.preferences_exclude: properties.pop(val) values.pop(val) schema['properties'] = properties self._values_orig_set_list.append(set(values.items())) self._values_set_list.append(set(values.items())) # Only add pages if there are any properties to add. if properties: self.add_page(schema, values) def restore_defaults(self): """Launches dialog to confirm restore settings choice.""" widget = ConfirmDialog( parent=self, text=trans._("Are you sure you want to restore default settings?"), ) widget.valueChanged.connect(self._reset_widgets) widget.exec_() def _reset_widgets(self): """Deletes the widgets and rebuilds with defaults.""" self.close() self._list.clear() for n in range(self._stack.count()): widget = self._stack.removeWidget(self._stack.currentWidget()) del widget self.make_dialog() self._list.setCurrentRow(0) self.show() def on_click_ok(self): """Keeps the selected preferences saved to SETTINGS.""" self.close() def on_click_cancel(self): """Restores the settings in place when dialog was launched.""" # Need to check differences for each page. for n in range(self._stack.count()): # Must set the current row so that the proper set list is updated # in check differences. self._list.setCurrentRow(n) self.check_differences( self._values_orig_set_list[n], self._values_set_list[n], ) self._list.setCurrentRow(0) self.close() def add_page(self, schema, values): """Creates a new page for each section in dialog. Parameters ---------- schema : dict Json schema including all information to build each page in the preferences dialog. values : dict Dictionary of current values set in preferences. """ widget = self.build_page_dialog(schema, values) self._list.addItem(schema["title"]) self._stack.addWidget(widget) def build_page_dialog(self, schema, values): """Builds the preferences widget using the json schema builder. Parameters ---------- schema : dict Json schema including all information to build each page in the preferences dialog. values : dict Dictionary of current values set in preferences. """ builder = WidgetBuilder() form = builder.create_form(schema, {}) # set state values for widget form.widget.state = values form.widget.on_changed.connect(lambda d: self.check_differences( set(d.items()), self._values_set_list[self._list.currentIndex().row()], )) return form def check_differences(self, new_set, values_set): """Changes settings in settings manager with changes from dialog. Parameters ---------- new_set : set The set of new values, with tuples of key value pairs for each setting. values_set : set The old set of values. """ page = self._list.currentItem().text().split(" ")[0].lower() different_values = list(new_set - values_set) if len(different_values) > 0: # change the values in SETTINGS for val in different_values: try: setattr(SETTINGS._settings[page], val[0], val[1]) self._values_set_list[ self._list.currentIndex().row()] = new_set except: # noqa: E722 continue
class Plots(SpyderPluginWidget): """Plots plugin.""" CONF_SECTION = 'plots' CONF_FILE = False DISABLE_ACTIONS_WHEN_HIDDEN = False def __init__(self, parent): SpyderPluginWidget.__init__(self, parent) # Widgets self.stack = QStackedWidget(self) self.stack.setStyleSheet("QStackedWidget{padding: 0px; border: 0px}") self.shellwidgets = {} # Layout layout = QGridLayout(self) layout.addWidget(self.stack) def get_settings(self): """Retrieve all Plots configuration settings.""" return { name: self.get_option(name) for name in ['mute_inline_plotting', 'show_plot_outline', 'auto_fit_plotting'] } # ---- Stack accesors def set_current_widget(self, fig_browser): """ Set the currently visible fig_browser in the stack widget, refresh the actions of the cog menu button and move it to the layout of the new fig_browser. """ self.stack.setCurrentWidget(fig_browser) # We update the actions of the options button (cog menu) and # we move it to the layout of the current widget. self._refresh_actions() fig_browser.setup_options_button() def current_widget(self): return self.stack.currentWidget() def count(self): return self.stack.count() def remove_widget(self, fig_browser): self.stack.removeWidget(fig_browser) def add_widget(self, fig_browser): self.stack.addWidget(fig_browser) # ---- Public API def add_shellwidget(self, shellwidget): """ Register shell with figure explorer. This function opens a new FigureBrowser for browsing the figures in the shell. """ shellwidget_id = id(shellwidget) if shellwidget_id not in self.shellwidgets: self.options_button.setVisible(True) fig_browser = FigureBrowser(self, options_button=self.options_button, background_color=MAIN_BG_COLOR) fig_browser.set_shellwidget(shellwidget) fig_browser.setup(**self.get_settings()) fig_browser.sig_option_changed.connect( self.sig_option_changed.emit) fig_browser.thumbnails_sb.redirect_stdio.connect( self.main.redirect_internalshell_stdio) self.register_widget_shortcuts(fig_browser) self.add_widget(fig_browser) self.shellwidgets[shellwidget_id] = fig_browser self.set_shellwidget_from_id(shellwidget_id) return fig_browser def remove_shellwidget(self, shellwidget_id): # If shellwidget_id is not in self.shellwidgets, it simply means # that shell was not a Python-based console (it was a terminal) if shellwidget_id in self.shellwidgets: fig_browser = self.shellwidgets.pop(shellwidget_id) self.remove_widget(fig_browser) fig_browser.close() def set_shellwidget_from_id(self, shellwidget_id): if shellwidget_id in self.shellwidgets: fig_browser = self.shellwidgets[shellwidget_id] self.set_current_widget(fig_browser) # ---- SpyderPluginWidget API def get_plugin_title(self): """Return widget title""" return _('Plots') def get_plugin_icon(self): """Return plugin icon""" return ima.icon('hist') def get_focus_widget(self): """ Return the widget to give focus to when this plugin's dockwidget is raised on top-level """ return self.current_widget() def get_plugin_actions(self): """Return a list of actions related to plugin""" return self.current_widget().actions if self.current_widget() else [] def apply_plugin_settings(self, options): """Apply configuration file's plugin settings""" for fig_browser in list(self.shellwidgets.values()): fig_browser.setup(**self.get_settings()) def on_first_registration(self): """Action to be performed on first plugin registration""" self.tabify(self.main.variableexplorer)
class ArrayEditor(QDialog): """Array Editor Dialog""" def __init__(self, parent=None): QDialog.__init__(self, parent) # Destroying the C++ object right after closing the dialog box, # otherwise it may be garbage-collected in another QThread # (e.g. the editor's analysis thread in Spyder), thus leading to # a segmentation fault on UNIX or an application crash on Windows self.setAttribute(Qt.WA_DeleteOnClose) self.data = None self.arraywidget = None self.stack = None self.layout = None # Values for 3d array editor self.dim_indexes = [{}, {}, {}] self.last_dim = 0 # Adjust this for changing the startup dimension def setup_and_check(self, data, title='', readonly=False, xlabels=None, ylabels=None): """ Setup ArrayEditor: return False if data is not supported, True otherwise """ self.data = data is_record_array = data.dtype.names is not None is_masked_array = isinstance(data, np.ma.MaskedArray) if data.size == 0: self.error(_("Array is empty")) return False if data.ndim > 3: self.error(_("Arrays with more than 3 dimensions are not supported")) return False if xlabels is not None and len(xlabels) != self.data.shape[1]: self.error(_("The 'xlabels' argument length do no match array " "column number")) return False if ylabels is not None and len(ylabels) != self.data.shape[0]: self.error(_("The 'ylabels' argument length do no match array row " "number")) return False if not is_record_array: dtn = data.dtype.name if dtn not in SUPPORTED_FORMATS and not dtn.startswith('str') \ and not dtn.startswith('unicode'): arr = _("%s arrays") % data.dtype.name self.error(_("%s are currently not supported") % arr) return False self.layout = QGridLayout() self.setLayout(self.layout) self.setWindowIcon(ima.icon('arredit')) if title: title = to_text_string(title) + " - " + _("NumPy array") else: title = _("Array editor") if readonly: title += ' (' + _('read only') + ')' self.setWindowTitle(title) self.resize(600, 500) # Stack widget self.stack = QStackedWidget(self) if is_record_array: for name in data.dtype.names: self.stack.addWidget(ArrayEditorWidget(self, data[name], readonly, xlabels, ylabels)) elif is_masked_array: self.stack.addWidget(ArrayEditorWidget(self, data, readonly, xlabels, ylabels)) self.stack.addWidget(ArrayEditorWidget(self, data.data, readonly, xlabels, ylabels)) self.stack.addWidget(ArrayEditorWidget(self, data.mask, readonly, xlabels, ylabels)) elif data.ndim == 3: pass else: self.stack.addWidget(ArrayEditorWidget(self, data, readonly, xlabels, ylabels)) self.arraywidget = self.stack.currentWidget() self.stack.currentChanged.connect(self.current_widget_changed) self.layout.addWidget(self.stack, 1, 0) # Buttons configuration btn_layout = QHBoxLayout() if is_record_array or is_masked_array or data.ndim == 3: if is_record_array: btn_layout.addWidget(QLabel(_("Record array fields:"))) names = [] for name in data.dtype.names: field = data.dtype.fields[name] text = name if len(field) >= 3: title = field[2] if not is_text_string(title): title = repr(title) text += ' - '+title names.append(text) else: names = [_('Masked data'), _('Data'), _('Mask')] if data.ndim == 3: # QSpinBox self.index_spin = QSpinBox(self, keyboardTracking=False) self.index_spin.valueChanged.connect(self.change_active_widget) # QComboBox names = [str(i) for i in range(3)] ra_combo = QComboBox(self) ra_combo.addItems(names) ra_combo.currentIndexChanged.connect(self.current_dim_changed) # Adding the widgets to layout label = QLabel(_("Axis:")) btn_layout.addWidget(label) btn_layout.addWidget(ra_combo) self.shape_label = QLabel() btn_layout.addWidget(self.shape_label) label = QLabel(_("Index:")) btn_layout.addWidget(label) btn_layout.addWidget(self.index_spin) self.slicing_label = QLabel() btn_layout.addWidget(self.slicing_label) # set the widget to display when launched self.current_dim_changed(self.last_dim) else: ra_combo = QComboBox(self) ra_combo.currentIndexChanged.connect(self.stack.setCurrentIndex) ra_combo.addItems(names) btn_layout.addWidget(ra_combo) if is_masked_array: label = QLabel(_("<u>Warning</u>: changes are applied separately")) label.setToolTip(_("For performance reasons, changes applied "\ "to masked array won't be reflected in "\ "array's data (and vice-versa).")) btn_layout.addWidget(label) btn_layout.addStretch() bbox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) bbox.accepted.connect(self.accept) bbox.rejected.connect(self.reject) btn_layout.addWidget(bbox) self.layout.addLayout(btn_layout, 2, 0) self.setMinimumSize(400, 300) # Make the dialog act as a window self.setWindowFlags(Qt.Window) return True def current_widget_changed(self, index): self.arraywidget = self.stack.widget(index) def change_active_widget(self, index): """ This is implemented for handling negative values in index for 3d arrays, to give the same behavior as slicing """ string_index = [':']*3 string_index[self.last_dim] = '<font color=red>%i</font>' self.slicing_label.setText((r"Slicing: [" + ", ".join(string_index) + "]") % index) if index < 0: data_index = self.data.shape[self.last_dim] + index else: data_index = index slice_index = [slice(None)]*3 slice_index[self.last_dim] = data_index stack_index = self.dim_indexes[self.last_dim].get(data_index) if stack_index == None: stack_index = self.stack.count() self.stack.addWidget(ArrayEditorWidget(self, self.data[slice_index])) self.dim_indexes[self.last_dim][data_index] = stack_index self.stack.update() self.stack.setCurrentIndex(stack_index) def current_dim_changed(self, index): """ This change the active axis the array editor is plotting over in 3D """ self.last_dim = index string_size = ['%i']*3 string_size[index] = '<font color=red>%i</font>' self.shape_label.setText(('Shape: (' + ', '.join(string_size) + ') ') % self.data.shape) if self.index_spin.value() != 0: self.index_spin.setValue(0) else: # this is done since if the value is currently 0 it does not emit # currentIndexChanged(int) self.change_active_widget(0) self.index_spin.setRange(-self.data.shape[index], self.data.shape[index]-1) @Slot() def accept(self): """Reimplement Qt method""" for index in range(self.stack.count()): self.stack.widget(index).accept_changes() QDialog.accept(self) def get_value(self): """Return modified array -- this is *not* a copy""" # It is import to avoid accessing Qt C++ object as it has probably # already been destroyed, due to the Qt.WA_DeleteOnClose attribute return self.data def error(self, message): """An error occured, closing the dialog box""" QMessageBox.critical(self, _("Array editor"), message) self.setAttribute(Qt.WA_DeleteOnClose) self.reject() @Slot() def reject(self): """Reimplement Qt method""" if self.arraywidget is not None: for index in range(self.stack.count()): self.stack.widget(index).reject_changes() QDialog.reject(self)
class VariableExplorer(SpyderPluginWidget): """Variable Explorer plugin.""" CONF_SECTION = 'variable_explorer' CONFIGWIDGET_CLASS = VariableExplorerConfigPage DISABLE_ACTIONS_WHEN_HIDDEN = False INITIAL_FREE_MEMORY_TIME_TRIGGER = 60 * 1000 # ms SECONDARY_FREE_MEMORY_TIME_TRIGGER = 180 * 1000 # ms def __init__(self, parent): SpyderPluginWidget.__init__(self, parent) # Widgets self.stack = QStackedWidget(self) self.stack.setStyleSheet("QStackedWidget{padding: 0px; border: 0px}") self.shellwidgets = {} # Layout layout = QVBoxLayout() layout.addWidget(self.stack) self.setLayout(layout) def get_settings(self): """ Retrieve all Variable Explorer configuration settings. Specifically, return the settings in CONF_SECTION with keys in REMOTE_SETTINGS, and the setting 'dataframe_format'. Returns: dict: settings """ settings = {} for name in REMOTE_SETTINGS: settings[name] = self.get_option(name) # dataframe_format is stored without percent sign in config # to avoid interference with ConfigParser's interpolation name = 'dataframe_format' settings[name] = '%{0}'.format(self.get_option(name)) return settings @Slot(str, object) def change_option(self, option_name, new_value): """ Change a config option. This function is called if sig_option_changed is received. If the option changed is the dataframe format, then the leading '%' character is stripped (because it can't be stored in the user config). Then, the signal is emitted again, so that the new value is saved in the user config. """ if option_name == 'dataframe_format': assert new_value.startswith('%') new_value = new_value[1:] self.sig_option_changed.emit(option_name, new_value) @Slot() def free_memory(self): """Free memory signal.""" self.main.free_memory() QTimer.singleShot(self.INITIAL_FREE_MEMORY_TIME_TRIGGER, lambda: self.main.free_memory()) QTimer.singleShot(self.SECONDARY_FREE_MEMORY_TIME_TRIGGER, lambda: self.main.free_memory()) # ----- Stack accesors ---------------------------------------------------- def set_current_widget(self, nsb): self.stack.setCurrentWidget(nsb) # We update the actions of the options button (cog menu) and we move # it to the layout of the current widget. self._refresh_actions() nsb.setup_options_button() def current_widget(self): return self.stack.currentWidget() def count(self): return self.stack.count() def remove_widget(self, nsb): self.stack.removeWidget(nsb) def add_widget(self, nsb): self.stack.addWidget(nsb) # ----- Public API -------------------------------------------------------- def add_shellwidget(self, shellwidget): """ Register shell with variable explorer. This function opens a new NamespaceBrowser for browsing the variables in the shell. """ shellwidget_id = id(shellwidget) if shellwidget_id not in self.shellwidgets: self.options_button.setVisible(True) nsb = NamespaceBrowser(self, options_button=self.options_button) nsb.set_shellwidget(shellwidget) nsb.setup(**self.get_settings()) nsb.sig_option_changed.connect(self.change_option) nsb.sig_free_memory.connect(self.free_memory) self.add_widget(nsb) self.shellwidgets[shellwidget_id] = nsb self.set_shellwidget_from_id(shellwidget_id) return nsb def remove_shellwidget(self, shellwidget_id): # If shellwidget_id is not in self.shellwidgets, it simply means # that shell was not a Python-based console (it was a terminal) if shellwidget_id in self.shellwidgets: nsb = self.shellwidgets.pop(shellwidget_id) self.remove_widget(nsb) nsb.close() def set_shellwidget_from_id(self, shellwidget_id): if shellwidget_id in self.shellwidgets: nsb = self.shellwidgets[shellwidget_id] self.set_current_widget(nsb) def import_data(self, fname): """Import data in current namespace""" if self.count(): nsb = self.current_widget() nsb.refresh_table() nsb.import_data(filenames=fname) if self.dockwidget: self.switch_to_plugin() #------ SpyderPluginWidget API --------------------------------------------- def get_plugin_title(self): """Return widget title""" return _('Variable explorer') def get_plugin_icon(self): """Return plugin icon""" return ima.icon('dictedit') def get_focus_widget(self): """ Return the widget to give focus to when this plugin's dockwidget is raised on top-level """ return self.current_widget() def get_plugin_actions(self): """Return a list of actions related to plugin""" return self.current_widget().actions if self.current_widget() else [] def apply_plugin_settings(self, options): """Apply configuration file's plugin settings""" for nsb in list(self.shellwidgets.values()): nsb.setup(**self.get_settings())
class ConfigDialog(QDialog): """Spyder configuration ('Preferences') dialog box""" # Signals check_settings = Signal() size_change = Signal(QSize) def __init__(self, parent=None): QDialog.__init__(self, parent) self.main = parent # Widgets self.pages_widget = QStackedWidget() self.pages_widget.setMinimumWidth(600) self.contents_widget = QListWidget() self.button_reset = QPushButton(_('Reset to defaults')) bbox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Apply | QDialogButtonBox.Cancel) self.apply_btn = bbox.button(QDialogButtonBox.Apply) # Widgets setup # Destroying the C++ object right after closing the dialog box, # otherwise it may be garbage-collected in another QThread # (e.g. the editor's analysis thread in Spyder), thus leading to # a segmentation fault on UNIX or an application crash on Windows self.setAttribute(Qt.WA_DeleteOnClose) self.setWindowTitle(_('Preferences')) self.setWindowIcon(ima.icon('configure')) self.contents_widget.setMovement(QListView.Static) self.contents_widget.setSpacing(1) self.contents_widget.setCurrentRow(0) self.contents_widget.setMinimumWidth(220) self.contents_widget.setMinimumHeight(400) # Layout hsplitter = QSplitter() hsplitter.addWidget(self.contents_widget) hsplitter.addWidget(self.pages_widget) hsplitter.setStretchFactor(0, 1) hsplitter.setStretchFactor(1, 2) btnlayout = QHBoxLayout() btnlayout.addWidget(self.button_reset) btnlayout.addStretch(1) btnlayout.addWidget(bbox) vlayout = QVBoxLayout() vlayout.addWidget(hsplitter) vlayout.addLayout(btnlayout) self.setLayout(vlayout) # Signals and slots if self.main: self.button_reset.clicked.connect(self.main.reset_spyder) self.pages_widget.currentChanged.connect(self.current_page_changed) self.contents_widget.currentRowChanged.connect( self.pages_widget.setCurrentIndex) bbox.accepted.connect(self.accept) bbox.rejected.connect(self.reject) bbox.clicked.connect(self.button_clicked) # Ensures that the config is present on spyder first run CONF.set('main', 'interface_language', load_lang_conf()) def get_current_index(self): """Return current page index""" return self.contents_widget.currentRow() def set_current_index(self, index): """Set current page index""" self.contents_widget.setCurrentRow(index) def get_page(self, index=None): """Return page widget""" if index is None: widget = self.pages_widget.currentWidget() else: widget = self.pages_widget.widget(index) return widget.widget() @Slot() def accept(self): """Reimplement Qt method""" for index in range(self.pages_widget.count()): configpage = self.get_page(index) if not configpage.is_valid(): return configpage.apply_changes() QDialog.accept(self) def button_clicked(self, button): if button is self.apply_btn: # Apply button was clicked configpage = self.get_page() if not configpage.is_valid(): return configpage.apply_changes() def current_page_changed(self, index): widget = self.get_page(index) self.apply_btn.setVisible(widget.apply_callback is not None) self.apply_btn.setEnabled(widget.is_modified) def add_page(self, widget): self.check_settings.connect(widget.check_settings) widget.show_this_page.connect(lambda row=self.contents_widget.count(): self.contents_widget.setCurrentRow(row)) widget.apply_button_enabled.connect(self.apply_btn.setEnabled) scrollarea = QScrollArea(self) scrollarea.setWidgetResizable(True) scrollarea.setWidget(widget) self.pages_widget.addWidget(scrollarea) item = QListWidgetItem(self.contents_widget) try: item.setIcon(widget.get_icon()) except TypeError: pass item.setText(widget.get_name()) item.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled) item.setSizeHint(QSize(0, 25)) def check_all_settings(self): """This method is called to check all configuration page settings after configuration dialog has been shown""" self.check_settings.emit() def resizeEvent(self, event): """ Reimplement Qt method to be able to save the widget's size from the main application """ QDialog.resizeEvent(self, event) self.size_change.emit(self.size())
class PreferencesDialog(QDialog): """Preferences Dialog for Napari user settings.""" ui_schema = { "call_order": { "ui:widget": "plugins" }, } resized = Signal(QSize) closed = Signal() def __init__(self, parent=None): super().__init__(parent) self._list = QListWidget(self) self._stack = QStackedWidget(self) self._list.setObjectName("Preferences") # Set up buttons self._button_cancel = QPushButton(trans._("Cancel")) self._button_ok = QPushButton(trans._("OK")) self._default_restore = QPushButton(trans._("Restore defaults")) # Setup self.setWindowTitle(trans._("Preferences")) # Layout left_layout = QVBoxLayout() left_layout.addWidget(self._list) left_layout.addStretch() left_layout.addWidget(self._default_restore) left_layout.addWidget(self._button_cancel) left_layout.addWidget(self._button_ok) main_layout = QHBoxLayout() main_layout.addLayout(left_layout, 1) main_layout.addWidget(self._stack, 3) self.setLayout(main_layout) # Signals self._list.currentRowChanged.connect( lambda index: self._stack.setCurrentIndex(index)) self._button_cancel.clicked.connect(self.on_click_cancel) self._button_ok.clicked.connect(self.on_click_ok) self._default_restore.clicked.connect(self.restore_defaults) # Make widget self.make_dialog() self._list.setCurrentRow(0) def closeEvent(self, event): """Override to emit signal.""" self.closed.emit() super().closeEvent(event) def reject(self): """Override to handle Escape.""" super().reject() self.close() def resizeEvent(self, event): """Override to emit signal.""" self.resized.emit(event.size()) super().resizeEvent(event) def make_dialog(self): """Removes settings not to be exposed to user and creates dialog pages.""" # Because there are multiple pages, need to keep a dictionary of values dicts. # One set of keywords are for each page, then in each entry for a page, there are dicts # of setting and its value. self._values_orig_dict = {} self._values_dict = {} self._setting_changed_dict = {} for page, setting in SETTINGS.schemas().items(): schema, values, properties = self.get_page_dict(setting) self._setting_changed_dict[page] = {} self._values_orig_dict[page] = values self._values_dict[page] = values # Only add pages if there are any properties to add. if properties: self.add_page(schema, values) def get_page_dict(self, setting): """Provides the schema, set of values for each setting, and the properties for each setting. Parameters ---------- setting : dict Dictionary of settings for a page within the settings manager. Returns ------- schema : dict Json schema of the setting page. values : dict Dictionary of values currently set for each parameter in the settings. properties : dict Dictionary of properties within the json schema. """ schema = json.loads(setting['json_schema']) # Need to remove certain properties that will not be displayed on the GUI properties = schema.pop('properties') model = setting['model'] values = model.dict() napari_config = getattr(model, "NapariConfig", None) if napari_config is not None: for val in napari_config.preferences_exclude: properties.pop(val) values.pop(val) schema['properties'] = properties return schema, values, properties def restore_defaults(self): """Launches dialog to confirm restore settings choice.""" widget = ConfirmDialog( parent=self, text=trans._("Are you sure you want to restore default settings?"), ) widget.valueChanged.connect(self._reset_widgets) widget.exec_() def _reset_widgets(self): """Deletes the widgets and rebuilds with defaults.""" self.close() self._list.clear() for n in range(self._stack.count()): widget = self._stack.removeWidget(self._stack.currentWidget()) del widget self.make_dialog() self._list.setCurrentRow(0) self.show() def on_click_ok(self): """Keeps the selected preferences saved to SETTINGS.""" self.close() def on_click_cancel(self): """Restores the settings in place when dialog was launched.""" # Need to check differences for each page. for n in range(self._stack.count()): # Must set the current row so that the proper list is updated # in check differences. self._list.setCurrentRow(n) page = self._list.currentItem().text().split(" ")[0].lower() # get new values for settings. If they were changed from values at beginning # of preference dialog session, change them back. # Using the settings value seems to be the best way to get the checkboxes right # on the plugin call order widget. setting = SETTINGS.schemas()[page] schema, new_values, properties = self.get_page_dict(setting) self.check_differences(self._values_orig_dict[page], new_values) self._list.setCurrentRow(0) self.close() def add_page(self, schema, values): """Creates a new page for each section in dialog. Parameters ---------- schema : dict Json schema including all information to build each page in the preferences dialog. values : dict Dictionary of current values set in preferences. """ widget = self.build_page_dialog(schema, values) self._list.addItem(schema["title"]) self._stack.addWidget(widget) def build_page_dialog(self, schema, values): """Builds the preferences widget using the json schema builder. Parameters ---------- schema : dict Json schema including all information to build each page in the preferences dialog. values : dict Dictionary of current values set in preferences. """ builder = WidgetBuilder() form = builder.create_form(schema, self.ui_schema) # set state values for widget form.widget.state = values form.widget.on_changed.connect(lambda d: self.check_differences( d, self._values_dict[schema["title"].lower()], )) return form def _values_changed(self, page, new_dict, old_dict): """Loops through each setting in a page to determine if it changed. Parameters ---------- new_dict : dict Dict that has the most recent changes by user. Each key is a setting value and each item is the value. old_dict : dict Dict wtih values set at the begining of preferences dialog session. """ for setting_name, value in new_dict.items(): if value != old_dict[setting_name]: self._setting_changed_dict[page][setting_name] = value elif (value == old_dict[setting_name] and setting_name in self._setting_changed_dict[page]): self._setting_changed_dict[page].pop(setting_name) def check_differences(self, new_dict, old_dict): """Changes settings in settings manager with changes from dialog. Parameters ---------- new_dict : dict Dict that has the most recent changes by user. Each key is a setting parameter and each item is the value. old_dict : dict Dict wtih values set at the beginning of the preferences dialog session. """ page = self._list.currentItem().text().split(" ")[0].lower() self._values_changed(page, new_dict, old_dict) different_values = self._setting_changed_dict[page] if len(different_values) > 0: # change the values in SETTINGS for setting_name, value in different_values.items(): try: setattr(SETTINGS._settings[page], setting_name, value) self._values_dict[page] = new_dict except: # noqa: E722 continue
class FeaturesPlotWidget(CustomWidget): call_manage_settings = Signal() def __init__(self, *args, **kwargs): # define status images self.status_icons = { -2: QPixmap( os.path.join(os.path.dirname(__file__), 'icons', 'depth_status_delay.png')), -1: QPixmap( os.path.join(os.path.dirname(__file__), 'icons', 'depth_status_in_use.png')), 1: QPixmap( os.path.join(os.path.dirname(__file__), 'icons', 'depth_status_done.png')), 0: QPixmap( os.path.join(os.path.dirname(__file__), 'icons', 'depth_status_off.png')), } # Settings self.subject_settings = None self.procedure_settings = None self.depth_settings = None self.features_settings = None # Plot options self.plot_config = {} self.y_range = uVRANGE self.plot_stack = QStackedWidget() # generate a dict {chan_label: {Feature:[stack idx, latest_datum]}} self.stack_dict = {} # shared memory to display the currently monitored electrode self.monitored_channel_mem = QSharedMemory() self.monitored_channel_mem.setKey("MonitoredChannelMemory") self.monitored_channel_mem.attach(QSharedMemory.ReadOnly) # wrap up init super(FeaturesPlotWidget, self).__init__(*args, **kwargs) self.move(WINDOWDIMS_FEATURES[0], WINDOWDIMS_FEATURES[1]) self.resize(WINDOWDIMS_FEATURES[2], WINDOWDIMS_FEATURES[3]) self.setMaximumWidth(WINDOWDIMS_FEATURES[2]) # initialize plots self.layout().addWidget(self.plot_stack) self.refresh_axes() # Extra time on purpose. # Define and start processes # will only start processes when settings are received self.depth_wrapper = ProcessWrapper('Depth_Process') self.depth_process_running = False self.features_wrapper = ProcessWrapper('Features_Process') self.features_process_running = False def create_control_panel(self): # define Qt GUI elements layout = QHBoxLayout() layout_L = QVBoxLayout() layout_L1 = QHBoxLayout() # layout_L1.addSpacing(10) layout_L1.addWidget( QLabel("Electrode: ", alignment=Qt.AlignVCenter | Qt.AlignRight)) # Channel selection self.chan_select = QComboBox() self.chan_select.addItem("None") self.chan_select.setMinimumWidth(70) self.chan_select.setEnabled(False) layout_L1.addWidget(self.chan_select) layout_L1.addSpacing(20) # features selection layout_L1.addWidget( QLabel("Feature set: ", alignment=Qt.AlignVCenter | Qt.AlignRight)) self.feature_select = QComboBox() self.feature_select.setMinimumWidth(60) self.feature_select.addItems(['Raw', 'Mapping']) self.feature_select.setCurrentIndex(0) layout_L1.addWidget(self.feature_select) layout_L.addLayout(layout_L1) layout_L.addSpacing(5) layout_L2 = QHBoxLayout() layout_L2.addSpacing(10) layout_L2.addWidget( QLabel("+/- ", alignment=Qt.AlignVCenter | Qt.AlignRight)) self.range_edit = QLineEdit("{:.2f}".format(uVRANGE)) self.range_edit.setMaximumWidth(50) layout_L2.addWidget(self.range_edit) layout_L2.addSpacing(30) self.do_hp = QCheckBox('HP') self.do_hp.setChecked(True) layout_L2.addWidget(self.do_hp) layout_L2.addSpacing(30) self.sweep_control = QCheckBox("Match SweepGUI.") self.sweep_control.setChecked(True) self.sweep_control.setEnabled(True) layout_L2.addWidget(self.sweep_control) layout_L.addLayout(layout_L2) layout_R = QHBoxLayout() self.bt_refresh = QPushButton("Refresh") self.bt_refresh.setMaximumWidth(50) layout_R.addWidget(self.bt_refresh) layout_R.addSpacing(20) self.btn_settings = QPushButton("Settings") self.btn_settings.setMaximumWidth(50) layout_R.addWidget(self.btn_settings) layout_R.addSpacing(20) self.features_process_btn = QPushButton('Features') self.features_process_btn.setMaximumWidth(50) self.features_process_btn.setStyleSheet("QPushButton { color: white; " "background-color : red; " "border-color : red; " "border-width: 2px}") self.features_process_btn.clicked.connect( self.features_process_btn_callback) layout_R.addWidget(self.features_process_btn) layout_R.addSpacing(5) self.depth_process_btn = QPushButton('Record') self.depth_process_btn.setMaximumWidth(50) self.depth_process_btn.setStyleSheet("QPushButton { color: white; " "background-color : red; " "border-color : red; " "border-width: 2px}") self.depth_process_btn.clicked.connect(self.depth_process_btn_callback) layout_R.addWidget(self.depth_process_btn) layout_R.addSpacing(20) self.status_label = QLabel() self.status_label.setPixmap(self.status_icons[0]) layout_R.addWidget(self.status_label) layout_R.addSpacing(10) layout.addLayout(layout_L) layout.addStretch() layout.addLayout(layout_R) # layout.addSpacing(10) self.layout().addLayout(layout) # callbacks self.btn_settings.clicked.connect(self.call_manage_settings.emit) self.chan_select.currentIndexChanged.connect( self.manage_feat_chan_select) self.feature_select.currentIndexChanged.connect( self.manage_feat_chan_select) self.sweep_control.clicked.connect(self.manage_sweep_control) self.range_edit.editingFinished.connect(self.manage_range_edit) self.bt_refresh.clicked.connect(self.manage_refresh) def depth_process_btn_callback(self): # kill if self.depth_process_running: self.manage_depth_process(False) self.manage_nsp(False) else: # if we terminate and re-start the processes, we need to re-enable the shared memory self.depth_wrapper.manage_shared_memory() # re-send the settings self.depth_wrapper.send_settings(self.depth_settings) # start nsp recording if self.manage_nsp(True) == 0: # re-start the worker self.manage_depth_process(True) def features_process_btn_callback(self): # kill if self.features_process_running: self.manage_feature_process(False) else: # if we terminate and re-start the processes, we need to re-enable the shared memory self.features_wrapper.manage_shared_memory() # re-send the settings self.features_wrapper.send_settings(self.features_settings) # re-start the worker self.manage_feature_process(True) # GUI Callbacks def manage_feat_chan_select(self): self.plot_stack.setCurrentIndex( self.stack_dict[self.chan_select.currentText()][ self.feature_select.currentText()][0]) def manage_sweep_control(self): if self.sweep_control.isChecked( ) and self.monitored_channel_mem.isAttached(): self.chan_select.setEnabled(False) self.do_hp.setEnabled(False) self.range_edit.setEnabled(False) self.read_from_shared_memory() else: self.chan_select.setEnabled(True) self.do_hp.setEnabled(True) self.range_edit.setEnabled(True) def manage_range_edit(self): # need to do it like this because if we simply read the QLineEdit.text() on update calls, it breaks during # typing the new range values. self.y_range = float(self.range_edit.text()) @staticmethod def parse_patient_name(full_name): # parse the subject information names = full_name.split(' ') m_name = '' m_idx = -1 l_idx = -1 for idx, n in enumerate(names): if all([x.isupper() for x in n]): m_idx = idx l_idx = idx + 1 m_name = n break f_name = str.join(' ', names[:m_idx]) l_name = str.join(' ', names[l_idx:]) return f_name, m_name, l_name def manage_nsp(self, on_off): f_name, m_name, l_name = self.parse_patient_name( self.subject_settings['name']) file_info = { 'filename': os.path.normpath( os.path.join( BASEPATH, self.subject_settings['id'], self.procedure_settings['date'].strftime('%m%d%y') + '_' + self.subject_settings['id'] + '_' + self.procedure_settings['target_name'] + '_' + self.procedure_settings['recording_config'])), 'comment': self.subject_settings['NSP_comment'], 'patient_info': { 'ID': self.subject_settings['id'], # if only single name, returned in l_name 'firstname': f_name if f_name else l_name, 'middlename': m_name, # TODO: implement MiddleName 'lastname': l_name, 'DOBMonth': self.subject_settings['birthday'].month, 'DOBDay': self.subject_settings['birthday'].day, 'DOBYear': self.subject_settings['birthday'].year } } if not CbSdkConnection().is_connected: CbSdkConnection().connect() return CbSdkConnection().set_recording_state(on_off, file_info) def manage_depth_process(self, on_off): # start process if on_off and not self.depth_process_running: self.depth_wrapper.start_worker() self.depth_process_running = True else: self.depth_wrapper.kill_worker() self.depth_process_running = False def manage_feature_process(self, on_off): if on_off and not self.features_process_running: self.features_wrapper.start_worker() self.features_process_running = True else: self.features_wrapper.kill_worker() self.features_process_running = False def manage_refresh(self): self.plot_stack.widget(self.stack_dict[self.chan_select.currentText()][ self.feature_select.currentText()][0]).clear_plot() self.stack_dict[self.chan_select.currentText()][ self.feature_select.currentText()][1] = 0 def process_settings(self, sub_sett, proc_sett, depth_sett, feat_sett): self.subject_settings = dict(sub_sett) self.procedure_settings = dict(proc_sett) self.depth_settings = dict(depth_sett) self.features_settings = dict(feat_sett) # validate that we have some data in the electrode_settings. If the NSP is not connected we will have # to load the channel names from the DB. Also we want to keep the FeaturesGUI unaware of the DB channels. if len(self.depth_settings['electrode_settings']) == 0: self.depth_settings['electrode_settings'] = {} for lbl in DBWrapper().list_channel_labels(): self.depth_settings['electrode_settings'][lbl] = DEPTHSETTINGS CbSdkConnection().is_simulating = True # set new features self.feature_select.setCurrentIndex(0) # Raw while self.feature_select.count() > 2: # Raw and Mapping self.feature_select.removeItem(2) self.feature_select.addItems(self.features_settings['features'].keys()) # set new channels self.chan_select.setCurrentIndex(0) # None while self.chan_select.count() > 1: self.chan_select.removeItem(1) self.chan_select.addItems( self.depth_settings['electrode_settings'].keys()) # clear and update stacked widget to_delete = [ self.plot_stack.widget(x) for x in range(self.plot_stack.count()) ] for wid in to_delete: self.plot_stack.removeWidget(wid) wid.deleteLater() wid = None self.stack_dict = {} self.create_plots() self.depth_wrapper.send_settings(self.depth_settings) self.features_wrapper.send_settings(self.features_settings) if not self.features_process_running: self.manage_feature_process(True) # self.clear() self.read_from_shared_memory() def create_plots(self, theme='dark', **kwargs): # Collect PlotWidget configuration self.plot_config['theme'] = theme self.plot_config['color_iterator'] = -1 self.plot_config['x_range'] = XRANGE_FEATURES self.plot_config['y_range'] = uVRANGE self.plot_config['do_hp'] = True labels = [] for ii in range(0, self.chan_select.count()): labels.append(self.chan_select.itemText(ii)) # labels.extend(self.channel_labels) features = [] for ii in range(0, self.feature_select.count()): features.append(self.feature_select.itemText(ii)) stack_idx = 0 for lbl_idx, lbl in enumerate(labels): self.stack_dict[lbl] = {} self.plot_config['color_iterator'] = lbl_idx - 1 self.plot_config['title'] = lbl for feat in features: self.stack_dict[lbl][feat] = [stack_idx, 0] # TODO: not hard-coding?? if feat == 'Raw': self.plot_stack.addWidget(RawPlots(dict(self.plot_config))) elif feat == 'Mapping': self.plot_stack.addWidget( MappingPlots(dict(self.plot_config))) elif feat == 'STN': self.plot_stack.addWidget(STNPlots(dict(self.plot_config))) elif feat == 'LFP': self.plot_stack.addWidget(LFPPlots(dict(self.plot_config))) elif feat == 'Spikes': self.plot_stack.addWidget( SpikePlots(dict(self.plot_config))) else: self.plot_stack.addWidget( NullPlotWidget(dict(self.plot_config))) stack_idx += 1 self.plot_stack.setCurrentIndex(0) def refresh_axes(self): pass def clear(self): # set the current datum of all stacks to 0 for lbl in self.stack_dict: for feat in self.stack_dict[lbl]: self.stack_dict[lbl][feat][1] = 0 self.plot_stack.widget( self.stack_dict[lbl][feat][0]).clear_plot() def update(self): # Depth process output = self.depth_wrapper.worker_status() self.status_label.setPixmap(self.status_icons[output]) if self.depth_wrapper.is_running(): self.depth_process_btn.setStyleSheet("QPushButton { color: white; " "background-color : green; " "border-color : green; " "border-width: 2px}") else: self.depth_process_running = False self.depth_process_btn.setStyleSheet("QPushButton { color: white; " "background-color : red; " "border-color : red; " "border-width: 2px}") if self.features_wrapper.is_running(): self.features_process_btn.setStyleSheet( "QPushButton { color: white; " "background-color : green; " "border-color : green; " "border-width: 2px}") else: self.features_process_running = False self.features_process_btn.setStyleSheet( "QPushButton { color: white; " "background-color : red; " "border-color : red; " "border-width: 2px}") if self.sweep_control.isChecked(): self.read_from_shared_memory() # features plot curr_chan_lbl = self.chan_select.currentText() if curr_chan_lbl != 'None': curr_feat = self.feature_select.currentText() do_hp = self.do_hp.isChecked() if do_hp != self.plot_stack.currentWidget().plot_config['do_hp'] or \ self.y_range != self.plot_stack.currentWidget().plot_config['y_range']: self.plot_stack.currentWidget().clear_plot() self.stack_dict[curr_chan_lbl][curr_feat][1] = 0 self.plot_stack.currentWidget().plot_config['do_hp'] = do_hp self.plot_stack.currentWidget( ).plot_config['y_range'] = self.y_range curr_datum = self.stack_dict[curr_chan_lbl][curr_feat][1] if curr_feat == 'Raw': all_data = DBWrapper().load_depth_data(chan_lbl=curr_chan_lbl, gt=curr_datum, do_hp=do_hp, return_uV=True) elif curr_feat == 'Mapping': all_data = DBWrapper().load_mapping_response( chan_lbl=curr_chan_lbl, gt=curr_datum) else: all_data = DBWrapper().load_features_data( category=curr_feat, chan_lbl=curr_chan_lbl, gt=curr_datum) if all_data: self.plot_stack.currentWidget().update_plot(dict(all_data)) self.stack_dict[curr_chan_lbl][curr_feat][1] = max( all_data.keys()) def kill_processes(self): self.manage_depth_process(False) self.manage_feature_process(False) def read_from_shared_memory(self): if self.monitored_channel_mem.isAttached(): self.monitored_channel_mem.lock() settings = np.frombuffer(self.monitored_channel_mem.data(), dtype=np.float)[-3:] self.chan_select.setCurrentIndex(int(settings[0])) self.range_edit.setText(str(settings[1])) self.manage_range_edit() self.do_hp.setChecked(bool(settings[2])) self.monitored_channel_mem.unlock() else: self.monitored_channel_mem.attach() # self.sweep_control.setChecked(False) self.manage_sweep_control()
class PreferencesDialog(QDialog): """Preferences Dialog for Napari user settings.""" valueChanged = Signal() ui_schema = { "call_order": { "ui:widget": "plugins" }, "highlight_thickness": { "ui:widget": "highlight" }, } resized = Signal(QSize) closed = Signal() def __init__(self, parent=None): super().__init__(parent) self._list = QListWidget(self) self._stack = QStackedWidget(self) self._list.setObjectName("Preferences") # Set up buttons self._button_cancel = QPushButton(trans._("Cancel")) self._button_ok = QPushButton(trans._("OK")) self._default_restore = QPushButton(trans._("Restore defaults")) # Setup self.setWindowTitle(trans._("Preferences")) # Layout left_layout = QVBoxLayout() left_layout.addWidget(self._list) left_layout.addStretch() left_layout.addWidget(self._default_restore) left_layout.addWidget(self._button_cancel) left_layout.addWidget(self._button_ok) main_layout = QHBoxLayout() main_layout.addLayout(left_layout, 1) main_layout.addWidget(self._stack, 3) self.setLayout(main_layout) # Signals self._list.currentRowChanged.connect( lambda index: self._stack.setCurrentIndex(index)) self._button_cancel.clicked.connect(self.on_click_cancel) self._button_ok.clicked.connect(self.on_click_ok) self._default_restore.clicked.connect(self.restore_defaults) # Make widget self.make_dialog() self._list.setCurrentRow(0) def _restart_dialog(self, event=None, extra_str=""): """Displays the dialog informing user a restart is required. Paramters --------- event : Event extra_str : str Extra information to add to the message about needing a restart. """ text_str = trans._( "napari requires a restart for image rendering changes to apply.") widget = ResetNapariInfoDialog( parent=self, text=text_str, ) widget.exec_() def closeEvent(self, event): """Override to emit signal.""" self.closed.emit() super().closeEvent(event) def reject(self): """Override to handle Escape.""" super().reject() self.close() def resizeEvent(self, event): """Override to emit signal.""" self.resized.emit(event.size()) super().resizeEvent(event) def make_dialog(self): """Removes settings not to be exposed to user and creates dialog pages.""" settings = get_settings() # Because there are multiple pages, need to keep a dictionary of values dicts. # One set of keywords are for each page, then in each entry for a page, there are dicts # of setting and its value. self._values_orig_dict = {} self._values_dict = {} self._setting_changed_dict = {} for page, setting in settings.schemas().items(): schema, values, properties = self.get_page_dict(setting) self._setting_changed_dict[page] = {} self._values_orig_dict[page] = values self._values_dict[page] = values # Only add pages if there are any properties to add. if properties: self.add_page(schema, values) def get_page_dict(self, setting): """Provides the schema, set of values for each setting, and the properties for each setting. Parameters ---------- setting : dict Dictionary of settings for a page within the settings manager. Returns ------- schema : dict Json schema of the setting page. values : dict Dictionary of values currently set for each parameter in the settings. properties : dict Dictionary of properties within the json schema. """ schema = json.loads(setting['json_schema']) # Resolve allOf references definitions = schema.get("definitions", {}) if definitions: for key, data in schema["properties"].items(): if "allOf" in data: allof = data["allOf"] allof = [d["$ref"].rsplit("/")[-1] for d in allof] for definition in allof: local_def = definitions[definition] schema["properties"][key]["enum"] = local_def["enum"] schema["properties"][key]["type"] = "string" # Need to remove certain properties that will not be displayed on the GUI properties = schema.pop('properties') model = setting['model'] values = model.dict() napari_config = getattr(model, "NapariConfig", None) if napari_config is not None: for val in napari_config.preferences_exclude: properties.pop(val) values.pop(val) schema['properties'] = properties return schema, values, properties def restore_defaults(self): """Launches dialog to confirm restore settings choice.""" self._reset_dialog = ConfirmDialog( parent=self, text=trans._("Are you sure you want to restore default settings?"), ) self._reset_dialog.valueChanged.connect(self._reset_widgets) self._reset_dialog.exec_() def _reset_widgets(self): """Deletes the widgets and rebuilds with defaults.""" self.close() self.valueChanged.emit() self._list.clear() for n in range(self._stack.count()): widget = self._stack.removeWidget(self._stack.currentWidget()) del widget self.make_dialog() self._list.setCurrentRow(0) self.show() def on_click_ok(self): """Keeps the selected preferences saved to settings.""" self.close() def on_click_cancel(self): """Restores the settings in place when dialog was launched.""" # Need to check differences for each page. settings = get_settings() for n in range(self._stack.count()): # Must set the current row so that the proper list is updated # in check differences. self._list.setCurrentRow(n) page = self._list.currentItem().text().split(" ")[0].lower() # get new values for settings. If they were changed from values at beginning # of preference dialog session, change them back. # Using the settings value seems to be the best way to get the checkboxes right # on the plugin call order widget. setting = settings.schemas()[page] schema, new_values, properties = self.get_page_dict(setting) self.check_differences(self._values_orig_dict[page], new_values) self._list.setCurrentRow(0) self.close() def add_page(self, schema, values): """Creates a new page for each section in dialog. Parameters ---------- schema : dict Json schema including all information to build each page in the preferences dialog. values : dict Dictionary of current values set in preferences. """ widget = self.build_page_dialog(schema, values) self._list.addItem(schema["title"]) self._stack.addWidget(widget) def build_page_dialog(self, schema, values): """Builds the preferences widget using the json schema builder. Parameters ---------- schema : dict Json schema including all information to build each page in the preferences dialog. values : dict Dictionary of current values set in preferences. """ settings = get_settings() builder = WidgetBuilder() form = builder.create_form(schema, self.ui_schema) # Disable widgets that loaded settings from environment variables section = schema["section"] form_layout = form.widget.layout() for row in range(form.widget.layout().rowCount()): widget = form_layout.itemAt(row, form_layout.FieldRole).widget() name = widget._name disable = bool( settings._env_settings.get(section, {}).get(name, None)) widget.setDisabled(disable) try: widget.opacity.setOpacity(0.3 if disable else 1) except AttributeError: # some widgets may not have opacity (such as the QtPluginSorter) pass # set state values for widget form.widget.state = values if section == 'experimental': # need to disable async if octree is enabled. if values['octree'] is True: form = self._disable_async(form, values) form.widget.on_changed.connect(lambda d: self.check_differences( d, self._values_dict[schema["title"].lower()], )) return form def _disable_async(self, form, values, disable=True, state=True): """Disable async if octree is True.""" settings = get_settings() # need to make sure that if async_ is an environment setting, that we don't # enable it here. if (settings._env_settings['experimental'].get('async_', None) is not None): disable = True idx = list(values.keys()).index('async_') form_layout = form.widget.layout() widget = form_layout.itemAt(idx, form_layout.FieldRole).widget() widget.opacity.setOpacity(0.3 if disable else 1) widget.setDisabled(disable) return form def _values_changed(self, page, new_dict, old_dict): """Loops through each setting in a page to determine if it changed. Parameters ---------- new_dict : dict Dict that has the most recent changes by user. Each key is a setting value and each item is the value. old_dict : dict Dict wtih values set at the begining of preferences dialog session. """ for setting_name, value in new_dict.items(): if value != old_dict[setting_name]: self._setting_changed_dict[page][setting_name] = value elif (value == old_dict[setting_name] and setting_name in self._setting_changed_dict[page]): self._setting_changed_dict[page].pop(setting_name) def set_current_index(self, index: int): """ Set the current page on the preferences by index. Parameters ---------- index : int Index of page to set as current one. """ self._list.setCurrentRow(index) def check_differences(self, new_dict, old_dict): """Changes settings in settings manager with changes from dialog. Parameters ---------- new_dict : dict Dict that has the most recent changes by user. Each key is a setting parameter and each item is the value. old_dict : dict Dict wtih values set at the beginning of the preferences dialog session. """ settings = get_settings() page = self._list.currentItem().text().split(" ")[0].lower() self._values_changed(page, new_dict, old_dict) different_values = self._setting_changed_dict[page] if len(different_values) > 0: # change the values in settings for setting_name, value in different_values.items(): try: setattr(settings._settings[page], setting_name, value) self._values_dict[page] = new_dict if page == 'experimental': if setting_name == 'octree': # disable/enable async checkbox widget = self._stack.currentWidget() cstate = True if value is True else False self._disable_async(widget, new_dict, disable=cstate) # need to inform user that napari restart needed. self._restart_dialog() elif setting_name == 'async_': # need to inform user that napari restart needed. self._restart_dialog() except: # noqa: E722 continue
class VariableExplorer(QWidget, SpyderPluginMixin): """ Variable Explorer Plugin """ CONF_SECTION = 'variable_explorer' CONFIGWIDGET_CLASS = VariableExplorerConfigPage sig_option_changed = Signal(str, object) def __init__(self, parent): QWidget.__init__(self, parent) SpyderPluginMixin.__init__(self, parent) # Widgets self.stack = QStackedWidget(self) self.shellwidgets = {} # Layout layout = QVBoxLayout() layout.addWidget(self.stack) self.setLayout(layout) # Initialize plugin self.initialize_plugin() @staticmethod def get_settings(): """ Return Variable Explorer settings dictionary (i.e. namespace browser settings according to Spyder's configuration file) """ settings = {} # CONF.load_from_ini() # necessary only when called from another process for name in REMOTE_SETTINGS: settings[name] = CONF.get(VariableExplorer.CONF_SECTION, name) return settings # ----- Stack accesors ---------------------------------------------------- def set_current_widget(self, nsb): self.stack.setCurrentWidget(nsb) def current_widget(self): return self.stack.currentWidget() def count(self): return self.stack.count() def remove_widget(self, nsb): self.stack.removeWidget(nsb) def add_widget(self, nsb): self.stack.addWidget(nsb) # ----- Public API -------------------------------------------------------- def add_shellwidget(self, shellwidget): shellwidget_id = id(shellwidget) # Add shell only once: this method may be called two times in a row # by the External console plugin (dev. convenience) from spyder.widgets.externalshell import systemshell if isinstance(shellwidget, systemshell.ExternalSystemShell): return if shellwidget_id not in self.shellwidgets: nsb = NamespaceBrowser(self) nsb.set_shellwidget(shellwidget) nsb.setup(**VariableExplorer.get_settings()) nsb.sig_option_changed.connect(self.sig_option_changed.emit) self.add_widget(nsb) self.shellwidgets[shellwidget_id] = nsb self.set_shellwidget_from_id(shellwidget_id) return nsb def remove_shellwidget(self, shellwidget_id): # If shellwidget_id is not in self.shellwidgets, it simply means # that shell was not a Python-based console (it was a terminal) if shellwidget_id in self.shellwidgets: nsb = self.shellwidgets.pop(shellwidget_id) self.remove_widget(nsb) nsb.close() def set_shellwidget_from_id(self, shellwidget_id): if shellwidget_id in self.shellwidgets: nsb = self.shellwidgets[shellwidget_id] self.set_current_widget(nsb) if self.isvisible: nsb.visibility_changed(True) def import_data(self, fname): """Import data in current namespace""" if self.count(): nsb = self.current_widget() nsb.refresh_table() nsb.import_data(filenames=fname) if self.dockwidget and not self.ismaximized: self.dockwidget.setVisible(True) self.dockwidget.raise_() #------ SpyderPluginMixin API --------------------------------------------- def visibility_changed(self, enable): """DockWidget visibility has changed""" SpyderPluginMixin.visibility_changed(self, enable) for nsb in list(self.shellwidgets.values()): nsb.visibility_changed(enable and nsb is self.current_widget()) #------ SpyderPluginWidget API --------------------------------------------- def get_plugin_title(self): """Return widget title""" return _('Variable explorer') def get_plugin_icon(self): """Return plugin icon""" return ima.icon('dictedit') def get_focus_widget(self): """ Return the widget to give focus to when this plugin's dockwidget is raised on top-level """ return self.current_widget() def closing_plugin(self, cancelable=False): """Perform actions before parent main window is closed""" return True def refresh_plugin(self): """Refresh widget""" pass def get_plugin_actions(self): """Return a list of actions related to plugin""" return [] def register_plugin(self): """Register plugin in Spyder's main window""" self.main.extconsole.set_variableexplorer(self) self.main.add_dockwidget(self) def apply_plugin_settings(self, options): """Apply configuration file's plugin settings""" for nsb in list(self.shellwidgets.values()): nsb.setup(**VariableExplorer.get_settings()) ar_timeout = self.get_option('autorefresh/timeout') for shellwidget in self.main.extconsole.shellwidgets: shellwidget.set_autorefresh_timeout(ar_timeout)
class VariableExplorer(QWidget, SpyderPluginMixin): """ Variable Explorer Plugin """ CONF_SECTION = 'variable_explorer' CONFIGWIDGET_CLASS = VariableExplorerConfigPage sig_option_changed = Signal(str, object) def __init__(self, parent): QWidget.__init__(self, parent) SpyderPluginMixin.__init__(self, parent) # Widgets self.stack = QStackedWidget(self) self.shellwidgets = {} # Layout layout = QVBoxLayout() layout.addWidget(self.stack) self.setLayout(layout) # Initialize plugin self.initialize_plugin() @staticmethod def get_settings(): """ Return Variable Explorer settings dictionary (i.e. namespace browser settings according to Spyder's configuration file) """ settings = {} # CONF.load_from_ini() # necessary only when called from another process for name in REMOTE_SETTINGS: settings[name] = CONF.get(VariableExplorer.CONF_SECTION, name) return settings # ----- Stack accesors ---------------------------------------------------- def set_current_widget(self, nsb): self.stack.setCurrentWidget(nsb) def current_widget(self): return self.stack.currentWidget() def count(self): return self.stack.count() def remove_widget(self, nsb): self.stack.removeWidget(nsb) def add_widget(self, nsb): self.stack.addWidget(nsb) # ----- Public API -------------------------------------------------------- def add_shellwidget(self, shellwidget): shellwidget_id = id(shellwidget) # Add shell only once: this method may be called two times in a row # by the External console plugin (dev. convenience) from spyderlib.widgets.externalshell import systemshell if isinstance(shellwidget, systemshell.ExternalSystemShell): return if shellwidget_id not in self.shellwidgets: nsb = NamespaceBrowser(self) nsb.set_shellwidget(shellwidget) nsb.setup(**VariableExplorer.get_settings()) nsb.sig_option_changed.connect(self.sig_option_changed.emit) self.add_widget(nsb) self.shellwidgets[shellwidget_id] = nsb self.set_shellwidget_from_id(shellwidget_id) return nsb def remove_shellwidget(self, shellwidget_id): # If shellwidget_id is not in self.shellwidgets, it simply means # that shell was not a Python-based console (it was a terminal) if shellwidget_id in self.shellwidgets: nsb = self.shellwidgets.pop(shellwidget_id) self.remove_widget(nsb) nsb.close() def set_shellwidget_from_id(self, shellwidget_id): if shellwidget_id in self.shellwidgets: nsb = self.shellwidgets[shellwidget_id] self.set_current_widget(nsb) if self.isvisible: nsb.visibility_changed(True) def import_data(self, fname): """Import data in current namespace""" if self.count(): nsb = self.current_widget() nsb.refresh_table() nsb.import_data(filename=fname) if self.dockwidget and not self.ismaximized: self.dockwidget.setVisible(True) self.dockwidget.raise_() #------ SpyderPluginMixin API --------------------------------------------- def visibility_changed(self, enable): """DockWidget visibility has changed""" SpyderPluginMixin.visibility_changed(self, enable) for nsb in list(self.shellwidgets.values()): nsb.visibility_changed(enable and nsb is self.current_widget()) #------ SpyderPluginWidget API --------------------------------------------- def get_plugin_title(self): """Return widget title""" return _('Variable explorer') def get_plugin_icon(self): """Return plugin icon""" return ima.icon('dictedit') def get_focus_widget(self): """ Return the widget to give focus to when this plugin's dockwidget is raised on top-level """ return self.current_widget() def closing_plugin(self, cancelable=False): """Perform actions before parent main window is closed""" return True def refresh_plugin(self): """Refresh widget""" pass def get_plugin_actions(self): """Return a list of actions related to plugin""" return [] def register_plugin(self): """Register plugin in Spyder's main window""" self.main.extconsole.set_variableexplorer(self) self.main.add_dockwidget(self) def apply_plugin_settings(self, options): """Apply configuration file's plugin settings""" for nsb in list(self.shellwidgets.values()): nsb.setup(**VariableExplorer.get_settings()) ar_timeout = self.get_option('autorefresh/timeout') for shellwidget in self.main.extconsole.shellwidgets: shellwidget.set_autorefresh_timeout(ar_timeout)
class VariableExplorer(QWidget, SpyderPluginMixin): """ Variable Explorer Plugin """ CONF_SECTION = 'variable_explorer' CONFIGWIDGET_CLASS = VariableExplorerConfigPage sig_option_changed = Signal(str, object) def __init__(self, parent): QWidget.__init__(self, parent) SpyderPluginMixin.__init__(self, parent) # Widgets self.stack = QStackedWidget(self) self.shellwidgets = {} # Layout layout = QVBoxLayout() layout.addWidget(self.stack) self.setLayout(layout) # Initialize plugin self.initialize_plugin() def get_settings(self): """ Retrieve all Variable Explorer configuration settings. Specifically, return the settings in CONF_SECTION with keys in REMOTE_SETTINGS, and the setting 'dataframe_format'. Returns: dict: settings """ settings = {} for name in REMOTE_SETTINGS: settings[name] = self.get_option(name) # dataframe_format is stored without percent sign in config # to avoid interference with ConfigParser's interpolation name = 'dataframe_format' settings[name] = '%{0}'.format(self.get_option(name)) return settings @Slot(str, object) def change_option(self, option_name, new_value): """ Change a config option. This function is called if sig_option_changed is received. If the option changed is the dataframe format, then the leading '%' character is stripped (because it can't be stored in the user config). Then, the signal is emitted again, so that the new value is saved in the user config. """ if option_name == 'dataframe_format': assert new_value.startswith('%') new_value = new_value[1:] self.sig_option_changed.emit(option_name, new_value) # ----- Stack accesors ---------------------------------------------------- def set_current_widget(self, nsb): self.stack.setCurrentWidget(nsb) def current_widget(self): return self.stack.currentWidget() def count(self): return self.stack.count() def remove_widget(self, nsb): self.stack.removeWidget(nsb) def add_widget(self, nsb): self.stack.addWidget(nsb) # ----- Public API -------------------------------------------------------- def add_shellwidget(self, shellwidget): """ Register shell with variable explorer. This function opens a new NamespaceBrowser for browsing the variables in the shell. """ shellwidget_id = id(shellwidget) if shellwidget_id not in self.shellwidgets: nsb = NamespaceBrowser(self) nsb.set_shellwidget(shellwidget) nsb.setup(**self.get_settings()) nsb.sig_option_changed.connect(self.change_option) self.add_widget(nsb) self.shellwidgets[shellwidget_id] = nsb self.set_shellwidget_from_id(shellwidget_id) return nsb def remove_shellwidget(self, shellwidget_id): # If shellwidget_id is not in self.shellwidgets, it simply means # that shell was not a Python-based console (it was a terminal) if shellwidget_id in self.shellwidgets: nsb = self.shellwidgets.pop(shellwidget_id) self.remove_widget(nsb) nsb.close() def set_shellwidget_from_id(self, shellwidget_id): if shellwidget_id in self.shellwidgets: nsb = self.shellwidgets[shellwidget_id] self.set_current_widget(nsb) def import_data(self, fname): """Import data in current namespace""" if self.count(): nsb = self.current_widget() nsb.refresh_table() nsb.import_data(filenames=fname) if self.dockwidget and not self.ismaximized: self.dockwidget.setVisible(True) self.dockwidget.raise_() #------ SpyderPluginWidget API --------------------------------------------- def get_plugin_title(self): """Return widget title""" return _('Variable explorer') def get_plugin_icon(self): """Return plugin icon""" return ima.icon('dictedit') def get_focus_widget(self): """ Return the widget to give focus to when this plugin's dockwidget is raised on top-level """ return self.current_widget() def closing_plugin(self, cancelable=False): """Perform actions before parent main window is closed""" return True def refresh_plugin(self): """Refresh widget""" pass def get_plugin_actions(self): """Return a list of actions related to plugin""" return [] def register_plugin(self): """Register plugin in Spyder's main window""" self.main.add_dockwidget(self) def apply_plugin_settings(self, options): """Apply configuration file's plugin settings""" for nsb in list(self.shellwidgets.values()): nsb.setup(**self.get_settings())
class ConfigDialog(QDialog): """Spyder configuration ('Preferences') dialog box""" # Signals check_settings = Signal() size_change = Signal(QSize) def __init__(self, parent=None): QDialog.__init__(self, parent) self.main = parent # Widgets self.pages_widget = QStackedWidget() self.pages_widget.setMinimumWidth(600) self.contents_widget = QListWidget() self.button_reset = QPushButton(_('Reset to defaults')) bbox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Apply | QDialogButtonBox.Cancel) self.apply_btn = bbox.button(QDialogButtonBox.Apply) self.ok_btn = bbox.button(QDialogButtonBox.Ok) # Widgets setup # Destroying the C++ object right after closing the dialog box, # otherwise it may be garbage-collected in another QThread # (e.g. the editor's analysis thread in Spyder), thus leading to # a segmentation fault on UNIX or an application crash on Windows self.setAttribute(Qt.WA_DeleteOnClose) self.setWindowTitle(_('Preferences')) self.setWindowIcon(ima.icon('configure')) self.contents_widget.setMovement(QListView.Static) self.contents_widget.setSpacing(1) self.contents_widget.setCurrentRow(0) self.contents_widget.setMinimumWidth(220) self.contents_widget.setMinimumHeight(400) # Layout hsplitter = QSplitter() hsplitter.addWidget(self.contents_widget) hsplitter.addWidget(self.pages_widget) hsplitter.setStretchFactor(0, 1) hsplitter.setStretchFactor(1, 2) btnlayout = QHBoxLayout() btnlayout.addWidget(self.button_reset) btnlayout.addStretch(1) btnlayout.addWidget(bbox) vlayout = QVBoxLayout() vlayout.addWidget(hsplitter) vlayout.addLayout(btnlayout) self.setLayout(vlayout) # Signals and slots if self.main: self.button_reset.clicked.connect(self.main.reset_spyder) self.pages_widget.currentChanged.connect(self.current_page_changed) self.contents_widget.currentRowChanged.connect( self.pages_widget.setCurrentIndex) bbox.accepted.connect(self.accept) bbox.rejected.connect(self.reject) bbox.clicked.connect(self.button_clicked) # Ensures that the config is present on spyder first run CONF.set('main', 'interface_language', load_lang_conf()) def get_current_index(self): """Return current page index""" return self.contents_widget.currentRow() def set_current_index(self, index): """Set current page index""" self.contents_widget.setCurrentRow(index) def get_page(self, index=None): """Return page widget""" if index is None: widget = self.pages_widget.currentWidget() else: widget = self.pages_widget.widget(index) if widget: return widget.widget() def get_index_by_name(self, name): """Return page index by CONF_SECTION name.""" for idx in range(self.pages_widget.count()): widget = self.pages_widget.widget(idx) widget = widget.widget() if widget.CONF_SECTION == name: return idx else: return None @Slot() def accept(self): """Reimplement Qt method""" for index in range(self.pages_widget.count()): configpage = self.get_page(index) if not configpage.is_valid(): return configpage.apply_changes() QDialog.accept(self) def button_clicked(self, button): if button is self.apply_btn: # Apply button was clicked configpage = self.get_page() if not configpage.is_valid(): return configpage.apply_changes() def current_page_changed(self, index): widget = self.get_page(index) self.apply_btn.setVisible(widget.apply_callback is not None) self.apply_btn.setEnabled(widget.is_modified) def add_page(self, widget): self.check_settings.connect(widget.check_settings) widget.show_this_page.connect(lambda row=self.contents_widget.count(): self.contents_widget.setCurrentRow(row)) widget.apply_button_enabled.connect(self.apply_btn.setEnabled) scrollarea = QScrollArea(self) scrollarea.setWidgetResizable(True) scrollarea.setWidget(widget) self.pages_widget.addWidget(scrollarea) item = QListWidgetItem(self.contents_widget) try: item.setIcon(widget.get_icon()) except TypeError: pass item.setText(widget.get_name()) item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) item.setSizeHint(QSize(0, 25)) def check_all_settings(self): """This method is called to check all configuration page settings after configuration dialog has been shown""" self.check_settings.emit() def resizeEvent(self, event): """ Reimplement Qt method to be able to save the widget's size from the main application """ QDialog.resizeEvent(self, event) self.size_change.emit(self.size())
class ShellConnectMainWidget(PluginMainWidget): """ Main widget to use in a plugin that shows console-specific content. Notes ----- * This is composed of a QStackedWidget to stack widgets associated to each shell widget in the console and only show one of them at a time. * The current widget in the stack will display the content associated to the console with focus. """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Widgets self._stack = QStackedWidget(self) self._shellwidgets = {} # Layout layout = QVBoxLayout() layout.setSpacing(0) layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self._stack) self.setLayout(layout) def update_style(self): self._stack.setStyleSheet("QStackedWidget {padding: 0px; border: 0px}") # ---- Stack accesors # ------------------------------------------------------------------------ def count(self): """ Return the number of widgets in the stack. Returns ------- int The number of widgets in the stack. """ return self._stack.count() def current_widget(self): """ Return the current figure browser widget in the stack. Returns ------- QWidget The current widget. """ return self._stack.currentWidget() def get_focus_widget(self): return self.current_widget() # ---- Public API # ------------------------------------------------------------------------ def add_shellwidget(self, shellwidget): """ Create a new widget in the stack and associate it to shellwidget. """ shellwidget_id = id(shellwidget) if shellwidget_id not in self._shellwidgets: widget = self.create_new_widget(shellwidget) self._stack.addWidget(widget) self._shellwidgets[shellwidget_id] = widget # Add all actions to new widget for shortcuts to work. for __, action in self.get_actions().items(): if action: widget_actions = widget.actions() if action not in widget_actions: widget.addAction(action) self.set_shellwidget(shellwidget) def remove_shellwidget(self, shellwidget): """Remove widget associated to shellwidget.""" shellwidget_id = id(shellwidget) if shellwidget_id in self._shellwidgets: widget = self._shellwidgets.pop(shellwidget_id) self._stack.removeWidget(widget) self.close_widget(widget) self.update_actions() def set_shellwidget(self, shellwidget): """ Set widget associated with shellwidget as the current widget. """ shellwidget_id = id(shellwidget) old_widget = self.current_widget() if shellwidget_id in self._shellwidgets: widget = self._shellwidgets[shellwidget_id] self._stack.setCurrentWidget(widget) self.switch_widget(widget, old_widget) self.update_actions() def create_new_widget(self, shellwidget): """Create a widget to communicate with shellwidget.""" raise NotImplementedError def close_widget(self, widget): """Close the widget.""" raise NotImplementedError def switch_widget(self, widget, old_widget): """Switch the current widget.""" raise NotImplementedError def refresh(self): """Refresh widgets.""" if self.count(): widget = self.current_widget() widget.refresh()