class SnippetEditor(QDialog): SNIPPET_VALID = _('Valid snippet') SNIPPET_INVALID = _('Invalid snippet') INVALID_CB_CSS = "QComboBox {border: 1px solid red;}" VALID_CB_CSS = "QComboBox {border: 1px solid green;}" INVALID_LINE_CSS = "QLineEdit {border: 1px solid red;}" VALID_LINE_CSS = "QLineEdit {border: 1px solid green;}" MIN_SIZE = QSize(850, 600) def __init__(self, parent, language=None, trigger_text='', description='', snippet_text='', remove_trigger=False, trigger_texts=[], descriptions=[], get_option=None, set_option=None): super(SnippetEditor, self).__init__(parent) snippet_description = _( "To add a new text snippet, you need to define the text " "that triggers it, a short description (two words maximum) " "of the snippet and if it should delete the trigger text when " "inserted. Finally, you need to define the snippet body to insert." ) self.parent = parent self.trigger_text = trigger_text self.description = description self.remove_trigger = remove_trigger self.snippet_text = snippet_text self.descriptions = descriptions self.base_snippet = Snippet(language=language, trigger_text=trigger_text, snippet_text=snippet_text, description=description, remove_trigger=remove_trigger, get_option=get_option, set_option=set_option) # Widgets self.snippet_settings_description = QLabel(snippet_description) self.snippet_settings_description.setFixedWidth(450) # Trigger text self.trigger_text_label = QLabel(_('Trigger text:')) self.trigger_text_cb = QComboBox(self) self.trigger_text_cb.setEditable(True) # Description self.description_label = QLabel(_('Description:')) self.description_input = QLineEdit(self) # Remove trigger self.remove_trigger_cb = QCheckBox( _('Remove trigger text on insertion'), self) self.remove_trigger_cb.setToolTip( _('Check if the text that triggers ' 'this snippet should be removed ' 'when inserting it')) self.remove_trigger_cb.setChecked(self.remove_trigger) # Snippet body input self.snippet_label = QLabel(_('<b>Snippet text:</b>')) self.snippet_valid_label = QLabel(self.SNIPPET_INVALID, self) self.snippet_input = SimpleCodeEditor(None) # Dialog buttons self.bbox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) self.button_ok = self.bbox.button(QDialogButtonBox.Ok) self.button_cancel = self.bbox.button(QDialogButtonBox.Cancel) # Widget setup self.setWindowTitle(_('Snippet editor')) self.snippet_settings_description.setWordWrap(True) self.trigger_text_cb.setToolTip( _('Trigger text for the current snippet')) self.trigger_text_cb.addItems(trigger_texts) if self.trigger_text != '': idx = trigger_texts.index(self.trigger_text) self.trigger_text_cb.setCurrentIndex(idx) self.description_input.setText(self.description) self.description_input.textChanged.connect(lambda _x: self.validate()) text_inputs = (self.trigger_text, self.description, self.snippet_text) non_empty_text = all([x != '' for x in text_inputs]) if non_empty_text: self.button_ok.setEnabled(True) self.snippet_input.setup_editor(language=language, color_scheme=get_option( 'selected', section='appearance'), wrap=False, highlight_current_line=True, font=get_font()) self.snippet_input.set_language(language) self.snippet_input.setToolTip(_('Snippet text completion to insert')) self.snippet_input.set_text(snippet_text) # Layout setup general_layout = QVBoxLayout() general_layout.addWidget(self.snippet_settings_description) snippet_settings_group = QGroupBox(_('Trigger information')) settings_layout = QGridLayout() settings_layout.addWidget(self.trigger_text_label, 0, 0) settings_layout.addWidget(self.trigger_text_cb, 0, 1) settings_layout.addWidget(self.description_label, 1, 0) settings_layout.addWidget(self.description_input, 1, 1) all_settings_layout = QVBoxLayout() all_settings_layout.addLayout(settings_layout) all_settings_layout.addWidget(self.remove_trigger_cb) snippet_settings_group.setLayout(all_settings_layout) general_layout.addWidget(snippet_settings_group) text_layout = QVBoxLayout() text_layout.addWidget(self.snippet_label) text_layout.addWidget(self.snippet_input) text_layout.addWidget(self.snippet_valid_label) general_layout.addLayout(text_layout) general_layout.addWidget(self.bbox) self.setLayout(general_layout) # Signals self.trigger_text_cb.editTextChanged.connect(self.validate) self.description_input.textChanged.connect(self.validate) self.snippet_input.textChanged.connect(self.validate) self.bbox.accepted.connect(self.accept) self.bbox.rejected.connect(self.reject) # Final setup if trigger_text != '' or snippet_text != '': self.validate() @Slot() def validate(self): trigger_text = self.trigger_text_cb.currentText() description_text = self.description_input.text() snippet_text = self.snippet_input.toPlainText() invalid = False try: build_snippet_ast(snippet_text) self.snippet_valid_label.setText(self.SNIPPET_VALID) except SyntaxError: invalid = True self.snippet_valid_label.setText(self.SNIPPET_INVALID) if trigger_text == '': invalid = True self.trigger_text_cb.setStyleSheet(self.INVALID_CB_CSS) else: self.trigger_text_cb.setStyleSheet(self.VALID_CB_CSS) if trigger_text in self.descriptions: if self.trigger_text != trigger_text: if description_text in self.descriptions[trigger_text]: invalid = True self.description_input.setStyleSheet(self.INVALID_LINE_CSS) else: self.description_input.setStyleSheet(self.VALID_LINE_CSS) else: if description_text != self.description: if description_text in self.descriptions[trigger_text]: invalid = True self.description_input.setStyleSheet( self.INVALID_LINE_CSS) else: self.description_input.setStyleSheet( self.VALID_LINE_CSS) else: self.description_input.setStyleSheet(self.VALID_LINE_CSS) self.button_ok.setEnabled(not invalid) def get_options(self): trigger_text = self.trigger_text_cb.currentText() description_text = self.description_input.text() snippet_text = self.snippet_input.toPlainText() remove_trigger = self.remove_trigger_cb.isChecked() self.base_snippet.update(trigger_text, description_text, snippet_text, remove_trigger) return self.base_snippet
class ObjectExplorer(BaseDialog, SpyderConfigurationAccessor): """Object explorer main widget window.""" CONF_SECTION = 'variable_explorer' def __init__(self, obj, name='', expanded=False, resize_to_contents=True, parent=None, attribute_columns=DEFAULT_ATTR_COLS, attribute_details=DEFAULT_ATTR_DETAILS, readonly=None, reset=False): """ Constructor :param name: name of the object as it will appear in the root node :param expanded: show the first visible root element expanded :param resize_to_contents: resize columns to contents ignoring width of the attributes :param obj: any Python object or variable :param attribute_columns: list of AttributeColumn objects that define which columns are present in the table and their defaults :param attribute_details: list of AttributeDetails objects that define which attributes can be selected in the details pane. :param reset: If true the persistent settings, such as column widths, are reset. """ QDialog.__init__(self, parent=parent) self.setAttribute(Qt.WA_DeleteOnClose) # Options show_callable_attributes = self.get_conf('show_callable_attributes') show_special_attributes = self.get_conf('show_special_attributes') # Model self._attr_cols = attribute_columns self._attr_details = attribute_details self.readonly = readonly self.btn_save_and_close = None self.btn_close = None self._tree_model = TreeModel(obj, obj_name=name, attr_cols=self._attr_cols) self._proxy_tree_model = TreeProxyModel( show_callable_attributes=show_callable_attributes, show_special_attributes=show_special_attributes) self._proxy_tree_model.setSourceModel(self._tree_model) # self._proxy_tree_model.setSortRole(RegistryTableModel.SORT_ROLE) self._proxy_tree_model.setDynamicSortFilter(True) # self._proxy_tree_model.setSortCaseSensitivity(Qt.CaseInsensitive) # Tree widget self.obj_tree = ToggleColumnTreeView() self.obj_tree.setAlternatingRowColors(True) self.obj_tree.setModel(self._proxy_tree_model) self.obj_tree.setSelectionBehavior(QAbstractItemView.SelectRows) self.obj_tree.setUniformRowHeights(True) self.obj_tree.add_header_context_menu() # Views self._setup_actions() self._setup_menu(show_callable_attributes=show_callable_attributes, show_special_attributes=show_special_attributes) self._setup_views() if name: name = "{} -".format(name) self.setWindowTitle("{} {}".format(name, EDITOR_NAME)) self.setWindowFlags(Qt.Window) self._resize_to_contents = resize_to_contents self._readViewSettings(reset=reset) # Update views with model self.toggle_show_special_attribute_action.setChecked( show_special_attributes) self.toggle_show_callable_action.setChecked(show_callable_attributes) # Select first row so that a hidden root node will not be selected. first_row_index = self._proxy_tree_model.firstItemIndex() self.obj_tree.setCurrentIndex(first_row_index) if self._tree_model.inspectedNodeIsVisible or expanded: self.obj_tree.expand(first_row_index) def get_value(self): """Get editor current object state.""" return self._tree_model.inspectedItem.obj def _make_show_column_function(self, column_idx): """Creates a function that shows or hides a column.""" show_column = lambda checked: self.obj_tree.setColumnHidden( column_idx, not checked) return show_column def _setup_actions(self): """Creates the main window actions.""" # Show/hide callable objects self.toggle_show_callable_action = QAction( _("Show callable attributes"), self, checkable=True, shortcut=QKeySequence("Alt+C"), statusTip=_("Shows/hides attributes that are callable " "(functions, methods, etc)")) self.toggle_show_callable_action.toggled.connect( self._proxy_tree_model.setShowCallables) self.toggle_show_callable_action.toggled.connect( self.obj_tree.resize_columns_to_contents) # Show/hide special attributes self.toggle_show_special_attribute_action = QAction( _("Show __special__ attributes"), self, checkable=True, shortcut=QKeySequence("Alt+S"), statusTip=_("Shows or hides __special__ attributes")) self.toggle_show_special_attribute_action.toggled.connect( self._proxy_tree_model.setShowSpecialAttributes) self.toggle_show_special_attribute_action.toggled.connect( self.obj_tree.resize_columns_to_contents) def _setup_menu(self, show_callable_attributes=False, show_special_attributes=False): """Sets up the main menu.""" self.tools_layout = QHBoxLayout() callable_attributes = create_toolbutton( self, text=_("Show callable attributes"), icon=ima.icon("class"), toggled=self._toggle_show_callable_attributes_action) callable_attributes.setCheckable(True) callable_attributes.setChecked(show_callable_attributes) self.tools_layout.addWidget(callable_attributes) special_attributes = create_toolbutton( self, text=_("Show __special__ attributes"), icon=ima.icon("private2"), toggled=self._toggle_show_special_attributes_action) special_attributes.setCheckable(True) special_attributes.setChecked(show_special_attributes) self.tools_layout.addWidget(special_attributes) self.tools_layout.addStretch() self.options_button = create_toolbutton(self, text=_('Options'), icon=ima.icon('tooloptions')) self.options_button.setPopupMode(QToolButton.InstantPopup) self.show_cols_submenu = QMenu(self) self.options_button.setMenu(self.show_cols_submenu) # Don't show menu arrow and remove padding if is_dark_interface(): self.options_button.setStyleSheet( ("QToolButton::menu-indicator{image: none;}\n" "QToolButton{padding: 3px;}")) else: self.options_button.setStyleSheet( "QToolButton::menu-indicator{image: none;}") self.tools_layout.addWidget(self.options_button) @Slot() def _toggle_show_callable_attributes_action(self): """Toggle show callable atributes action.""" action_checked = not self.toggle_show_callable_action.isChecked() self.toggle_show_callable_action.setChecked(action_checked) self.set_conf('show_callable_attributes', action_checked) @Slot() def _toggle_show_special_attributes_action(self): """Toggle show special attributes action.""" action_checked = ( not self.toggle_show_special_attribute_action.isChecked()) self.toggle_show_special_attribute_action.setChecked(action_checked) self.set_conf('show_special_attributes', action_checked) def _setup_views(self): """Creates the UI widgets.""" self.central_splitter = QSplitter(self, orientation=Qt.Vertical) layout = create_plugin_layout(self.tools_layout, self.central_splitter) self.setLayout(layout) # Stretch last column? # It doesn't play nice when columns are hidden and then shown again. obj_tree_header = self.obj_tree.header() obj_tree_header.setSectionsMovable(True) obj_tree_header.setStretchLastSection(False) add_actions(self.show_cols_submenu, self.obj_tree.toggle_column_actions_group.actions()) self.central_splitter.addWidget(self.obj_tree) # Bottom pane bottom_pane_widget = QWidget() bottom_layout = QHBoxLayout() bottom_layout.setSpacing(0) bottom_layout.setContentsMargins(5, 5, 5, 5) # left top right bottom bottom_pane_widget.setLayout(bottom_layout) self.central_splitter.addWidget(bottom_pane_widget) group_box = QGroupBox(_("Details")) bottom_layout.addWidget(group_box) v_group_layout = QVBoxLayout() h_group_layout = QHBoxLayout() h_group_layout.setContentsMargins(2, 2, 2, 2) # left top right bottom group_box.setLayout(v_group_layout) v_group_layout.addLayout(h_group_layout) # Radio buttons radio_widget = QWidget() radio_layout = QVBoxLayout() radio_layout.setContentsMargins(0, 0, 0, 0) # left top right bottom radio_widget.setLayout(radio_layout) self.button_group = QButtonGroup(self) for button_id, attr_detail in enumerate(self._attr_details): radio_button = QRadioButton(attr_detail.name) radio_layout.addWidget(radio_button) self.button_group.addButton(radio_button, button_id) self.button_group.buttonClicked[int].connect( self._change_details_field) self.button_group.button(0).setChecked(True) radio_layout.addStretch(1) h_group_layout.addWidget(radio_widget) # Editor widget self.editor = SimpleCodeEditor(self) self.editor.setReadOnly(True) h_group_layout.addWidget(self.editor) # Save and close buttons btn_layout = QHBoxLayout() btn_layout.addStretch() if not self.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) v_group_layout.addLayout(btn_layout) # Splitter parameters self.central_splitter.setCollapsible(0, False) self.central_splitter.setCollapsible(1, True) self.central_splitter.setSizes([500, 320]) # Connect signals # Keep a temporary reference of the selection_model to prevent # segfault in PySide. # See http://permalink.gmane.org/gmane.comp.lib.qt.pyside.devel/222 selection_model = self.obj_tree.selectionModel() selection_model.currentChanged.connect(self._update_details) # Check if the values of the model have been changed self._proxy_tree_model.sig_setting_data.connect( self.save_and_close_enable) self._proxy_tree_model.sig_update_details.connect( self._update_details_for_item) # End of setup_methods def _readViewSettings(self, reset=False): """ Reads the persistent program settings. :param reset: If True, the program resets to its default settings. """ pos = QPoint(20, 20) window_size = QSize(825, 650) details_button_idx = 0 header = self.obj_tree.header() header_restored = False if reset: logger.debug("Resetting persistent view settings") else: pos = pos window_size = window_size details_button_idx = details_button_idx # splitter_state = settings.value("central_splitter/state") splitter_state = None if splitter_state: self.central_splitter.restoreState(splitter_state) # header_restored = self.obj_tree.read_view_settings( # 'table/header_state', # settings, reset) header_restored = False if not header_restored: column_sizes = [col.width for col in self._attr_cols] column_visible = [col.col_visible for col in self._attr_cols] for idx, size in enumerate(column_sizes): if not self._resize_to_contents and size > 0: # Just in case header.resizeSection(idx, size) else: header.resizeSections(QHeaderView.ResizeToContents) break for idx, visible in enumerate(column_visible): elem = self.obj_tree.toggle_column_actions_group.actions()[idx] elem.setChecked(visible) self.resize(window_size) button = self.button_group.button(details_button_idx) if button is not None: button.setChecked(True) @Slot() def save_and_close_enable(self): """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) @Slot(QModelIndex, QModelIndex) def _update_details(self, current_index, _previous_index): """Shows the object details in the editor given an index.""" tree_item = self._proxy_tree_model.treeItem(current_index) self._update_details_for_item(tree_item) def _change_details_field(self, _button_id=None): """Changes the field that is displayed in the details pane.""" # logger.debug("_change_details_field: {}".format(_button_id)) current_index = self.obj_tree.selectionModel().currentIndex() tree_item = self._proxy_tree_model.treeItem(current_index) self._update_details_for_item(tree_item) @Slot(TreeItem) def _update_details_for_item(self, tree_item): """Shows the object details in the editor given an tree_item.""" try: # obj = tree_item.obj button_id = self.button_group.checkedId() assert button_id >= 0, ("No radio button selected. " "Please report this bug.") attr_details = self._attr_details[button_id] data = attr_details.data_fn(tree_item) self.editor.setPlainText(data) self.editor.setWordWrapMode(attr_details.line_wrap) self.editor.setup_editor( font=get_font(font_size_delta=DEFAULT_SMALL_DELTA), show_blanks=False, color_scheme=CONF.get('appearance', 'selected'), scroll_past_end=False, ) self.editor.set_text(data) if attr_details.name == 'Source code': self.editor.set_language('Python') else: self.editor.set_language('Rst') except Exception as ex: self.editor.setStyleSheet("color: red;") stack_trace = traceback.format_exc() self.editor.setPlainText("{}\n\n{}".format(ex, stack_trace)) self.editor.setWordWrapMode( QTextOption.WrapAtWordBoundaryOrAnywhere) @classmethod def create_explorer(cls, *args, **kwargs): """ Creates and shows and ObjectExplorer window. The *args and **kwargs will be passed to the ObjectExplorer constructor A (class attribute) reference to the browser window is kept to prevent it from being garbage-collected. """ object_explorer = cls(*args, **kwargs) object_explorer.exec_() return object_explorer
class PlainText(QWidget): """ Read-only editor widget with find dialog """ # Signals focus_changed = Signal() sig_custom_context_menu_requested = Signal(QPoint) def __init__(self, parent): QWidget.__init__(self, parent) self.editor = None # Read-only simple code editor self.editor = SimpleCodeEditor(self) self.editor.setup_editor( language='py', highlight_current_line=False, linenumbers=False, ) self.editor.sig_focus_changed.connect(self.focus_changed) self.editor.setReadOnly(True) self.editor.setContextMenuPolicy(Qt.CustomContextMenu) # Find/replace widget self.find_widget = FindReplace(self) self.find_widget.set_editor(self.editor) self.find_widget.hide() layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self.editor) layout.addWidget(self.find_widget) self.setLayout(layout) self.editor.customContextMenuRequested.connect( self.sig_custom_context_menu_requested) def set_font(self, font, color_scheme=None): """Set font""" self.editor.set_color_scheme(color_scheme) self.editor.set_font(font) def set_color_scheme(self, color_scheme): """Set color scheme""" self.editor.set_color_scheme(color_scheme) def set_text(self, text, is_code): if is_code: self.editor.set_language('py') else: self.editor.set_language(None) self.editor.set_text(text) self.editor.set_cursor_position('sof') def clear(self): self.editor.clear() def set_wrap_mode(self, value): self.editor.toggle_wrap_mode(value) def copy(self): self.editor.copy() def select_all(self): self.editor.selectAll()
class AppearanceConfigPage(PluginConfigPage): APPLY_CONF_PAGE_SETTINGS = True def setup_page(self): names = self.get_option("names") try: names.pop(names.index(u'Custom')) except ValueError: pass custom_names = self.get_option("custom_names", []) # Interface options theme_group = QGroupBox(_("Main interface")) # Interface Widgets ui_themes = ['Automatic', 'Light', 'Dark'] ui_theme_choices = list(zip(ui_themes, [ui_theme.lower() for ui_theme in ui_themes])) ui_theme_combo = self.create_combobox(_('Interface theme'), ui_theme_choices, 'ui_theme', restart=True) themes = ['Spyder 2', 'Spyder 3'] icon_choices = list(zip(themes, [theme.lower() for theme in themes])) icons_combo = self.create_combobox(_('Icon theme'), icon_choices, 'icon_theme', restart=True) theme_comboboxes_layout = QGridLayout() theme_comboboxes_layout.addWidget(ui_theme_combo.label, 0, 0) theme_comboboxes_layout.addWidget(ui_theme_combo.combobox, 0, 1) theme_comboboxes_layout.addWidget(icons_combo.label, 1, 0) theme_comboboxes_layout.addWidget(icons_combo.combobox, 1, 1) theme_layout = QVBoxLayout() theme_layout.addLayout(theme_comboboxes_layout) theme_group.setLayout(theme_layout) # Syntax coloring options syntax_group = QGroupBox(_("Syntax highlighting theme")) # Syntax Widgets edit_button = QPushButton(_("Edit selected scheme")) create_button = QPushButton(_("Create new scheme")) self.delete_button = QPushButton(_("Delete scheme")) self.reset_button = QPushButton(_("Reset to defaults")) self.preview_editor = SimpleCodeEditor(self) self.stacked_widget = QStackedWidget(self) self.scheme_editor_dialog = SchemeEditor(parent=self, stack=self.stacked_widget) self.scheme_choices_dict = {} schemes_combobox_widget = self.create_combobox('', [('', '')], 'selected') self.schemes_combobox = schemes_combobox_widget.combobox # Syntax layout syntax_layout = QGridLayout(syntax_group) btns = [self.schemes_combobox, edit_button, self.reset_button, create_button, self.delete_button] for i, btn in enumerate(btns): syntax_layout.addWidget(btn, i, 1) syntax_layout.setColumnStretch(0, 1) syntax_layout.setColumnStretch(1, 2) syntax_layout.setColumnStretch(2, 1) syntax_layout.setContentsMargins(0, 12, 0, 12) # Fonts options fonts_group = QGroupBox(_("Fonts")) # Fonts widgets self.plain_text_font = self.create_fontgroup( option='font', title=_("Plain text"), fontfilters=QFontComboBox.MonospacedFonts, without_group=True) self.rich_text_font = self.create_fontgroup( option='rich_font', title=_("Rich text"), without_group=True) # Fonts layouts fonts_layout = QGridLayout(fonts_group) fonts_layout.addWidget(self.plain_text_font.fontlabel, 0, 0) fonts_layout.addWidget(self.plain_text_font.fontbox, 0, 1) fonts_layout.addWidget(self.plain_text_font.sizelabel, 0, 2) fonts_layout.addWidget(self.plain_text_font.sizebox, 0, 3) fonts_layout.addWidget(self.rich_text_font.fontlabel, 1, 0) fonts_layout.addWidget(self.rich_text_font.fontbox, 1, 1) fonts_layout.addWidget(self.rich_text_font.sizelabel, 1, 2) fonts_layout.addWidget(self.rich_text_font.sizebox, 1, 3) fonts_layout.setRowStretch(fonts_layout.rowCount(), 1) # Left options layout options_layout = QVBoxLayout() options_layout.addWidget(theme_group) options_layout.addWidget(syntax_group) options_layout.addWidget(fonts_group) # Right preview layout preview_group = QGroupBox(_("Preview")) preview_layout = QVBoxLayout() preview_layout.addWidget(self.preview_editor) preview_group.setLayout(preview_layout) # Combined layout combined_layout = QGridLayout() combined_layout.setRowStretch(0, 1) combined_layout.setColumnStretch(1, 100) combined_layout.addLayout(options_layout, 0, 0) combined_layout.addWidget(preview_group, 0, 1) self.setLayout(combined_layout) # Signals and slots create_button.clicked.connect(self.create_new_scheme) edit_button.clicked.connect(self.edit_scheme) self.reset_button.clicked.connect(self.reset_to_default) self.delete_button.clicked.connect(self.delete_scheme) self.schemes_combobox.currentIndexChanged.connect(self.update_preview) self.schemes_combobox.currentIndexChanged.connect(self.update_buttons) # Setup for name in names: self.scheme_editor_dialog.add_color_scheme_stack(name) for name in custom_names: self.scheme_editor_dialog.add_color_scheme_stack(name, custom=True) self.update_combobox() self.update_preview() def get_font(self, option): """Return global font used in Spyder.""" return get_font(option=option) def set_font(self, font, option): """Set global font used in Spyder.""" # Update fonts in all plugins set_font(font, option=option) plugins = self.main.widgetlist + self.main.thirdparty_plugins for plugin in plugins: plugin.update_font() def apply_settings(self, options): self.set_option('selected', self.current_scheme) color_scheme = self.get_option('selected') ui_theme = self.get_option('ui_theme') style_sheet = self.main.styleSheet() if ui_theme == 'automatic': if ((not is_dark_font_color(color_scheme) and not style_sheet) or (is_dark_font_color(color_scheme) and style_sheet)): self.changed_options.add('ui_theme') elif 'ui_theme' in self.changed_options: self.changed_options.remove('ui_theme') if 'ui_theme' not in self.changed_options: self.main.editor.apply_plugin_settings(['color_scheme_name']) if self.main.ipyconsole is not None: self.main.ipyconsole.apply_plugin_settings( ['color_scheme_name']) for plugin in self.main.thirdparty_plugins: try: # New API plugin.apply_conf(['color_scheme_name']) except AttributeError: # Old API plugin.apply_plugin_settings(['color_scheme_name']) self.update_combobox() self.update_preview() else: if 'ui_theme' in self.changed_options: if (style_sheet and ui_theme == 'dark' or not style_sheet and ui_theme == 'light'): self.changed_options.remove('ui_theme') if 'ui_theme' not in self.changed_options: self.main.editor.apply_plugin_settings(['color_scheme_name']) if self.main.ipyconsole is not None: self.main.ipyconsole.apply_plugin_settings( ['color_scheme_name']) for plugin in self.main.thirdparty_plugins: try: # New API plugin.apply_conf(['color_scheme_name']) except AttributeError: # Old API plugin.apply_plugin_settings(['color_scheme_name']) self.update_combobox() self.update_preview() if self.main.historylog is not None: self.main.historylog.apply_conf(['color_scheme_name']) # Helpers # ------------------------------------------------------------------------- @property def current_scheme_name(self): return self.schemes_combobox.currentText() @property def current_scheme(self): return self.scheme_choices_dict[self.current_scheme_name] @property def current_scheme_index(self): return self.schemes_combobox.currentIndex() def update_combobox(self): """Recreates the combobox contents.""" index = self.current_scheme_index self.schemes_combobox.blockSignals(True) names = self.get_option("names") try: names.pop(names.index(u'Custom')) except ValueError: pass custom_names = self.get_option("custom_names", []) # Useful for retrieving the actual data for n in names + custom_names: self.scheme_choices_dict[self.get_option('{0}/name'.format(n))] = n if custom_names: choices = names + [None] + custom_names else: choices = names combobox = self.schemes_combobox combobox.clear() for name in choices: if name is None: continue combobox.addItem(self.get_option('{0}/name'.format(name)), name) if custom_names: combobox.insertSeparator(len(names)) self.schemes_combobox.blockSignals(False) self.schemes_combobox.setCurrentIndex(index) def update_buttons(self): """Updates the enable status of delete and reset buttons.""" current_scheme = self.current_scheme names = self.get_option("names") try: names.pop(names.index(u'Custom')) except ValueError: pass delete_enabled = current_scheme not in names self.delete_button.setEnabled(delete_enabled) self.reset_button.setEnabled(not delete_enabled) def update_preview(self, index=None, scheme_name=None): """ Update the color scheme of the preview editor and adds text. Note ---- 'index' is needed, because this is triggered by a signal that sends the selected index. """ text = ('"""A string"""\n\n' '# A comment\n\n' 'class Foo(object):\n' ' def __init__(self):\n' ' bar = 42\n' ' print(bar)\n' ) if scheme_name is None: scheme_name = self.current_scheme self.preview_editor.setup_editor( font=get_font(), color_scheme=scheme_name, show_blanks=False, scroll_past_end=False, ) self.preview_editor.set_language('Python') self.preview_editor.set_text(text) # Actions # ------------------------------------------------------------------------- def create_new_scheme(self): """Creates a new color scheme with a custom name.""" names = self.get_option('names') custom_names = self.get_option('custom_names', []) # Get the available number this new color scheme counter = len(custom_names) - 1 custom_index = [int(n.split('-')[-1]) for n in custom_names] for i in range(len(custom_names)): if custom_index[i] != i: counter = i - 1 break custom_name = "custom-{0}".format(counter+1) # Add the config settings, based on the current one. custom_names.append(custom_name) self.set_option('custom_names', custom_names) for key in syntaxhighlighters.COLOR_SCHEME_KEYS: name = "{0}/{1}".format(custom_name, key) default_name = "{0}/{1}".format(self.current_scheme, key) option = self.get_option(default_name) self.set_option(name, option) self.set_option('{0}/name'.format(custom_name), custom_name) # Now they need to be loaded! how to make a partial load_from_conf? dlg = self.scheme_editor_dialog dlg.add_color_scheme_stack(custom_name, custom=True) dlg.set_scheme(custom_name) self.load_from_conf() if dlg.exec_(): # This is needed to have the custom name updated on the combobox name = dlg.get_scheme_name() self.set_option('{0}/name'.format(custom_name), name) # The +1 is needed because of the separator in the combobox index = (names + custom_names).index(custom_name) + 1 self.update_combobox() self.schemes_combobox.setCurrentIndex(index) else: # Delete the config .... custom_names.remove(custom_name) self.set_option('custom_names', custom_names) dlg.delete_color_scheme_stack(custom_name) def edit_scheme(self): """Edit current scheme.""" dlg = self.scheme_editor_dialog dlg.set_scheme(self.current_scheme) if dlg.exec_(): # Update temp scheme to reflect instant edits on the preview temporal_color_scheme = dlg.get_edited_color_scheme() for key in temporal_color_scheme: option = "temp/{0}".format(key) value = temporal_color_scheme[key] self.set_option(option, value) self.update_preview(scheme_name='temp') def delete_scheme(self): """Deletes the currently selected custom color scheme.""" scheme_name = self.current_scheme answer = QMessageBox.warning(self, _("Warning"), _("Are you sure you want to delete " "this scheme?"), QMessageBox.Yes | QMessageBox.No) if answer == QMessageBox.Yes: # Put the combobox in Spyder by default, when deleting a scheme names = self.get_option('names') self.set_scheme('spyder') self.schemes_combobox.setCurrentIndex(names.index('spyder')) self.set_option('selected', 'spyder') # Delete from custom_names custom_names = self.get_option('custom_names', []) if scheme_name in custom_names: custom_names.remove(scheme_name) self.set_option('custom_names', custom_names) # Delete config options for key in syntaxhighlighters.COLOR_SCHEME_KEYS: option = "{0}/{1}".format(scheme_name, key) CONF.remove_option(self.CONF_SECTION, option) CONF.remove_option(self.CONF_SECTION, "{0}/name".format(scheme_name)) self.update_combobox() self.update_preview() def set_scheme(self, scheme_name): """ Set the current stack in the dialog to the scheme with 'scheme_name'. """ dlg = self.scheme_editor_dialog dlg.set_scheme(scheme_name) @Slot() def reset_to_default(self): """Restore initial values for default color schemes.""" # Checks that this is indeed a default scheme scheme = self.current_scheme names = self.get_option('names') if scheme in names: for key in syntaxhighlighters.COLOR_SCHEME_KEYS: option = "{0}/{1}".format(scheme, key) value = CONF.get_default(self.CONF_SECTION, option) self.set_option(option, value) self.load_from_conf()
class LSPServerEditor(QDialog): DEFAULT_HOST = '127.0.0.1' DEFAULT_PORT = 2084 DEFAULT_CMD = '' DEFAULT_ARGS = '' DEFAULT_CONFIGURATION = '{}' DEFAULT_EXTERNAL = False DEFAULT_STDIO = False HOST_REGEX = re.compile(r'^\w+([.]\w+)*$') NON_EMPTY_REGEX = re.compile(r'^\S+$') JSON_VALID = _('Valid JSON') JSON_INVALID = _('Invalid JSON') MIN_SIZE = QSize(850, 600) INVALID_CSS = "QLineEdit {border: 1px solid red;}" VALID_CSS = "QLineEdit {border: 1px solid green;}" def __init__(self, parent, language=None, cmd='', host='127.0.0.1', port=2084, args='', external=False, stdio=False, configurations={}, get_option=None, set_option=None, remove_option=None, **kwargs): super(LSPServerEditor, self).__init__(parent) description = _( "To create a new server configuration, you need to select a " "programming language, set the command to start its associated " "server and enter any arguments that should be passed to it on " "startup. Additionally, you can set the server's hostname and " "port if connecting to an external server, " "or to a local one using TCP instead of stdio pipes." "<br><br>" "<i>Note</i>: You can use the placeholders <tt>{host}</tt> and " "<tt>{port}</tt> in the server arguments field to automatically " "fill in the respective values.<br>") self.parent = parent self.external = external self.set_option = set_option self.get_option = get_option self.remove_option = remove_option # Widgets self.server_settings_description = QLabel(description) self.lang_cb = QComboBox(self) self.external_cb = QCheckBox(_('External server'), self) self.host_label = QLabel(_('Host:')) self.host_input = QLineEdit(self) self.port_label = QLabel(_('Port:')) self.port_spinner = QSpinBox(self) self.cmd_label = QLabel(_('Command:')) self.cmd_input = QLineEdit(self) self.args_label = QLabel(_('Arguments:')) self.args_input = QLineEdit(self) self.json_label = QLabel(self.JSON_VALID, self) self.conf_label = QLabel(_('<b>Server Configuration:</b>')) self.conf_input = SimpleCodeEditor(None) self.bbox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) self.button_ok = self.bbox.button(QDialogButtonBox.Ok) self.button_cancel = self.bbox.button(QDialogButtonBox.Cancel) # Widget setup self.setMinimumSize(self.MIN_SIZE) self.setWindowTitle(_('LSP server editor')) self.server_settings_description.setWordWrap(True) self.lang_cb.setToolTip( _('Programming language provided by the LSP server')) self.lang_cb.addItem(_('Select a language')) self.lang_cb.addItems(SUPPORTED_LANGUAGES) self.button_ok.setEnabled(False) if language is not None: idx = SUPPORTED_LANGUAGES.index(language) self.lang_cb.setCurrentIndex(idx + 1) self.button_ok.setEnabled(True) self.host_input.setPlaceholderText('127.0.0.1') self.host_input.setText(host) self.host_input.textChanged.connect(lambda _: self.validate()) self.port_spinner.setToolTip(_('TCP port number of the server')) self.port_spinner.setMinimum(1) self.port_spinner.setMaximum(60000) self.port_spinner.setValue(port) self.port_spinner.valueChanged.connect(lambda _: self.validate()) self.cmd_input.setText(cmd) self.cmd_input.setPlaceholderText('/absolute/path/to/command') self.args_input.setToolTip( _('Additional arguments required to start the server')) self.args_input.setText(args) self.args_input.setPlaceholderText(r'--host {host} --port {port}') self.conf_input.setup_editor(language='json', color_scheme=get_option( 'selected', section='appearance'), wrap=False, highlight_current_line=True, font=get_font()) self.conf_input.set_language('json') self.conf_input.setToolTip( _('Additional LSP server configuration ' 'set at runtime. JSON required')) try: conf_text = json.dumps(configurations, indent=4, sort_keys=True) except Exception: conf_text = '{}' self.conf_input.set_text(conf_text) self.external_cb.setToolTip( _('Check if the server runs on a remote location')) self.external_cb.setChecked(external) self.stdio_cb = QCheckBox(_('Use stdio pipes for communication'), self) self.stdio_cb.setToolTip( _('Check if the server communicates ' 'using stdin/out pipes')) self.stdio_cb.setChecked(stdio) # Layout setup hlayout = QHBoxLayout() general_vlayout = QVBoxLayout() general_vlayout.addWidget(self.server_settings_description) vlayout = QVBoxLayout() lang_group = QGroupBox(_('Language')) lang_layout = QVBoxLayout() lang_layout.addWidget(self.lang_cb) lang_group.setLayout(lang_layout) vlayout.addWidget(lang_group) server_group = QGroupBox(_('Language server')) server_layout = QGridLayout() server_layout.addWidget(self.cmd_label, 0, 0) server_layout.addWidget(self.cmd_input, 0, 1) server_layout.addWidget(self.args_label, 1, 0) server_layout.addWidget(self.args_input, 1, 1) server_group.setLayout(server_layout) vlayout.addWidget(server_group) address_group = QGroupBox(_('Server address')) host_layout = QVBoxLayout() host_layout.addWidget(self.host_label) host_layout.addWidget(self.host_input) port_layout = QVBoxLayout() port_layout.addWidget(self.port_label) port_layout.addWidget(self.port_spinner) conn_info_layout = QHBoxLayout() conn_info_layout.addLayout(host_layout) conn_info_layout.addLayout(port_layout) address_group.setLayout(conn_info_layout) vlayout.addWidget(address_group) advanced_group = QGroupBox(_('Advanced')) advanced_layout = QVBoxLayout() advanced_layout.addWidget(self.external_cb) advanced_layout.addWidget(self.stdio_cb) advanced_group.setLayout(advanced_layout) vlayout.addWidget(advanced_group) conf_layout = QVBoxLayout() conf_layout.addWidget(self.conf_label) conf_layout.addWidget(self.conf_input) conf_layout.addWidget(self.json_label) vlayout.addStretch() hlayout.addLayout(vlayout, 2) hlayout.addLayout(conf_layout, 3) general_vlayout.addLayout(hlayout) general_vlayout.addWidget(self.bbox) self.setLayout(general_vlayout) self.form_status(False) # Signals if not external: self.cmd_input.textChanged.connect(lambda x: self.validate()) self.external_cb.stateChanged.connect(self.set_local_options) self.stdio_cb.stateChanged.connect(self.set_stdio_options) self.lang_cb.currentIndexChanged.connect(self.lang_selection_changed) self.conf_input.textChanged.connect(self.validate) self.bbox.accepted.connect(self.accept) self.bbox.rejected.connect(self.reject) # Final setup if language is not None: self.form_status(True) self.validate() if stdio: self.set_stdio_options(True) if external: self.set_local_options(True) @Slot() def validate(self): host_text = self.host_input.text() cmd_text = self.cmd_input.text() if host_text not in ['127.0.0.1', 'localhost']: self.external = True self.external_cb.setChecked(True) if not self.HOST_REGEX.match(host_text): self.button_ok.setEnabled(False) self.host_input.setStyleSheet(self.INVALID_CSS) if bool(host_text): self.host_input.setToolTip(_('Hostname must be valid')) else: self.host_input.setToolTip( _('Hostname or IP address of the host on which the server ' 'is running. Must be non empty.')) else: self.host_input.setStyleSheet(self.VALID_CSS) self.host_input.setToolTip(_('Hostname is valid')) self.button_ok.setEnabled(True) if not self.external: if not self.NON_EMPTY_REGEX.match(cmd_text): self.button_ok.setEnabled(False) self.cmd_input.setStyleSheet(self.INVALID_CSS) self.cmd_input.setToolTip( _('Command used to start the LSP server locally. Must be ' 'non empty')) return if find_program(cmd_text) is None: self.button_ok.setEnabled(False) self.cmd_input.setStyleSheet(self.INVALID_CSS) self.cmd_input.setToolTip( _('Program was not found ' 'on your system')) else: self.cmd_input.setStyleSheet(self.VALID_CSS) self.cmd_input.setToolTip( _('Program was found on your ' 'system')) self.button_ok.setEnabled(True) else: port = int(self.port_spinner.text()) response = check_connection_port(host_text, port) if not response: self.button_ok.setEnabled(False) try: json.loads(self.conf_input.toPlainText()) try: self.json_label.setText(self.JSON_VALID) except Exception: pass except ValueError: try: self.json_label.setText(self.JSON_INVALID) self.button_ok.setEnabled(False) except Exception: pass def form_status(self, status): self.host_input.setEnabled(status) self.port_spinner.setEnabled(status) self.external_cb.setEnabled(status) self.stdio_cb.setEnabled(status) self.cmd_input.setEnabled(status) self.args_input.setEnabled(status) self.conf_input.setEnabled(status) self.json_label.setVisible(status) @Slot() def lang_selection_changed(self): idx = self.lang_cb.currentIndex() if idx == 0: self.set_defaults() self.form_status(False) self.button_ok.setEnabled(False) else: server = self.parent.get_server_by_lang(SUPPORTED_LANGUAGES[idx - 1]) self.form_status(True) if server is not None: self.host_input.setText(server.host) self.port_spinner.setValue(server.port) self.external_cb.setChecked(server.external) self.stdio_cb.setChecked(server.stdio) self.cmd_input.setText(server.cmd) self.args_input.setText(server.args) self.conf_input.set_text(json.dumps(server.configurations)) self.json_label.setText(self.JSON_VALID) self.button_ok.setEnabled(True) else: self.set_defaults() def set_defaults(self): self.cmd_input.setStyleSheet('') self.host_input.setStyleSheet('') self.host_input.setText(self.DEFAULT_HOST) self.port_spinner.setValue(self.DEFAULT_PORT) self.external_cb.setChecked(self.DEFAULT_EXTERNAL) self.stdio_cb.setChecked(self.DEFAULT_STDIO) self.cmd_input.setText(self.DEFAULT_CMD) self.args_input.setText(self.DEFAULT_ARGS) self.conf_input.set_text(self.DEFAULT_CONFIGURATION) self.json_label.setText(self.JSON_VALID) @Slot(bool) @Slot(int) def set_local_options(self, enabled): self.external = enabled self.cmd_input.setEnabled(True) self.args_input.setEnabled(True) if enabled: self.cmd_input.setEnabled(False) self.cmd_input.setStyleSheet('') self.args_input.setEnabled(False) self.stdio_cb.stateChanged.disconnect() self.stdio_cb.setChecked(False) self.stdio_cb.setEnabled(False) else: self.cmd_input.setEnabled(True) self.args_input.setEnabled(True) self.stdio_cb.setEnabled(True) self.stdio_cb.setChecked(False) self.stdio_cb.stateChanged.connect(self.set_stdio_options) try: self.validate() except Exception: pass @Slot(bool) @Slot(int) def set_stdio_options(self, enabled): self.stdio = enabled if enabled: self.cmd_input.setEnabled(True) self.args_input.setEnabled(True) self.external_cb.stateChanged.disconnect() self.external_cb.setChecked(False) self.external_cb.setEnabled(False) self.host_input.setStyleSheet('') self.host_input.setEnabled(False) self.port_spinner.setEnabled(False) else: self.cmd_input.setEnabled(True) self.args_input.setEnabled(True) self.external_cb.setChecked(False) self.external_cb.setEnabled(True) self.external_cb.stateChanged.connect(self.set_local_options) self.host_input.setEnabled(True) self.port_spinner.setEnabled(True) try: self.validate() except Exception: pass def get_options(self): language_idx = self.lang_cb.currentIndex() language = SUPPORTED_LANGUAGES[language_idx - 1] host = self.host_input.text() port = int(self.port_spinner.value()) external = self.external_cb.isChecked() stdio = self.stdio_cb.isChecked() args = self.args_input.text() cmd = self.cmd_input.text() configurations = json.loads(self.conf_input.toPlainText()) server = LSPServer(language=language.lower(), cmd=cmd, args=args, host=host, port=port, external=external, stdio=stdio, configurations=configurations, get_option=self.get_option, set_option=self.set_option, remove_option=self.remove_option) return server