def __init__(self, parent): QTableView.__init__(self, parent) self._model = None # Setting up actions self.date_dayfirst_action = create_action(self, "dayfirst", triggered=ft_partial(self.parse_to_type, atype="date", dayfirst=True)) self.date_monthfirst_action = create_action(self, "monthfirst", triggered=ft_partial(self.parse_to_type, atype="date", dayfirst=False)) self.perc_action = create_action(self, "perc", triggered=ft_partial(self.parse_to_type, atype="perc")) self.acc_action = create_action(self, "account", triggered=ft_partial(self.parse_to_type, atype="account")) self.str_action = create_action(self, "unicode", triggered=ft_partial(self.parse_to_type, atype="unicode")) self.int_action = create_action(self, "int", triggered=ft_partial(self.parse_to_type, atype="int")) self.float_action = create_action(self, "float", triggered=ft_partial(self.parse_to_type, atype="float")) # Setting up menus self.date_menu = QMenu() self.date_menu.setTitle("Date") add_actions( self.date_menu, (self.date_dayfirst_action, self.date_monthfirst_action)) self.parse_menu = QMenu(self) self.parse_menu.addMenu(self.date_menu) add_actions( self.parse_menu, (self.perc_action, self.acc_action)) self.parse_menu.setTitle("String to") self.opt_menu = QMenu(self) self.opt_menu.addMenu(self.parse_menu) add_actions( self.opt_menu, (self.str_action, self.int_action, self.float_action))
def __init__(self, parent, actions=None, menu=None, corner_widgets=None, menu_use_tooltips=False): QTabWidget.__init__(self, parent) self.setUsesScrollButtons(True) # To style tabs on Mac if sys.platform == 'darwin': self.setObjectName('plugin-tab') self.corner_widgets = {} self.menu_use_tooltips = menu_use_tooltips if menu is None: self.menu = QMenu(self) if actions: add_actions(self.menu, actions) else: self.menu = menu # Corner widgets if corner_widgets is None: corner_widgets = {} corner_widgets.setdefault(Qt.TopLeftCorner, []) corner_widgets.setdefault(Qt.TopRightCorner, []) self.browse_button = create_toolbutton(self, icon=ima.icon('browse_tab'), tip=_("Browse tabs")) self.browse_tabs_menu = QMenu(self) self.browse_button.setMenu(self.browse_tabs_menu) self.browse_button.setPopupMode(self.browse_button.InstantPopup) self.browse_tabs_menu.aboutToShow.connect(self.update_browse_tabs_menu) corner_widgets[Qt.TopLeftCorner] += [self.browse_button] self.set_corner_widgets(corner_widgets)
def get_plugin_actions(self): """Return a list of actions related to plugin""" quit_action = create_action(self, _("&Quit"), icon=ima.icon('exit'), tip=_("Quit"), triggered=self.quit) self.register_shortcut(quit_action, "_", "Quit", "Ctrl+Q") run_action = create_action(self, _("&Run..."), None, ima.icon('run_small'), _("Run a Python script"), triggered=self.run_script) environ_action = create_action(self, _("Environment variables..."), icon=ima.icon('environ'), tip=_("Show and edit environment variables" " (for current session)"), triggered=self.show_env) syspath_action = create_action(self, _("Show sys.path contents..."), icon=ima.icon('syspath'), tip=_("Show (read-only) sys.path"), triggered=self.show_syspath) buffer_action = create_action(self, _("Buffer..."), None, tip=_("Set maximum line count"), triggered=self.change_max_line_count) exteditor_action = create_action(self, _("External editor path..."), None, None, _("Set external editor executable path"), triggered=self.change_exteditor) wrap_action = create_action(self, _("Wrap lines"), toggled=self.toggle_wrap_mode) wrap_action.setChecked(self.get_option('wrap')) calltips_action = create_action(self, _("Display balloon tips"), toggled=self.toggle_calltips) calltips_action.setChecked(self.get_option('calltips')) codecompletion_action = create_action(self, _("Automatic code completion"), toggled=self.toggle_codecompletion) codecompletion_action.setChecked(self.get_option('codecompletion/auto')) codecompenter_action = create_action(self, _("Enter key selects completion"), toggled=self.toggle_codecompletion_enter) codecompenter_action.setChecked(self.get_option( 'codecompletion/enter_key')) option_menu = QMenu(_('Internal console settings'), self) option_menu.setIcon(ima.icon('tooloptions')) add_actions(option_menu, (buffer_action, wrap_action, calltips_action, codecompletion_action, codecompenter_action, exteditor_action)) plugin_actions = [None, run_action, environ_action, syspath_action, option_menu, None, quit_action] # Add actions to context menu add_actions(self.shell.menu, plugin_actions) return plugin_actions
def tableMenu(self, event): """Right click menu for table, can plot or print selected logs""" menu = QMenu(self) plotAction = menu.addAction("Plot selected") plotAction.triggered.connect(self.presenter.new_plot_logs) plotAction = menu.addAction("Print selected") plotAction.triggered.connect(self.presenter.print_selected_logs) menu.exec_(event.globalPos())
def context_menu_requested(self, event): """Popup context menu.""" if self.fig: pos = QPoint(event.x(), event.y()) context_menu = QMenu(self) context_menu.addAction(ima.icon('editcopy'), "Copy Image", self.copy_figure, QKeySequence( get_shortcut('plots', 'copy'))) context_menu.popup(self.mapToGlobal(pos))
def _display_roi_context_menu(self, roi_index): def delete_roi(event): self._dc.remove_subset_group(self._dc.subset_groups[roi_index]) context_menu = QMenu() action = QAction("Delete ROI", context_menu) action.triggered.connect(delete_roi) context_menu.addAction(action) pos = self._viewer.mapToParent(QCursor().pos()) context_menu.exec_(pos)
def _add_axes_scale_menu(self, menu): """Add the Axes scale options menu to the given menu""" axes_menu = QMenu("Axes", menu) axes_actions = QActionGroup(axes_menu) current_scale_types = self._get_axes_scale_types() for label, scale_types in iteritems(AXES_SCALE_MENU_OPTS): action = axes_menu.addAction(label, partial(self._quick_change_axes, scale_types)) if current_scale_types == scale_types: action.setCheckable(True) action.setChecked(True) axes_actions.addAction(action) menu.addMenu(axes_menu)
def _make_export_button(self): """ Makes the export button menu, containing a list of export types :return: The export button menu """ export_button = QPushButton("Export") export_menu = QMenu() for text, extension in EXPORT_TYPES: export_menu.addAction(text, lambda ext=extension: self.presenter.export_plots_called(ext)) export_button.setMenu(export_menu) return export_button
def _init_ui(self): loadUi(os.path.abspath( os.path.join(os.path.dirname(__file__), ".", "model_editor.ui")), self) # Populate the add mode button with a dropdown containing available # fittable model objects self.add_model_button.setPopupMode(QToolButton.InstantPopup) models_menu = QMenu(self.add_model_button) self.add_model_button.setMenu(models_menu) for k, v in MODELS.items(): action = QAction(k, models_menu) action.triggered.connect(lambda x, m=v: self._add_fittable_model(m)) models_menu.addAction(action) self.fit_model_thread = None # Initially hide the model editor tools until user has selected an # editable model spectrum object self.editor_holder_widget.setHidden(True) self.setup_holder_widget.setHidden(False) self.equation_edit_button.clicked.connect( self._on_equation_edit_button_clicked) self.new_model_button.clicked.connect(self._on_create_new_model) self.remove_model_button.clicked.connect(self._on_remove_model) self.advanced_settings_button.clicked.connect( lambda: ModelAdvancedSettingsDialog(self, self).exec()) self.save_model_button.clicked.connect(self._on_save_model) self.load_model_button.clicked.connect(self._on_load_from_file) self._data_item_proxy_model = DataItemProxyModel() self._data_item_proxy_model.setSourceModel(self.hub.model) self.data_selection_combo.setModel(self._data_item_proxy_model) self.data_selection_combo.currentIndexChanged.connect(self._redraw_model) # When a plot data item is select, get its model editor model # representation self.hub.workspace.current_selected_changed.connect( self._on_plot_item_selected) # When the plot window changes, reset model editor self.hub.workspace.mdi_area.subWindowActivated.connect(self._on_new_plot_activated) # Listen for when data items are added to internal model self.hub.model.data_added.connect(self._on_data_item_added) # Connect the fit model button self.fit_button.clicked.connect(self._on_fit_clicked)
def openWidgetMenu(self,position): indexes = self.ui.treeWidget_2.selectedIndexes() item = self.ui.treeWidget_2.itemAt(position) if item == None: return #item = self.ui.listWidget.itemAt(position) if len(indexes) > 0: menu = QMenu() menu.addAction(QAction("Delete", menu,checkable = True))#This function is perhaps useless #menu.triggered.connect(self.eraseItem) item = self.ui.treeWidget_2.itemAt(position) #collec = str(item.text()) menu.triggered.connect(lambda action: self.ListMethodSelected(action, item)) menu.exec_(self.ui.treeWidget_2.viewport().mapToGlobal(position))
def context_menu_requested(self, event): """ """ pos = QPoint(event.x(), event.y()) menu = QMenu(self) actions = [] action_title = create_action(self, _('Go to step: '), icon=QIcon()) action_title.setDisabled(True) actions.append(action_title) # actions.append(create_action(self, _(': '), icon=QIcon())) add_actions(menu, actions) menu.popup(self.mapToGlobal(pos))
def contextMenuEvent(self, event): menu = QMenu(self) actions = [self.pageAction(QWebEnginePage.Back), self.pageAction(QWebEnginePage.Forward), None, self.pageAction(QWebEnginePage.SelectAll), self.pageAction(QWebEnginePage.Copy), None, self.zoom_in_action, self.zoom_out_action] if DEV and not WEBENGINE: settings = self.page().settings() settings.setAttribute(QWebEngineSettings.DeveloperExtrasEnabled, True) actions += [None, self.pageAction(QWebEnginePage.InspectElement)] add_actions(menu, actions) menu.popup(event.globalPos()) event.accept()
def get_plugin_actions(self): """Return a list of actions related to plugin""" self.new_project_action = create_action(self, _("New Project..."), triggered=self.create_new_project) self.open_project_action = create_action(self, _("Open Project..."), triggered=lambda v: self.open_project()) self.close_project_action = create_action(self, _("Close Project"), triggered=self.close_project) self.clear_recent_projects_action = create_action( self, _("Clear this list"), triggered=self.clear_recent_projects ) self.edit_project_preferences_action = create_action( self, _("Project Preferences"), triggered=self.edit_project_preferences ) self.recent_project_menu = QMenu(_("Recent Projects"), self) explorer_action = self.toggle_view_action self.main.projects_menu_actions += [ self.new_project_action, None, self.open_project_action, self.close_project_action, None, self.recent_project_menu, explorer_action, ] self.setup_menu_actions() return []
def get_plugin_actions(self): """Return a list of actions related to plugin""" self.new_project_action = create_action(self, _("New Project..."), triggered=self.create_new_project) self.open_project_action = create_action(self, _("Open Project..."), triggered=lambda v: self.open_project()) self.close_project_action = create_action(self, _("Close Project"), triggered=self.close_project) self.delete_project_action = create_action(self, _("Delete Project"), triggered=self._delete_project) self.clear_recent_projects_action =\ create_action(self, _("Clear this list"), triggered=self.clear_recent_projects) self.edit_project_preferences_action =\ create_action(self, _("Project Preferences"), triggered=self.edit_project_preferences) self.recent_project_menu = QMenu(_("Recent Projects"), self) if self.main is not None: self.main.projects_menu_actions += [self.new_project_action, MENU_SEPARATOR, self.open_project_action, self.close_project_action, self.delete_project_action, MENU_SEPARATOR, self.recent_project_menu, self.toggle_view_action] self.setup_menu_actions() return []
def setup_context_menu(self): """Setup shell context menu""" self.menu = QMenu(self) self.cut_action = create_action(self, _("Cut"), shortcut=keybinding('Cut'), icon=ima.icon('editcut'), triggered=self.cut) self.copy_action = create_action(self, _("Copy"), shortcut=keybinding('Copy'), icon=ima.icon('editcopy'), triggered=self.copy) paste_action = create_action(self, _("Paste"), shortcut=keybinding('Paste'), icon=ima.icon('editpaste'), triggered=self.paste) save_action = create_action(self, _("Save history log..."), icon=ima.icon('filesave'), tip=_("Save current history log (i.e. all " "inputs and outputs) in a text file"), triggered=self.save_historylog) self.delete_action = create_action(self, _("Delete"), shortcut=keybinding('Delete'), icon=ima.icon('editdelete'), triggered=self.delete) selectall_action = create_action(self, _("Select All"), shortcut=keybinding('SelectAll'), icon=ima.icon('selectall'), triggered=self.selectAll) add_actions(self.menu, (self.cut_action, self.copy_action, paste_action, self.delete_action, None, selectall_action, None, save_action) )
def _make_context_menu(self): """ Makes the context menu with options relating to plots :return: The context menu, and export sub-menu with a list of export types """ context_menu = QMenu() context_menu.addAction("Show", self.presenter.show_multiple_selected) context_menu.addAction("Hide", self.presenter.hide_selected_plots) context_menu.addAction("Delete", self.presenter.close_action_called) context_menu.addAction("Rename", self.rename_selected_in_context_menu) export_menu = context_menu.addMenu("Export") for text, extension in EXPORT_TYPES: export_menu.addAction(text, lambda ext=extension: self.presenter.export_plots_called(ext)) return context_menu, export_menu
def build_context_menu(self, index): """Build context menu for test item that given index points to.""" contextMenu = QMenu(self) if self.isExpanded(index): menuItem = create_action(self, _('Collapse'), triggered=lambda: self.collapse(index)) else: menuItem = create_action(self, _('Expand'), triggered=lambda: self.expand(index)) menuItem.setEnabled(self.model().hasChildren(index)) contextMenu.addAction(menuItem) menuItem = create_action( self, _('Go to definition'), triggered=lambda: self.go_to_test_definition(index)) test_location = self.model().data(index, Qt.UserRole) menuItem.setEnabled(test_location[0] is not None) contextMenu.addAction(menuItem) return contextMenu
def setup(self, name_filters=['*.py', '*.pyw'], show_all=False): """Setup tree widget""" self.setup_view() self.set_name_filters(name_filters) self.show_all = show_all # Setup context menu self.menu = QMenu(self) self.common_actions = self.setup_common_actions()
def context_menu_requested(self, event): """ Custom context menu""" index = self.current_index model_index = self.proxy_model.mapToSource(index) row = self.source_model.row(model_index.row()) name, license_ = row[const.NAME], row[const.LICENSE] pos = QPoint(event.x(), event.y()) self._menu = QMenu(self) metadata = self._parent.get_package_metadata(name) pypi = metadata["pypi"] home = metadata["home"] dev = metadata["dev"] docs = metadata["docs"] q_pypi = QIcon(get_image_path("python.png")) q_home = QIcon(get_image_path("home.png")) q_docs = QIcon(get_image_path("conda_docs.png")) if "git" in dev: q_dev = QIcon(get_image_path("conda_github.png")) elif "bitbucket" in dev: q_dev = QIcon(get_image_path("conda_bitbucket.png")) else: q_dev = QIcon() if "mit" in license_.lower(): lic = "http://opensource.org/licenses/MIT" elif "bsd" == license_.lower(): lic = "http://opensource.org/licenses/BSD-3-Clause" else: lic = None actions = [] if license_ != "": actions.append( create_action(self, _("License: " + license_), icon=QIcon(), triggered=lambda: self.open_url(lic)) ) actions.append(None) if pypi != "": actions.append( create_action(self, _("Python Package Index"), icon=q_pypi, triggered=lambda: self.open_url(pypi)) ) if home != "": actions.append(create_action(self, _("Homepage"), icon=q_home, triggered=lambda: self.open_url(home))) if docs != "": actions.append(create_action(self, _("Documentation"), icon=q_docs, triggered=lambda: self.open_url(docs))) if dev != "": actions.append(create_action(self, _("Development"), icon=q_dev, triggered=lambda: self.open_url(dev))) if len(actions): add_actions(self._menu, actions) self._menu.popup(self.viewport().mapToGlobal(pos))
def _dict_to_menu(self, menu_dict, menu_widget=None): '''Stolen shamelessly from specviz. Thanks!''' if not menu_widget: menu_widget = QMenu() for k, v in menu_dict.items(): if isinstance(v, dict): new_menu = menu_widget.addMenu(k) self._dict_to_menu(v, menu_widget=new_menu) else: act = QAction(k, menu_widget) if isinstance(v, list): if v[0] == 'checkable': v = v[1] act.setCheckable(True) act.setChecked(False) act.triggered.connect(v) menu_widget.addAction(act) return menu_widget
def _show_context_menu(self, event): """Display a relevant context menu on the canvas :param event: The MouseEvent that generated this call """ if not event.inaxes: # the current context menus are ony relevant for axes return fig_type = figure_type(self.canvas.figure) if fig_type == FigureType.Empty or fig_type == FigureType.Image: # Fitting or changing scale types does not make sense in # these cases return menu = QMenu() if self.fit_browser.tool is not None: self.fit_browser.add_to_menu(menu) menu.addSeparator() self._add_axes_scale_menu(menu) menu.exec_(QCursor.pos())
def on_mouse_press_event(self, event): """ Event handling for mouse press action Args: event: Returns: """ # get the button and position information. curr_x = event.xdata curr_y = event.ydata if curr_x is None or curr_y is None: # outside of canvas return button = event.button if button == 1: # left button: no operation pass elif button == 3: # right button: # Pop-out menu self.menu = QMenu(self) if self.get_canvas().is_legend_on: # figure has legend: remove legend action1 = QAction('Hide legend', self) action1.triggered.connect(self._myCanvas.hide_legend) action2 = QAction('Legend font larger', self) action2.triggered.connect(self._myCanvas.increase_legend_font_size) action3 = QAction('Legend font smaller', self) action3.triggered.connect(self._myCanvas.decrease_legend_font_size) self.menu.addAction(action2) self.menu.addAction(action3) else: # figure does not have legend: add legend action1 = QAction('Show legend', self) action1.triggered.connect(self._myCanvas.show_legend) self.menu.addAction(action1) # pop up menu self.menu.popup(QCursor.pos()) # END-IF-ELSE return
def __init__(self, parent=None, init_channel=None): QLineEdit.__init__(self, parent) PyDMWritableWidget.__init__(self, init_channel=init_channel) self.app = QApplication.instance() self._display = None self._scale = 1 self.returnPressed.connect(self.send_value) self.unitMenu = QMenu('Convert Units', self) self.create_unit_options() self._display_format_type = self.DisplayFormat.Default self._string_encoding = "utf_8" if utilities.is_pydm_app(): self._string_encoding = self.app.get_string_encoding()
def context_menu(self): try: menu = super(PyDMRelatedDisplayButton, self).context_menu() except: menu = QMenu(self) if len(menu.findChildren(QAction)) > 0: menu.addSeparator() menu.addAction(self.open_in_new_window_action) return menu
def _rebuild_menu(self): if not any(self._filenames): self._filenames = [] if not any(self._titles): self._titles = [] if len(self._filenames) == 0: self.setEnabled(False) if len(self._filenames) <= 1: self.setMenu(None) self._menu_needs_rebuild = False return menu = QMenu(self) for i, filename in enumerate(self._filenames): if i >= len(self._titles): title = filename else: title = self._titles[i] action = menu.addAction(title) macros = "" if i < len(self._macros): macros = self._macros[i] action.triggered.connect(partial(self.open_display, filename, macros, target=None)) self.setMenu(menu) self._menu_needs_rebuild = False
def __init__(self, main=None): """Bind widget to a QMainWindow instance.""" super(PluginWidget, self).__init__(main) assert self.CONF_SECTION is not None self.dockwidget = None self.undocked_window = None # Check compatibility check_compatibility, message = self.check_compatibility() if not check_compatibility: self.show_compatibility_message(message) self.PLUGIN_PATH = os.path.dirname(inspect.getfile(self.__class__)) self.main = main self.default_margins = None self.plugin_actions = None self.ismaximized = False self.isvisible = False # Options button and menu self.options_button = create_toolbutton(self, text=_('Options'), icon=ima.icon('tooloptions')) self.options_button.setPopupMode(QToolButton.InstantPopup) # 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.options_menu = QMenu(self) # NOTE: Don't use the default option of CONF.get to assign a # None shortcut to plugins that don't have one. That will mess # the creation of our Keyboard Shortcuts prefs page try: self.shortcut = CONF.get('shortcuts', '_/switch to %s' % self.CONF_SECTION) except configparser.NoOptionError: pass # We decided to create our own toggle action instead of using # the one that comes with dockwidget because it's not possible # to raise and focus the plugin with it. self.toggle_view_action = None
def setup_window(self): """ """ self.close_action = create_action(self, _("&Quit"), triggered=self.close) self.file_menu_actions.append(self.close_action) self.file_menu = self.menuBar().addMenu(_("&File")) add_actions(self.file_menu, self.file_menu_actions) # Environments self.add_env_action = create_action(self, _("&Add"), triggered=self.add_env) self.clone_env_action = create_action(self, _("&Clone"), triggered=self.clone_env) self.remove_env_action = create_action(self, _("&Remove"), triggered=self.remove_env) self.envs_list_menu = QMenu(_('Environments')) self.envs_menu_actions = [self.add_env_action, self.clone_env_action, self.remove_env_action, None, self.envs_list_menu] self.envs_menu = self.menuBar().addMenu(_("&Environments")) add_actions(self.envs_menu, self.envs_menu_actions) self.update_env_menu() # Channels self.envs_menu = self.menuBar().addMenu(_("&Channels")) # Tools self.preferences_action = create_action(self, _("&Preferences"), triggered=self.preferences) self.tools_menu_actions.append(self.preferences_action) self.tools_menu = self.menuBar().addMenu(_("&Tools")) add_actions(self.tools_menu, self.tools_menu_actions) # Help self.report_action = create_action(self, _("&Report issue"), triggered=self.report_issue) self.about_action = create_action(self, _("&About"), triggered=self.about) self.help_menu_actions.append(self.report_action) self.help_menu_actions.append(self.about_action) self.help_menu = self.menuBar().addMenu(_("&Help")) add_actions(self.help_menu, self.help_menu_actions) self.setWindowIcon(get_icon('condapackages.png'))
def __init__(self, parent): QTreeWidget.__init__(self, parent) self.setItemsExpandable(True) self.setColumnCount(1) self.itemActivated.connect(self.activated) self.itemClicked.connect(self.clicked) # Setup context menu self.menu = QMenu(self) self.collapse_all_action = None self.collapse_selection_action = None self.expand_all_action = None self.expand_selection_action = None self.common_actions = self.setup_common_actions() self.__expanded_state = None self.itemSelectionChanged.connect(self.item_selection_changed) self.item_selection_changed()
def contextMenuEvent(self, event): index_clicked = self.indexAt(event.pos()) actions = [] self.popup_menu = QMenu(self) clear_all_breakpoints_action = create_action(self, _("Clear breakpoints in all files"), triggered=lambda: self.clear_all_breakpoints.emit()) actions.append(clear_all_breakpoints_action) if self.model.breakpoints: filename = self.model.breakpoints[index_clicked.row()][0] lineno = int(self.model.breakpoints[index_clicked.row()][1]) # QAction.triggered works differently for PySide and PyQt if not API == 'pyside': clear_slot = lambda _checked, filename=filename, lineno=lineno: \ self.clear_breakpoint.emit(filename, lineno) edit_slot = lambda _checked, filename=filename, lineno=lineno: \ (self.edit_goto.emit(filename, lineno, ''), self.set_or_edit_conditional_breakpoint.emit()) else: clear_slot = lambda filename=filename, lineno=lineno: \ self.clear_breakpoint.emit(filename, lineno) edit_slot = lambda filename=filename, lineno=lineno: \ (self.edit_goto.emit(filename, lineno, ''), self.set_or_edit_conditional_breakpoint.emit()) clear_breakpoint_action = create_action(self, _("Clear this breakpoint"), triggered=clear_slot) actions.insert(0,clear_breakpoint_action) edit_breakpoint_action = create_action(self, _("Edit this breakpoint"), triggered=edit_slot) actions.append(edit_breakpoint_action) add_actions(self.popup_menu, actions) self.popup_menu.popup(event.globalPos()) event.accept()
def open_header_menu(self, position): menu = QMenu(self) customize_header = menu.addAction("Customize header") customize_header.triggered.connect(self.open_header_dialog) menu.popup(self.table_header_view.viewport().mapToGlobal(position))
class GofRView(MplGraphicsView): """ Graphics view for G(R) """ def __init__(self, parent): """ Initialization """ MplGraphicsView.__init__(self, parent) # class variable containers self._grDict = dict() self._colorList = ['black', 'red', 'blue', 'green', 'brown', 'orange'] self._colorIndex = 0 # define the event handlers to the mouse actions self._myCanvas.mpl_connect('button_press_event', self.on_mouse_press_event) # self._myCanvas.mpl_connect('button_release_event', self.on_mouse_release_event) # self._myCanvas.mpl_connect('motion_notify_event', self.on_mouse_motion) # class variable self._minY = None self._maxY = None # variable self._isLegendOn = False return def on_mouse_press_event(self, event): """ Event handling for mouse press action Args: event: Returns: """ # get the button and position information. curr_x = event.xdata curr_y = event.ydata if curr_x is None or curr_y is None: # outside of canvas return button = event.button if button == 1: # left button: no operation pass elif button == 3: # right button: # Pop-out menu self.menu = QMenu(self) if self.get_canvas().is_legend_on: # figure has legend: remove legend action1 = QAction('Hide legend', self) action1.triggered.connect(self._myCanvas.hide_legend) action2 = QAction('Legend font larger', self) action2.triggered.connect(self._myCanvas.increase_legend_font_size) action3 = QAction('Legend font smaller', self) action3.triggered.connect(self._myCanvas.decrease_legend_font_size) self.menu.addAction(action2) self.menu.addAction(action3) else: # figure does not have legend: add legend action1 = QAction('Show legend', self) action1.triggered.connect(self._myCanvas.show_legend) self.menu.addAction(action1) # pop up menu self.menu.popup(QCursor.pos()) # END-IF-ELSE return def plot_gr(self, plot_key, ws_name, plotError=False, color='black', style='.', marker=None, alpha=1., label=None): """ Plot G(r) :param plot_key: a key to the current plot :param vec_r: numpy array for R :param vec_g: numpy array for G(r) :param vec_e: numpy array for G(r) error :param plot_error: :param color: :param style: :param marker: :param alpha: :param label: label for the line to plot :return: """ # q_min = 10., q_max = 50. # alpha = 1. - (q_now - q_min)/(q_max - q_min) if not label: label = str(plot_key) line_id = self.add_plot_1d(ws_name, wkspindex=0, marker=marker, color=color, line_style=style, alpha=alpha, label=label, x_label=r'r ($\AA$)', plotError=plotError) self._colorIndex += 1 self._grDict[str(plot_key)] = line_id # check the low/max self.auto_scale_y() def _reset_y_range(self, vec_gr): """ reset the Y range :param vec_gr: :return: """ this_min = min(vec_gr) this_max = max(vec_gr) if self._minY is None or this_min < self._minY: self._minY = this_min if self._maxY is None or this_max > self._maxY: self._maxY = this_max return def _auto_rescale_y(self): """ :return: """ if self._minY is None or self._maxY is None: return delta_y = self._maxY - self._minY lower_boundary = self._minY - delta_y * 0.05 upper_boundary = self._maxY + delta_y * 0.05 self.setXYLimit(ymin=lower_boundary, ymax=upper_boundary) return def has_gr(self, gr_ws_name): """Check whether a plot of G(r) exists on the canvas :param gr_ws_name: :return: """ return gr_ws_name in self._grDict def get_current_grs(self): """ list all the G(r) plotted on the figure now :return: """ return list(self._grDict.keys()) def remove_gr(self, plot_key): """Remove a plotted G(r) from canvas :param plot_key: key to locate the 1-D plot on canvas :return: boolean, string (as error message) """ # check assert isinstance(plot_key, str), 'Key for the plot must be a string but not %s.' % str(type(plot_key)) if plot_key not in self._grDict: return False, 'Workspace %s cannot be found in GofR dictionary of canvas' % plot_key # get line ID line_id = self._grDict[plot_key] # remove from plot self.remove_line(line_id) # clean G(r) plot del self._grDict[plot_key] # reset min and max self._minY = None self._maxY = None return def reset_color(self): """Reset color scheme :return: """ self._colorIndex = 0 def reset(self): """ Reset the canvas by deleting all lines and clean the dictionary Returns: """ # remove all lines and reset marker/color default sequence self.clear_all_lines() self.reset_line_color_marker_index() self._colorIndex = 0 # clean dictionary self._grDict.clear() return def update_gr(self, plot_key, ws_name, plotError=False): """update the value of an existing G(r) :param plot_key: :param vec_r: :param vec_g: :param vec_ge: :return: """ # check existence if plot_key not in self._grDict: raise RuntimeError('Plot with key/workspace name {0} does not exist on plot. Current plots are ' '{1}'.format(plot_key, list(self._grDict.keys()))) # update line_key = self._grDict[plot_key] self.updateLine(ikey=line_key, wkspname=ws_name) # update range self.auto_scale_y() return
class Projects(SpyderPluginWidget): """Projects plugin.""" CONF_SECTION = 'project_explorer' sig_pythonpath_changed = Signal() sig_project_created = Signal(object, object, object) sig_project_loaded = Signal(object) sig_project_closed = Signal(object) def __init__(self, parent=None): """Initialization.""" SpyderPluginWidget.__init__(self, parent) self.explorer = ProjectExplorerWidget( self, name_filters=self.get_option('name_filters'), show_all=self.get_option('show_all'), show_hscrollbar=self.get_option('show_hscrollbar'), options_button=self.options_button) layout = QVBoxLayout() layout.addWidget(self.explorer) self.setLayout(layout) self.recent_projects = self.get_option('recent_projects', default=[]) self.current_active_project = None self.latest_project = None # Initialize plugin self.initialize_plugin() self.explorer.setup_project(self.get_active_project_path()) #------ SpyderPluginWidget API --------------------------------------------- def get_plugin_title(self): """Return widget title""" return _("Project explorer") def get_focus_widget(self): """ Return the widget to give focus to when this plugin's dockwidget is raised on top-level """ return self.explorer.treewidget def get_plugin_actions(self): """Return a list of actions related to plugin""" self.new_project_action = create_action(self, _("New Project..."), triggered=self.create_new_project) self.open_project_action = create_action(self, _("Open Project..."), triggered=lambda v: self.open_project()) self.close_project_action = create_action(self, _("Close Project"), triggered=self.close_project) self.delete_project_action = create_action(self, _("Delete Project"), triggered=self._delete_project) self.clear_recent_projects_action =\ create_action(self, _("Clear this list"), triggered=self.clear_recent_projects) self.edit_project_preferences_action =\ create_action(self, _("Project Preferences"), triggered=self.edit_project_preferences) self.recent_project_menu = QMenu(_("Recent Projects"), self) if self.main is not None: self.main.projects_menu_actions += [self.new_project_action, MENU_SEPARATOR, self.open_project_action, self.close_project_action, self.delete_project_action, MENU_SEPARATOR, self.recent_project_menu, self.toggle_view_action] self.setup_menu_actions() return [] def register_plugin(self): """Register plugin in Spyder's main window""" ipyconsole = self.main.ipyconsole treewidget = self.explorer.treewidget lspmgr = self.main.lspmanager self.main.add_dockwidget(self) self.explorer.sig_open_file.connect(self.main.open_file) treewidget.sig_edit.connect(self.main.editor.load) treewidget.sig_removed.connect(self.main.editor.removed) treewidget.sig_removed_tree.connect(self.main.editor.removed_tree) treewidget.sig_renamed.connect(self.main.editor.renamed) treewidget.sig_renamed_tree.connect(self.main.editor.renamed_tree) treewidget.sig_create_module.connect(self.main.editor.new) treewidget.sig_new_file.connect( lambda t: self.main.editor.new(text=t)) treewidget.sig_open_interpreter.connect( ipyconsole.create_client_from_path) treewidget.redirect_stdio.connect( self.main.redirect_internalshell_stdio) treewidget.sig_run.connect( lambda fname: ipyconsole.run_script(fname, osp.dirname(fname), '', False, False, False, True)) # New project connections. Order matters! self.sig_project_loaded.connect( lambda v: self.main.workingdirectory.chdir(v)) self.sig_project_loaded.connect( lambda v: self.main.set_window_title()) self.sig_project_loaded.connect(lspmgr.reinit_all_lsp_clients) self.sig_project_loaded.connect( lambda v: self.main.editor.setup_open_files()) self.sig_project_loaded.connect(self.update_explorer) self.sig_project_closed[object].connect( lambda v: self.main.workingdirectory.chdir( self.get_last_working_dir())) self.sig_project_closed.connect( lambda v: self.main.set_window_title()) self.sig_project_closed.connect(lspmgr.reinit_all_lsp_clients) self.sig_project_closed.connect( lambda v: self.main.editor.setup_open_files()) self.recent_project_menu.aboutToShow.connect(self.setup_menu_actions) self.main.pythonpath_changed() self.main.restore_scrollbar_position.connect( self.restore_scrollbar_position) self.sig_pythonpath_changed.connect(self.main.pythonpath_changed) self.main.editor.set_projects(self) def refresh_plugin(self): """Refresh project explorer widget""" pass def closing_plugin(self, cancelable=False): """Perform actions before parent main window is closed""" self.save_config() self.explorer.closing_widget() return True def switch_to_plugin(self): """Switch to plugin.""" # Unmaxizime currently maximized plugin if (self.main.last_plugin is not None and self.main.last_plugin.ismaximized and self.main.last_plugin is not self): self.main.maximize_dockwidget() # Show plugin only if it was already visible if self.get_option('visible_if_project_open'): if not self.toggle_view_action.isChecked(): self.toggle_view_action.setChecked(True) self.visibility_changed(True) # ------ Public API ------------------------------------------------------- def setup_menu_actions(self): """Setup and update the menu actions.""" self.recent_project_menu.clear() self.recent_projects_actions = [] if self.recent_projects: for project in self.recent_projects: if self.is_valid_project(project): name = project.replace(get_home_dir(), '~') action = create_action( self, name, icon=ima.icon('project'), triggered=( lambda _, p=project: self.open_project(path=p)) ) self.recent_projects_actions.append(action) else: self.recent_projects.remove(project) self.recent_projects_actions += [None, self.clear_recent_projects_action] else: self.recent_projects_actions = [self.clear_recent_projects_action] add_actions(self.recent_project_menu, self.recent_projects_actions) self.update_project_actions() def update_project_actions(self): """Update actions of the Projects menu""" if self.recent_projects: self.clear_recent_projects_action.setEnabled(True) else: self.clear_recent_projects_action.setEnabled(False) active = bool(self.get_active_project_path()) self.close_project_action.setEnabled(active) self.delete_project_action.setEnabled(active) self.edit_project_preferences_action.setEnabled(active) def edit_project_preferences(self): """Edit Spyder active project preferences""" from spyder.plugins.projects.confpage import ProjectPreferences if self.project_active: active_project = self.project_list[0] dlg = ProjectPreferences(self, active_project) # dlg.size_change.connect(self.set_project_prefs_size) # if self.projects_prefs_dialog_size is not None: # dlg.resize(self.projects_prefs_dialog_size) dlg.show() # dlg.check_all_settings() # dlg.pages_widget.currentChanged.connect(self.__preference_page_changed) dlg.exec_() @Slot() def create_new_project(self): """Create new project""" self.switch_to_plugin() active_project = self.current_active_project dlg = ProjectDialog(self) dlg.sig_project_creation_requested.connect(self._create_project) dlg.sig_project_creation_requested.connect(self.sig_project_created) if dlg.exec_(): if (active_project is None and self.get_option('visible_if_project_open')): self.show_explorer() self.sig_pythonpath_changed.emit() self.restart_consoles() def _create_project(self, path): """Create a new project.""" self.open_project(path=path) self.setup_menu_actions() self.add_to_recent(path) def open_project(self, path=None, restart_consoles=True, save_previous_files=True): """Open the project located in `path`""" self.switch_to_plugin() if path is None: basedir = get_home_dir() path = getexistingdirectory(parent=self, caption=_("Open project"), basedir=basedir) path = encoding.to_unicode_from_fs(path) if not self.is_valid_project(path): if path: QMessageBox.critical(self, _('Error'), _("<b>%s</b> is not a Spyder project!") % path) return else: path = encoding.to_unicode_from_fs(path) self.add_to_recent(path) # A project was not open before if self.current_active_project is None: if save_previous_files and self.main.editor is not None: self.main.editor.save_open_files() if self.main.editor is not None: self.main.editor.set_option('last_working_dir', getcwd_or_home()) if self.get_option('visible_if_project_open'): self.show_explorer() else: # We are switching projects if self.main.editor is not None: self.set_project_filenames( self.main.editor.get_open_filenames()) self.current_active_project = EmptyProject(path) self.latest_project = EmptyProject(path) self.set_option('current_project_path', self.get_active_project_path()) self.setup_menu_actions() self.sig_project_loaded.emit(path) self.sig_pythonpath_changed.emit() if restart_consoles: self.restart_consoles() def close_project(self): """ Close current project and return to a window without an active project """ if self.current_active_project: self.switch_to_plugin() if self.main.editor is not None: self.set_project_filenames( self.main.editor.get_open_filenames()) path = self.current_active_project.root_path self.current_active_project = None self.set_option('current_project_path', None) self.setup_menu_actions() self.sig_project_closed.emit(path) self.sig_pythonpath_changed.emit() if self.dockwidget is not None: self.set_option('visible_if_project_open', self.dockwidget.isVisible()) self.dockwidget.close() self.explorer.clear() self.restart_consoles() def _delete_project(self): """Delete current project.""" if self.current_active_project: self.switch_to_plugin() self.explorer.delete_project() def clear_recent_projects(self): """Clear the list of recent projects""" self.recent_projects = [] self.setup_menu_actions() def get_active_project(self): """Get the active project""" return self.current_active_project def reopen_last_project(self): """ Reopen the active project when Spyder was closed last time, if any """ current_project_path = self.get_option('current_project_path', default=None) # Needs a safer test of project existence! if current_project_path and \ self.is_valid_project(current_project_path): self.open_project(path=current_project_path, restart_consoles=False, save_previous_files=False) self.load_config() def get_project_filenames(self): """Get the list of recent filenames of a project""" recent_files = [] if self.current_active_project: recent_files = self.current_active_project.get_recent_files() elif self.latest_project: recent_files = self.latest_project.get_recent_files() return recent_files def set_project_filenames(self, recent_files): """Set the list of open file names in a project""" if (self.current_active_project and self.is_valid_project( self.current_active_project.root_path)): self.current_active_project.set_recent_files(recent_files) def get_active_project_path(self): """Get path of the active project""" active_project_path = None if self.current_active_project: active_project_path = self.current_active_project.root_path return active_project_path def get_pythonpath(self, at_start=False): """Get project path as a list to be added to PYTHONPATH""" if at_start: current_path = self.get_option('current_project_path', default=None) else: current_path = self.get_active_project_path() if current_path is None: return [] else: return [current_path] def get_last_working_dir(self): """Get the path of the last working directory""" return self.main.editor.get_option('last_working_dir', default=getcwd_or_home()) def save_config(self): """ Save configuration: opened projects & tree widget state. Also save whether dock widget is visible if a project is open. """ self.set_option('recent_projects', self.recent_projects) self.set_option('expanded_state', self.explorer.treewidget.get_expanded_state()) self.set_option('scrollbar_position', self.explorer.treewidget.get_scrollbar_position()) if self.current_active_project and self.dockwidget: self.set_option('visible_if_project_open', self.dockwidget.isVisible()) def load_config(self): """Load configuration: opened projects & tree widget state""" expanded_state = self.get_option('expanded_state', None) # Sometimes the expanded state option may be truncated in .ini file # (for an unknown reason), in this case it would be converted to a # string by 'userconfig': if is_text_string(expanded_state): expanded_state = None if expanded_state is not None: self.explorer.treewidget.set_expanded_state(expanded_state) def restore_scrollbar_position(self): """Restoring scrollbar position after main window is visible""" scrollbar_pos = self.get_option('scrollbar_position', None) if scrollbar_pos is not None: self.explorer.treewidget.set_scrollbar_position(scrollbar_pos) def update_explorer(self): """Update explorer tree""" self.explorer.setup_project(self.get_active_project_path()) def show_explorer(self): """Show the explorer""" if self.dockwidget is not None: if self.dockwidget.isHidden(): self.dockwidget.show() self.dockwidget.raise_() self.dockwidget.update() def restart_consoles(self): """Restart consoles when closing, opening and switching projects""" if self.main.ipyconsole is not None: self.main.ipyconsole.restart() def is_valid_project(self, path): """Check if a directory is a valid Spyder project""" spy_project_dir = osp.join(path, '.spyproject') if osp.isdir(path) and osp.isdir(spy_project_dir): return True else: return False def add_to_recent(self, project): """ Add an entry to recent projetcs We only maintain the list of the 10 most recent projects """ if project not in self.recent_projects: self.recent_projects.insert(0, project) self.recent_projects = self.recent_projects[:10]
def __init__(self, parent=None, name_filters=['*.py', '*.pyw'], show_all=False, show_cd_only=None, show_icontext=True): QWidget.__init__(self, parent) # Widgets self.treewidget = ExplorerTreeWidget(self, show_cd_only=show_cd_only) button_previous = QToolButton(self) button_next = QToolButton(self) button_parent = QToolButton(self) self.button_menu = QToolButton(self) menu = QMenu(self) self.action_widgets = [ button_previous, button_next, button_parent, self.button_menu ] # Actions icontext_action = create_action(self, _("Show icons and text"), toggled=self.toggle_icontext) previous_action = create_action( self, text=_("Previous"), icon=ima.icon('ArrowBack'), triggered=self.treewidget.go_to_previous_directory) next_action = create_action( self, text=_("Next"), icon=ima.icon('ArrowForward'), triggered=self.treewidget.go_to_next_directory) parent_action = create_action( self, text=_("Parent"), icon=ima.icon('ArrowUp'), triggered=self.treewidget.go_to_parent_directory) options_action = create_action(self, text='', tip=_('Options')) # Setup widgets self.treewidget.setup(name_filters=name_filters, show_all=show_all) self.treewidget.chdir(getcwd()) self.treewidget.common_actions += [None, icontext_action] button_previous.setDefaultAction(previous_action) previous_action.setEnabled(False) button_next.setDefaultAction(next_action) next_action.setEnabled(False) button_parent.setDefaultAction(parent_action) self.button_menu.setIcon(ima.icon('tooloptions')) self.button_menu.setPopupMode(QToolButton.InstantPopup) self.button_menu.setMenu(menu) add_actions(menu, self.treewidget.common_actions) options_action.setMenu(menu) self.toggle_icontext(show_icontext) icontext_action.setChecked(show_icontext) for widget in self.action_widgets: widget.setAutoRaise(True) widget.setIconSize(QSize(16, 16)) # Layouts blayout = QHBoxLayout() blayout.addWidget(button_previous) blayout.addWidget(button_next) blayout.addWidget(button_parent) blayout.addStretch() blayout.addWidget(self.button_menu) layout = QVBoxLayout() layout.addLayout(blayout) layout.addWidget(self.treewidget) self.setLayout(layout) # Signals and slots self.treewidget.set_previous_enabled.connect( previous_action.setEnabled) self.treewidget.set_next_enabled.connect(next_action.setEnabled)
class DirView(QTreeView): """Base file/directory tree view""" def __init__(self, parent=None): super(DirView, self).__init__(parent) self.name_filters = ['*.py'] self.parent_widget = parent self.show_all = None self.menu = None self.common_actions = None self.__expanded_state = None self._to_be_loaded = None self.fsmodel = None self.setup_fs_model() self._scrollbar_positions = None #---- Model def setup_fs_model(self): """Setup filesystem model""" filters = QDir.AllDirs | QDir.Files | QDir.Drives | QDir.NoDotAndDotDot self.fsmodel = QFileSystemModel(self) self.fsmodel.setFilter(filters) self.fsmodel.setNameFilterDisables(False) def install_model(self): """Install filesystem model""" self.setModel(self.fsmodel) def setup_view(self): """Setup view""" self.install_model() if not is_pyqt46: self.fsmodel.directoryLoaded.connect( lambda: self.resizeColumnToContents(0)) self.setAnimated(False) self.setSortingEnabled(True) self.sortByColumn(0, Qt.AscendingOrder) def set_name_filters(self, name_filters): """Set name filters""" self.name_filters = name_filters self.fsmodel.setNameFilters(name_filters) def set_show_all(self, state): """Toggle 'show all files' state""" if state: self.fsmodel.setNameFilters([]) else: self.fsmodel.setNameFilters(self.name_filters) def get_filename(self, index): """Return filename associated with *index*""" if index: return osp.normpath(to_text_string(self.fsmodel.filePath(index))) def get_index(self, filename): """Return index associated with filename""" return self.fsmodel.index(filename) def get_selected_filenames(self): """Return selected filenames""" if self.selectionMode() == self.ExtendedSelection: return [self.get_filename(idx) for idx in self.selectedIndexes()] else: return [self.get_filename(self.currentIndex())] def get_dirname(self, index): """Return dirname associated with *index*""" fname = self.get_filename(index) if fname: if osp.isdir(fname): return fname else: return osp.dirname(fname) #---- Tree view widget def setup(self, name_filters=['*.py', '*.pyw'], show_all=False): """Setup tree widget""" self.setup_view() self.set_name_filters(name_filters) self.show_all = show_all # Setup context menu self.menu = QMenu(self) self.common_actions = self.setup_common_actions() #---- Context menu def setup_common_actions(self): """Setup context menu common actions""" # Filters filters_action = create_action(self, _("Edit filename filters..."), None, ima.icon('filter'), triggered=self.edit_filter) # Show all files all_action = create_action(self, _("Show all files"), toggled=self.toggle_all) all_action.setChecked(self.show_all) self.toggle_all(self.show_all) return [filters_action, all_action] @Slot() def edit_filter(self): """Edit name filters""" filters, valid = QInputDialog.getText(self, _('Edit filename filters'), _('Name filters:'), QLineEdit.Normal, ", ".join(self.name_filters)) if valid: filters = [f.strip() for f in to_text_string(filters).split(',')] self.parent_widget.sig_option_changed.emit('name_filters', filters) self.set_name_filters(filters) @Slot(bool) def toggle_all(self, checked): """Toggle all files mode""" self.parent_widget.sig_option_changed.emit('show_all', checked) self.show_all = checked self.set_show_all(checked) def create_file_new_actions(self, fnames): """Return actions for submenu 'New...'""" if not fnames: return [] new_file_act = create_action( self, _("File..."), icon=ima.icon('filenew'), triggered=lambda: self.new_file(fnames[-1])) new_module_act = create_action( self, _("Module..."), icon=ima.icon('spyder'), triggered=lambda: self.new_module(fnames[-1])) new_folder_act = create_action( self, _("Folder..."), icon=ima.icon('folder_new'), triggered=lambda: self.new_folder(fnames[-1])) new_package_act = create_action( self, _("Package..."), icon=ima.icon('package_new'), triggered=lambda: self.new_package(fnames[-1])) return [ new_file_act, new_folder_act, None, new_module_act, new_package_act ] def create_file_import_actions(self, fnames): """Return actions for submenu 'Import...'""" return [] def create_file_manage_actions(self, fnames): """Return file management actions""" only_files = all([osp.isfile(_fn) for _fn in fnames]) only_modules = all([ osp.splitext(_fn)[1] in ('.py', '.pyw', '.ipy') for _fn in fnames ]) only_notebooks = all( [osp.splitext(_fn)[1] == '.ipynb' for _fn in fnames]) only_valid = all([encoding.is_text_file(_fn) for _fn in fnames]) run_action = create_action(self, _("Run"), icon=ima.icon('run'), triggered=self.run) edit_action = create_action(self, _("Edit"), icon=ima.icon('edit'), triggered=self.clicked) move_action = create_action(self, _("Move..."), icon="move.png", triggered=self.move) delete_action = create_action(self, _("Delete..."), icon=ima.icon('editdelete'), triggered=self.delete) rename_action = create_action(self, _("Rename..."), icon=ima.icon('rename'), triggered=self.rename) open_action = create_action(self, _("Open"), triggered=self.open) ipynb_convert_action = create_action(self, _("Convert to Python script"), icon=ima.icon('python'), triggered=self.convert_notebooks) actions = [] if only_modules: actions.append(run_action) if only_valid and only_files: actions.append(edit_action) else: actions.append(open_action) actions += [delete_action, rename_action] basedir = fixpath(osp.dirname(fnames[0])) if all([fixpath(osp.dirname(_fn)) == basedir for _fn in fnames]): actions.append(move_action) actions += [None] if only_notebooks and nbexporter is not None: actions.append(ipynb_convert_action) # VCS support is quite limited for now, so we are enabling the VCS # related actions only when a single file/folder is selected: dirname = fnames[0] if osp.isdir(fnames[0]) else osp.dirname(fnames[0]) if len(fnames) == 1 and vcs.is_vcs_repository(dirname): # QAction.triggered works differently for PySide and PyQt if not API == 'pyside': commit_slot = lambda _checked, fnames=[dirname]:\ self.vcs_command(fnames, 'commit') browse_slot = lambda _checked, fnames=[dirname]:\ self.vcs_command(fnames, 'browse') else: commit_slot = lambda fnames=[dirname]:\ self.vcs_command(fnames, 'commit') browse_slot = lambda fnames=[dirname]:\ self.vcs_command(fnames, 'browse') vcs_ci = create_action(self, _("Commit"), icon=ima.icon('vcs_commit'), triggered=commit_slot) vcs_log = create_action(self, _("Browse repository"), icon=ima.icon('vcs_browse'), triggered=browse_slot) actions += [None, vcs_ci, vcs_log] return actions def create_folder_manage_actions(self, fnames): """Return folder management actions""" actions = [] if os.name == 'nt': _title = _("Open command prompt here") else: _title = _("Open terminal here") action = create_action(self, _title, icon=ima.icon('cmdprompt'), triggered=lambda: self.open_terminal(fnames)) actions.append(action) _title = _("Open Python console here") action = create_action(self, _title, icon=ima.icon('python'), triggered=lambda: self.open_interpreter(fnames)) actions.append(action) return actions def create_context_menu_actions(self): """Create context menu actions""" actions = [] fnames = self.get_selected_filenames() new_actions = self.create_file_new_actions(fnames) if len(new_actions) > 1: # Creating a submenu only if there is more than one entry new_act_menu = QMenu(_('New'), self) add_actions(new_act_menu, new_actions) actions.append(new_act_menu) else: actions += new_actions import_actions = self.create_file_import_actions(fnames) if len(import_actions) > 1: # Creating a submenu only if there is more than one entry import_act_menu = QMenu(_('Import'), self) add_actions(import_act_menu, import_actions) actions.append(import_act_menu) else: actions += import_actions if actions: actions.append(None) if fnames: actions += self.create_file_manage_actions(fnames) if actions: actions.append(None) if fnames and all([osp.isdir(_fn) for _fn in fnames]): actions += self.create_folder_manage_actions(fnames) if actions: actions.append(None) actions += self.common_actions return actions def update_menu(self): """Update context menu""" self.menu.clear() add_actions(self.menu, self.create_context_menu_actions()) #---- Events def viewportEvent(self, event): """Reimplement Qt method""" # Prevent Qt from crashing or showing warnings like: # "QSortFilterProxyModel: index from wrong model passed to # mapFromSource", probably due to the fact that the file system model # is being built. See Issue 1250. # # This workaround was inspired by the following KDE bug: # https://bugs.kde.org/show_bug.cgi?id=172198 # # Apparently, this is a bug from Qt itself. self.executeDelayedItemsLayout() return QTreeView.viewportEvent(self, event) def contextMenuEvent(self, event): """Override Qt method""" self.update_menu() self.menu.popup(event.globalPos()) def keyPressEvent(self, event): """Reimplement Qt method""" if event.key() in (Qt.Key_Enter, Qt.Key_Return): self.clicked() elif event.key() == Qt.Key_F2: self.rename() elif event.key() == Qt.Key_Delete: self.delete() elif event.key() == Qt.Key_Backspace: self.go_to_parent_directory() else: QTreeView.keyPressEvent(self, event) def mouseDoubleClickEvent(self, event): """Reimplement Qt method""" QTreeView.mouseDoubleClickEvent(self, event) self.clicked() @Slot() def clicked(self): """Selected item was double-clicked or enter/return was pressed""" fnames = self.get_selected_filenames() for fname in fnames: if osp.isdir(fname): self.directory_clicked(fname) else: self.open([fname]) def directory_clicked(self, dirname): """Directory was just clicked""" pass #---- Drag def dragEnterEvent(self, event): """Drag and Drop - Enter event""" event.setAccepted(event.mimeData().hasFormat("text/plain")) def dragMoveEvent(self, event): """Drag and Drop - Move event""" if (event.mimeData().hasFormat("text/plain")): event.setDropAction(Qt.MoveAction) event.accept() else: event.ignore() def startDrag(self, dropActions): """Reimplement Qt Method - handle drag event""" data = QMimeData() data.setUrls([QUrl(fname) for fname in self.get_selected_filenames()]) drag = QDrag(self) drag.setMimeData(data) drag.exec_() #---- File/Directory actions @Slot() def open(self, fnames=None): """Open files with the appropriate application""" if fnames is None: fnames = self.get_selected_filenames() for fname in fnames: if osp.isfile(fname) and encoding.is_text_file(fname): self.parent_widget.sig_open_file.emit(fname) else: self.open_outside_spyder([fname]) def open_outside_spyder(self, fnames): """Open file outside Spyder with the appropriate application If this does not work, opening unknown file in Spyder, as text file""" for path in sorted(fnames): path = file_uri(path) ok = programs.start_file(path) if not ok: self.parent_widget.edit.emit(path) def open_terminal(self, fnames): """Open terminal""" for path in sorted(fnames): self.parent_widget.open_terminal.emit(path) def open_interpreter(self, fnames): """Open interpreter""" for path in sorted(fnames): self.parent_widget.open_interpreter.emit(path) @Slot() def run(self, fnames=None): """Run Python scripts""" if fnames is None: fnames = self.get_selected_filenames() for fname in fnames: self.parent_widget.run.emit(fname) def remove_tree(self, dirname): """Remove whole directory tree Reimplemented in project explorer widget""" shutil.rmtree(dirname, onerror=misc.onerror) def delete_file(self, fname, multiple, yes_to_all): """Delete file""" if multiple: buttons = QMessageBox.Yes|QMessageBox.YesAll| \ QMessageBox.No|QMessageBox.Cancel else: buttons = QMessageBox.Yes | QMessageBox.No if yes_to_all is None: answer = QMessageBox.warning( self, _("Delete"), _("Do you really want " "to delete <b>%s</b>?") % osp.basename(fname), buttons) if answer == QMessageBox.No: return yes_to_all elif answer == QMessageBox.Cancel: return False elif answer == QMessageBox.YesAll: yes_to_all = True try: if osp.isfile(fname): misc.remove_file(fname) self.parent_widget.removed.emit(fname) else: self.remove_tree(fname) self.parent_widget.removed_tree.emit(fname) return yes_to_all except EnvironmentError as error: action_str = _('delete') QMessageBox.critical( self, _("Project Explorer"), _("<b>Unable to %s <i>%s</i></b>" "<br><br>Error message:<br>%s") % (action_str, fname, to_text_string(error))) return False @Slot() def delete(self, fnames=None): """Delete files""" if fnames is None: fnames = self.get_selected_filenames() multiple = len(fnames) > 1 yes_to_all = None for fname in fnames: yes_to_all = self.delete_file(fname, multiple, yes_to_all) if yes_to_all is not None and not yes_to_all: # Canceled return def convert_notebook(self, fname): """Convert an IPython notebook to a Python script in editor""" try: script = nbexporter().from_filename(fname)[0] except Exception as e: QMessageBox.critical(self, _('Conversion error'), _("It was not possible to convert this " "notebook. The error is:\n\n") + \ to_text_string(e)) return self.parent_widget.sig_new_file.emit(script) @Slot() def convert_notebooks(self): """Convert IPython notebooks to Python scripts in editor""" fnames = self.get_selected_filenames() if not isinstance(fnames, (tuple, list)): fnames = [fnames] for fname in fnames: self.convert_notebook(fname) def rename_file(self, fname): """Rename file""" path, valid = QInputDialog.getText(self, _('Rename'), _('New name:'), QLineEdit.Normal, osp.basename(fname)) if valid: path = osp.join(osp.dirname(fname), to_text_string(path)) if path == fname: return if osp.exists(path): if QMessageBox.warning( self, _("Rename"), _("Do you really want to rename <b>%s</b> and " "overwrite the existing file <b>%s</b>?") % (osp.basename(fname), osp.basename(path)), QMessageBox.Yes | QMessageBox.No) == QMessageBox.No: return try: misc.rename_file(fname, path) self.parent_widget.renamed.emit(fname, path) return path except EnvironmentError as error: QMessageBox.critical( self, _("Rename"), _("<b>Unable to rename file <i>%s</i></b>" "<br><br>Error message:<br>%s") % (osp.basename(fname), to_text_string(error))) @Slot() def rename(self, fnames=None): """Rename files""" if fnames is None: fnames = self.get_selected_filenames() if not isinstance(fnames, (tuple, list)): fnames = [fnames] for fname in fnames: self.rename_file(fname) @Slot() def move(self, fnames=None): """Move files/directories""" if fnames is None: fnames = self.get_selected_filenames() orig = fixpath(osp.dirname(fnames[0])) while True: self.parent_widget.redirect_stdio.emit(False) folder = getexistingdirectory(self, _("Select directory"), orig) self.parent_widget.redirect_stdio.emit(True) if folder: folder = fixpath(folder) if folder != orig: break else: return for fname in fnames: basename = osp.basename(fname) try: misc.move_file(fname, osp.join(folder, basename)) except EnvironmentError as error: QMessageBox.critical( self, _("Error"), _("<b>Unable to move <i>%s</i></b>" "<br><br>Error message:<br>%s") % (basename, to_text_string(error))) def create_new_folder(self, current_path, title, subtitle, is_package): """Create new folder""" if current_path is None: current_path = '' if osp.isfile(current_path): current_path = osp.dirname(current_path) name, valid = QInputDialog.getText(self, title, subtitle, QLineEdit.Normal, "") if valid: dirname = osp.join(current_path, to_text_string(name)) try: os.mkdir(dirname) except EnvironmentError as error: QMessageBox.critical( self, title, _("<b>Unable " "to create folder <i>%s</i></b>" "<br><br>Error message:<br>%s") % (dirname, to_text_string(error))) finally: if is_package: fname = osp.join(dirname, '__init__.py') try: with open(fname, 'wb') as f: f.write(to_binary_string('#')) return dirname except EnvironmentError as error: QMessageBox.critical( self, title, _("<b>Unable " "to create file <i>%s</i></b>" "<br><br>Error message:<br>%s") % (fname, to_text_string(error))) def new_folder(self, basedir): """New folder""" title = _('New folder') subtitle = _('Folder name:') self.create_new_folder(basedir, title, subtitle, is_package=False) def new_package(self, basedir): """New package""" title = _('New package') subtitle = _('Package name:') self.create_new_folder(basedir, title, subtitle, is_package=True) def create_new_file(self, current_path, title, filters, create_func): """Create new file Returns True if successful""" if current_path is None: current_path = '' if osp.isfile(current_path): current_path = osp.dirname(current_path) self.parent_widget.redirect_stdio.emit(False) fname, _selfilter = getsavefilename(self, title, current_path, filters) self.parent_widget.redirect_stdio.emit(True) if fname: try: create_func(fname) return fname except EnvironmentError as error: QMessageBox.critical( self, _("New file"), _("<b>Unable to create file <i>%s</i>" "</b><br><br>Error message:<br>%s") % (fname, to_text_string(error))) def new_file(self, basedir): """New file""" title = _("New file") filters = _("All files") + " (*)" def create_func(fname): """File creation callback""" if osp.splitext(fname)[1] in ('.py', '.pyw', '.ipy'): create_script(fname) else: with open(fname, 'wb') as f: f.write(to_binary_string('')) fname = self.create_new_file(basedir, title, filters, create_func) if fname is not None: self.open([fname]) def new_module(self, basedir): """New module""" title = _("New module") filters = _("Python scripts") + " (*.py *.pyw *.ipy)" create_func = lambda fname: self.parent_widget.create_module.emit(fname ) self.create_new_file(basedir, title, filters, create_func) #----- VCS actions def vcs_command(self, fnames, action): """VCS action (commit, browse)""" try: for path in sorted(fnames): vcs.run_vcs_tool(path, action) except vcs.ActionToolNotFound as error: msg = _("For %s support, please install one of the<br/> " "following tools:<br/><br/> %s")\ % (error.vcsname, ', '.join(error.tools)) QMessageBox.critical( self, _("Error"), _("""<b>Unable to find external program.</b><br><br>%s""") % to_text_string(msg)) #----- Settings def get_scrollbar_position(self): """Return scrollbar positions""" return (self.horizontalScrollBar().value(), self.verticalScrollBar().value()) def set_scrollbar_position(self, position): """Set scrollbar positions""" # Scrollbars will be restored after the expanded state self._scrollbar_positions = position if self._to_be_loaded is not None and len(self._to_be_loaded) == 0: self.restore_scrollbar_positions() def restore_scrollbar_positions(self): """Restore scrollbar positions once tree is loaded""" hor, ver = self._scrollbar_positions self.horizontalScrollBar().setValue(hor) self.verticalScrollBar().setValue(ver) def get_expanded_state(self): """Return expanded state""" self.save_expanded_state() return self.__expanded_state def set_expanded_state(self, state): """Set expanded state""" self.__expanded_state = state self.restore_expanded_state() def save_expanded_state(self): """Save all items expanded state""" model = self.model() # If model is not installed, 'model' will be None: this happens when # using the Project Explorer without having selected a workspace yet if model is not None: self.__expanded_state = [] for idx in model.persistentIndexList(): if self.isExpanded(idx): self.__expanded_state.append(self.get_filename(idx)) def restore_directory_state(self, fname): """Restore directory expanded state""" root = osp.normpath(to_text_string(fname)) if not osp.exists(root): # Directory has been (re)moved outside Spyder return for basename in os.listdir(root): path = osp.normpath(osp.join(root, basename)) if osp.isdir(path) and path in self.__expanded_state: self.__expanded_state.pop(self.__expanded_state.index(path)) if self._to_be_loaded is None: self._to_be_loaded = [] self._to_be_loaded.append(path) self.setExpanded(self.get_index(path), True) if not self.__expanded_state and not is_pyqt46: self.fsmodel.directoryLoaded.disconnect( self.restore_directory_state) def follow_directories_loaded(self, fname): """Follow directories loaded during startup""" if self._to_be_loaded is None: return path = osp.normpath(to_text_string(fname)) if path in self._to_be_loaded: self._to_be_loaded.remove(path) if self._to_be_loaded is not None and len(self._to_be_loaded) == 0 \ and not is_pyqt46: self.fsmodel.directoryLoaded.disconnect( self.follow_directories_loaded) if self._scrollbar_positions is not None: # The tree view need some time to render branches: QTimer.singleShot(50, self.restore_scrollbar_positions) def restore_expanded_state(self): """Restore all items expanded state""" if self.__expanded_state is not None: # In the old project explorer, the expanded state was a dictionnary: if isinstance(self.__expanded_state, list) and not is_pyqt46: self.fsmodel.directoryLoaded.connect( self.restore_directory_state) self.fsmodel.directoryLoaded.connect( self.follow_directories_loaded)
class Projects(SpyderPluginWidget): """Projects plugin.""" CONF_SECTION = 'project_explorer' CONF_FILE = False # Signals sig_project_created = Signal(str, str, object) """ This signal is emitted to request the Projects plugin the creation of a project. Parameters ---------- project_path: str Location of project. project_type: str Type of project as defined by project types. project_packages: object Package to install. Currently not in use. """ sig_project_loaded = Signal(object) sig_project_closed = Signal(object) sig_pythonpath_changed = Signal() def __init__(self, parent=None): """Initialization.""" SpyderPluginWidget.__init__(self, parent) self.explorer = ProjectExplorerWidget( self, name_filters=self.get_option('name_filters'), show_hscrollbar=self.get_option('show_hscrollbar'), options_button=self.options_button, single_click_to_open=CONF.get('explorer', 'single_click_to_open'), ) layout = QVBoxLayout() layout.addWidget(self.explorer) self.setLayout(layout) self.recent_projects = self.get_option('recent_projects', default=[]) self.current_active_project = None self.latest_project = None self.watcher = WorkspaceWatcher(self) self.completions_available = False self.explorer.setup_project(self.get_active_project_path()) self.watcher.connect_signals(self) self._project_types = OrderedDict() #------ SpyderPluginWidget API --------------------------------------------- def get_plugin_title(self): """Return widget title""" return _("Project") def get_focus_widget(self): """ Return the widget to give focus to when this plugin's dockwidget is raised on top-level """ return self.explorer.treewidget def get_plugin_actions(self): """Return a list of actions related to plugin""" self.new_project_action = create_action(self, _("New Project..."), triggered=self.create_new_project) self.open_project_action = create_action(self, _("Open Project..."), triggered=lambda v: self.open_project()) self.close_project_action = create_action(self, _("Close Project"), triggered=self.close_project) self.delete_project_action = create_action(self, _("Delete Project"), triggered=self.delete_project) self.clear_recent_projects_action = create_action( self, _("Clear this list"), triggered=self.clear_recent_projects) self.recent_project_menu = QMenu(_("Recent Projects"), self) self.max_recent_action = create_action( self, _("Maximum number of recent projects..."), triggered=self.change_max_recent_projects) if self.main is not None: self.main.projects_menu_actions += [self.new_project_action, MENU_SEPARATOR, self.open_project_action, self.close_project_action, self.delete_project_action, MENU_SEPARATOR, self.recent_project_menu, self._toggle_view_action] self.setup_menu_actions() return [] def register_plugin(self): """Register plugin in Spyder's main window""" ipyconsole = self.main.ipyconsole treewidget = self.explorer.treewidget lspmgr = self.main.completions self.add_dockwidget() self.explorer.sig_open_file.connect(self.main.open_file) self.register_widget_shortcuts(treewidget) treewidget.sig_delete_project.connect(self.delete_project) treewidget.sig_edit.connect(self.main.editor.load) treewidget.sig_removed.connect(self.main.editor.removed) treewidget.sig_removed_tree.connect(self.main.editor.removed_tree) treewidget.sig_renamed.connect(self.main.editor.renamed) treewidget.sig_renamed_tree.connect(self.main.editor.renamed_tree) treewidget.sig_create_module.connect(self.main.editor.new) treewidget.sig_new_file.connect( lambda t: self.main.editor.new(text=t)) treewidget.sig_open_interpreter.connect( ipyconsole.create_client_from_path) treewidget.redirect_stdio.connect( self.main.redirect_internalshell_stdio) treewidget.sig_run.connect( lambda fname: ipyconsole.run_script(fname, osp.dirname(fname), '', False, False, False, True, False)) # New project connections. Order matters! self.sig_project_loaded.connect( lambda path: self.main.workingdirectory.chdir( directory=path, sender_plugin=self ) ) self.sig_project_loaded.connect( lambda v: self.main.set_window_title()) self.sig_project_loaded.connect( functools.partial(lspmgr.project_path_update, update_kind=WorkspaceUpdateKind.ADDITION)) self.sig_project_loaded.connect( lambda v: self.main.editor.setup_open_files()) self.sig_project_loaded.connect(self.update_explorer) self.sig_project_loaded.connect( lambda v: self.main.outlineexplorer.update_all_editors()) self.sig_project_closed[object].connect( lambda path: self.main.workingdirectory.chdir( directory=self.get_last_working_dir(), sender_plugin=self ) ) self.sig_project_closed.connect( lambda v: self.main.set_window_title()) self.sig_project_closed.connect( functools.partial(lspmgr.project_path_update, update_kind=WorkspaceUpdateKind.DELETION)) self.sig_project_closed.connect( lambda v: self.main.editor.setup_open_files()) self.sig_project_closed.connect( lambda v: self.main.outlineexplorer.update_all_editors()) self.recent_project_menu.aboutToShow.connect(self.setup_menu_actions) self.main.restore_scrollbar_position.connect( self.restore_scrollbar_position) self.sig_pythonpath_changed.connect(self.main.pythonpath_changed) self.main.editor.set_projects(self) self.sig_project_loaded.connect( lambda v: self.main.editor.set_current_project_path(v)) self.sig_project_closed.connect( lambda v: self.main.editor.set_current_project_path()) # Connect to file explorer to keep single click to open files in sync self.main.explorer.fileexplorer.sig_option_changed.connect( self.set_single_click_to_open ) self.register_project_type(self, EmptyProject) def set_single_click_to_open(self, option, value): """Set single click to open files and directories.""" if option == 'single_click_to_open': self.explorer.treewidget.set_single_click_to_open(value) def closing_plugin(self, cancelable=False): """Perform actions before parent main window is closed""" self.save_config() self.explorer.closing_widget() return True def unmaximize(self): """Unmaximize the currently maximized plugin, if not self.""" if (self.main.last_plugin is not None and self.main.last_plugin._ismaximized and self.main.last_plugin is not self): self.main.maximize_dockwidget() def build_opener(self, project): """Build function opening passed project""" def opener(*args, **kwargs): self.open_project(path=project) return opener # ------ Public API ------------------------------------------------------- def on_first_registration(self): """Action to be performed on first plugin registration""" # TODO: Uncomment for Spyder 5 # self.tabify(self.main.explorer) def setup_menu_actions(self): """Setup and update the menu actions.""" self.recent_project_menu.clear() self.recent_projects_actions = [] if self.recent_projects: for project in self.recent_projects: if self.is_valid_project(project): name = project.replace(get_home_dir(), '~') action = create_action( self, name, icon=ima.icon('project'), triggered=self.build_opener(project), ) self.recent_projects_actions.append(action) else: self.recent_projects.remove(project) self.recent_projects_actions += [ None, self.clear_recent_projects_action, self.max_recent_action ] else: self.recent_projects_actions = [self.clear_recent_projects_action, self.max_recent_action] add_actions(self.recent_project_menu, self.recent_projects_actions) self.update_project_actions() def update_project_actions(self): """Update actions of the Projects menu""" if self.recent_projects: self.clear_recent_projects_action.setEnabled(True) else: self.clear_recent_projects_action.setEnabled(False) active = bool(self.get_active_project_path()) self.close_project_action.setEnabled(active) self.delete_project_action.setEnabled(active) @Slot() def create_new_project(self): """Create new project.""" self.unmaximize() active_project = self.current_active_project dlg = ProjectDialog(self, project_types=self.get_project_types()) result = dlg.exec_() data = dlg.project_data root_path = data.get("root_path", None) project_type = data.get("project_type", EmptyProject.ID) if result: # A project was not open before if active_project is None: if self.get_option('visible_if_project_open'): self.show_explorer() else: # We are switching projects. # TODO: Don't emit sig_project_closed when we support # multiple workspaces. self.sig_project_closed.emit(active_project.root_path) self._create_project(root_path, project_type_id=project_type) self.sig_pythonpath_changed.emit() self.restart_consoles() dlg.close() def _create_project(self, root_path, project_type_id=EmptyProject.ID, packages=None): """Create a new project.""" project_types = self.get_project_types() if project_type_id in project_types: project_type_class = project_types[project_type_id] project = project_type_class( root_path=root_path, parent_plugin=project_type_class._PARENT_PLUGIN, ) created_succesfully, message = project.create_project() if not created_succesfully: QMessageBox.warning(self, "Project creation", message) shutil.rmtree(root_path, ignore_errors=True) return # TODO: In a subsequent PR return a value and emit based on that self.sig_project_created.emit(root_path, project_type_id, packages) self.open_project(path=root_path, project=project) else: if not running_under_pytest(): QMessageBox.critical( self, _('Error'), _("<b>{}</b> is not a registered Spyder project " "type!").format(project_type) ) def open_project(self, path=None, project=None, restart_consoles=True, save_previous_files=True, workdir=None): """Open the project located in `path`.""" self.unmaximize() if path is None: basedir = get_home_dir() path = getexistingdirectory(parent=self, caption=_("Open project"), basedir=basedir) path = encoding.to_unicode_from_fs(path) if not self.is_valid_project(path): if path: QMessageBox.critical( self, _('Error'), _("<b>%s</b> is not a Spyder project!") % path, ) return else: path = encoding.to_unicode_from_fs(path) if project is None: project_type_class = self._load_project_type_class(path) project = project_type_class( root_path=path, parent_plugin=project_type_class._PARENT_PLUGIN, ) # A project was not open before if self.current_active_project is None: if save_previous_files and self.main.editor is not None: self.main.editor.save_open_files() if self.main.editor is not None: self.main.editor.set_option('last_working_dir', getcwd_or_home()) if self.get_option('visible_if_project_open'): self.show_explorer() else: # We are switching projects if self.main.editor is not None: self.set_project_filenames( self.main.editor.get_open_filenames()) # TODO: Don't emit sig_project_closed when we support # multiple workspaces. self.sig_project_closed.emit( self.current_active_project.root_path) self.current_active_project = project self.latest_project = project self.add_to_recent(path) self.set_option('current_project_path', self.get_active_project_path()) self.setup_menu_actions() if workdir and osp.isdir(workdir): self.sig_project_loaded.emit(workdir) else: self.sig_project_loaded.emit(path) self.sig_pythonpath_changed.emit() self.watcher.start(path) if restart_consoles: self.restart_consoles() open_successfully, message = project.open_project() if not open_successfully: QMessageBox.warning(self, "Project open", message) def close_project(self): """ Close current project and return to a window without an active project """ if self.current_active_project: self.unmaximize() if self.main.editor is not None: self.set_project_filenames( self.main.editor.get_open_filenames()) path = self.current_active_project.root_path closed_sucessfully, message = ( self.current_active_project.close_project()) if not closed_sucessfully: QMessageBox.warning(self, "Project close", message) self.current_active_project = None self.set_option('current_project_path', None) self.setup_menu_actions() self.sig_project_closed.emit(path) self.sig_pythonpath_changed.emit() if self.dockwidget is not None: self.set_option('visible_if_project_open', self.dockwidget.isVisible()) self.dockwidget.close() self.explorer.clear() self.restart_consoles() self.watcher.stop() def delete_project(self): """ Delete the current project without deleting the files in the directory. """ if self.current_active_project: self.unmaximize() path = self.current_active_project.root_path buttons = QMessageBox.Yes | QMessageBox.No answer = QMessageBox.warning( self, _("Delete"), _("Do you really want to delete <b>{filename}</b>?<br><br>" "<b>Note:</b> This action will only delete the project. " "Its files are going to be preserved on disk." ).format(filename=osp.basename(path)), buttons) if answer == QMessageBox.Yes: try: self.close_project() shutil.rmtree(osp.join(path, '.spyproject')) except EnvironmentError as error: QMessageBox.critical( self, _("Project Explorer"), _("<b>Unable to delete <i>{varpath}</i></b>" "<br><br>The error message was:<br>{error}" ).format(varpath=path, error=to_text_string(error))) def clear_recent_projects(self): """Clear the list of recent projects""" self.recent_projects = [] self.setup_menu_actions() def change_max_recent_projects(self): """Change max recent projects entries.""" mrf, valid = QInputDialog.getInt( self, _('Projects'), _('Maximum number of recent projects'), self.get_option('max_recent_projects'), 1, 35) if valid: self.set_option('max_recent_projects', mrf) def get_active_project(self): """Get the active project""" return self.current_active_project def reopen_last_project(self): """ Reopen the active project when Spyder was closed last time, if any """ current_project_path = self.get_option('current_project_path', default=None) # Needs a safer test of project existence! if (current_project_path and self.is_valid_project(current_project_path)): self.open_project(path=current_project_path, restart_consoles=False, save_previous_files=False) self.load_config() def get_project_filenames(self): """Get the list of recent filenames of a project""" recent_files = [] if self.current_active_project: recent_files = self.current_active_project.get_recent_files() elif self.latest_project: recent_files = self.latest_project.get_recent_files() return recent_files def set_project_filenames(self, recent_files): """Set the list of open file names in a project""" if (self.current_active_project and self.is_valid_project( self.current_active_project.root_path)): self.current_active_project.set_recent_files(recent_files) def get_active_project_path(self): """Get path of the active project""" active_project_path = None if self.current_active_project: active_project_path = self.current_active_project.root_path return active_project_path def get_pythonpath(self, at_start=False): """Get project path as a list to be added to PYTHONPATH""" if at_start: current_path = self.get_option('current_project_path', default=None) else: current_path = self.get_active_project_path() if current_path is None: return [] else: return [current_path] def get_last_working_dir(self): """Get the path of the last working directory""" return self.main.editor.get_option('last_working_dir', default=getcwd_or_home()) def save_config(self): """ Save configuration: opened projects & tree widget state. Also save whether dock widget is visible if a project is open. """ self.set_option('recent_projects', self.recent_projects) self.set_option('expanded_state', self.explorer.treewidget.get_expanded_state()) self.set_option('scrollbar_position', self.explorer.treewidget.get_scrollbar_position()) if self.current_active_project and self.dockwidget: self.set_option('visible_if_project_open', self.dockwidget.isVisible()) def load_config(self): """Load configuration: opened projects & tree widget state""" expanded_state = self.get_option('expanded_state', None) # Sometimes the expanded state option may be truncated in .ini file # (for an unknown reason), in this case it would be converted to a # string by 'userconfig': if is_text_string(expanded_state): expanded_state = None if expanded_state is not None: self.explorer.treewidget.set_expanded_state(expanded_state) def restore_scrollbar_position(self): """Restoring scrollbar position after main window is visible""" scrollbar_pos = self.get_option('scrollbar_position', None) if scrollbar_pos is not None: self.explorer.treewidget.set_scrollbar_position(scrollbar_pos) def update_explorer(self): """Update explorer tree""" self.explorer.setup_project(self.get_active_project_path()) def show_explorer(self): """Show the explorer""" if self.dockwidget is not None: if self.dockwidget.isHidden(): self.dockwidget.show() self.dockwidget.raise_() self.dockwidget.update() def restart_consoles(self): """Restart consoles when closing, opening and switching projects""" if self.main.ipyconsole is not None: self.main.ipyconsole.restart() def is_valid_project(self, path): """Check if a directory is a valid Spyder project""" spy_project_dir = osp.join(path, '.spyproject') return osp.isdir(path) and osp.isdir(spy_project_dir) def add_to_recent(self, project): """ Add an entry to recent projetcs We only maintain the list of the 10 most recent projects """ if project not in self.recent_projects: self.recent_projects.insert(0, project) if len(self.recent_projects) > self.get_option('max_recent_projects'): self.recent_projects.pop(-1) def start_workspace_services(self): """Enable LSP workspace functionality.""" self.completions_available = True if self.current_active_project: path = self.get_active_project_path() self.notify_project_open(path) def stop_workspace_services(self): """Disable LSP workspace functionality.""" self.completions_available = False def emit_request(self, method, params, requires_response): """Send request/notification/response to all LSP servers.""" params['requires_response'] = requires_response params['response_instance'] = self self.main.completions.broadcast_notification(method, params) @Slot(str, dict) def handle_response(self, method, params): """Method dispatcher for LSP requests.""" if method in self.handler_registry: handler_name = self.handler_registry[method] handler = getattr(self, handler_name) handler(params) @Slot(str, str, bool) @request(method=LSPRequestTypes.WORKSPACE_WATCHED_FILES_UPDATE, requires_response=False) def file_moved(self, src_file, dest_file, is_dir): """Notify LSP server about a file that is moved.""" # LSP specification only considers file updates if is_dir: return deletion_entry = { 'file': src_file, 'kind': FileChangeType.DELETED } addition_entry = { 'file': dest_file, 'kind': FileChangeType.CREATED } entries = [addition_entry, deletion_entry] params = { 'params': entries } return params @request(method=LSPRequestTypes.WORKSPACE_WATCHED_FILES_UPDATE, requires_response=False) @Slot(str, bool) def file_created(self, src_file, is_dir): """Notify LSP server about file creation.""" if is_dir: return params = { 'params': [{ 'file': src_file, 'kind': FileChangeType.CREATED }] } return params @request(method=LSPRequestTypes.WORKSPACE_WATCHED_FILES_UPDATE, requires_response=False) @Slot(str, bool) def file_deleted(self, src_file, is_dir): """Notify LSP server about file deletion.""" if is_dir: return params = { 'params': [{ 'file': src_file, 'kind': FileChangeType.DELETED }] } return params @request(method=LSPRequestTypes.WORKSPACE_WATCHED_FILES_UPDATE, requires_response=False) @Slot(str, bool) def file_modified(self, src_file, is_dir): """Notify LSP server about file modification.""" if is_dir: return params = { 'params': [{ 'file': src_file, 'kind': FileChangeType.CHANGED }] } return params @request(method=LSPRequestTypes.WORKSPACE_FOLDERS_CHANGE, requires_response=False) def notify_project_open(self, path): """Notify LSP server about project path availability.""" params = { 'folder': path, 'instance': self, 'kind': 'addition' } return params @request(method=LSPRequestTypes.WORKSPACE_FOLDERS_CHANGE, requires_response=False) def notify_project_close(self, path): """Notify LSP server to unregister project path.""" params = { 'folder': path, 'instance': self, 'kind': 'deletion' } return params @handles(LSPRequestTypes.WORKSPACE_APPLY_EDIT) @request(method=LSPRequestTypes.WORKSPACE_APPLY_EDIT, requires_response=False) def handle_workspace_edit(self, params): """Apply edits to multiple files and notify server about success.""" edits = params['params'] response = { 'applied': False, 'error': 'Not implemented', 'language': edits['language'] } return response # --- New API: # ------------------------------------------------------------------------ def _load_project_type_class(self, path): """ Load a project type class from the config project folder directly. Notes ----- This is done directly, since using the EmptyProject would rewrite the value in the constructor. If the project found has not been registered as a valid project type, the EmptyProject type will be returned. Returns ------- spyder.plugins.projects.api.BaseProjectType Loaded project type class. """ fpath = osp.join( path, get_project_config_folder(), 'config', WORKSPACE + ".ini") project_type_id = EmptyProject.ID if osp.isfile(fpath): config = configparser.ConfigParser() config.read(fpath) project_type_id = config[WORKSPACE].get( "project_type", EmptyProject.ID) EmptyProject._PARENT_PLUGIN = self project_types = self.get_project_types() project_type_class = project_types.get(project_type_id, EmptyProject) return project_type_class def register_project_type(self, parent_plugin, project_type): """ Register a new project type. Parameters ---------- parent_plugin: spyder.plugins.api.plugins.SpyderPluginV2 The parent plugin instance making the project type registration. project_type: spyder.plugins.projects.api.BaseProjectType Project type to register. """ if not issubclass(project_type, BaseProjectType): raise SpyderAPIError("A project type must subclass " "BaseProjectType!") project_id = project_type.ID if project_id in self._project_types: raise SpyderAPIError("A project type id '{}' has already been " "registered!".format(project_id)) project_type._PARENT_PLUGIN = parent_plugin self._project_types[project_id] = project_type def get_project_types(self): """ Return available registered project types. Returns ------- dict Project types dictionary. Keys are project type IDs and values are project type classes. """ return self._project_types # TODO: To be removed after migration def get_plugin(self, plugin_name): """ Return a plugin instance by providing the plugin's NAME. """ PLUGINS = self.main._PLUGINS if plugin_name in PLUGINS: return PLUGINS[plugin_name]
def __init__(self, model, *args, **kwargs): super(PlotWindow, self).__init__(*args, **kwargs) # Hide the icon in the title bar self.setWindowIcon(qta.icon('fa.circle', opacity=0)) # The central widget of the sub window will be a main window so that it # can support having tab bars self._central_widget = QMainWindow() self.setWidget(self._central_widget) loadUi(os.path.join(os.path.dirname(__file__), "ui", "plot_window.ui"), self._central_widget) # The central widget of the main window widget will be the plot self._model = model self._current_item_index = None self._plot_widget = PlotWidget(model=self._model) self._plot_widget.plotItem.setMenuEnabled(False) self._central_widget.setCentralWidget(self._plot_widget) # Setup action group for interaction modes mode_group = QActionGroup(self.tool_bar) mode_group.addAction(self._central_widget.pan_mode_action) self._central_widget.pan_mode_action.setChecked(True) mode_group.addAction(self._central_widget.zoom_mode_action) def _toggle_mode(state): view_state = self.plot_widget.plotItem.getViewBox().state.copy() view_state.update({ 'mouseMode': pg.ViewBox.RectMode if state else pg.ViewBox.PanMode }) self.plot_widget.plotItem.getViewBox().setState(view_state) # Setup plot settings options menu self.plot_settings_button = self.tool_bar.widgetForAction( self._central_widget.plot_settings_action) self.plot_settings_button.setPopupMode(QToolButton.InstantPopup) self.plot_settings_menu = QMenu(self.plot_settings_button) self.plot_settings_button.setMenu(self.plot_settings_menu) self.color_change_action = QAction("Line Color") self.plot_settings_menu.addAction(self.color_change_action) self.line_width_menu = QMenu("Line Widths") self.plot_settings_menu.addMenu(self.line_width_menu) # Setup the line width plot setting options for i in range(1, 4): act = QAction(str(i), self.line_width_menu) self.line_width_menu.addAction(act) act.triggered.connect( lambda *args, size=i: self._on_change_width(size)) # Setup connections self._central_widget.pan_mode_action.triggered.connect( lambda: _toggle_mode(False)) self._central_widget.zoom_mode_action.triggered.connect( lambda: _toggle_mode(True)) self._central_widget.linear_region_action.triggered.connect( self.plot_widget._on_add_linear_region) self._central_widget.remove_region_action.triggered.connect( self.plot_widget._on_remove_linear_region) self.color_change_action.triggered.connect(self._on_change_color) self._central_widget.export_plot_action.triggered.connect( self._on_export_plot) self._central_widget.reset_view_action.triggered.connect( lambda: self._on_reset_view())
class Projects(SpyderPluginWidget): """Projects plugin.""" CONF_SECTION = 'project_explorer' sig_pythonpath_changed = Signal() sig_project_created = Signal(object, object, object) sig_project_loaded = Signal(object) sig_project_closed = Signal(object) def __init__(self, parent=None): """Initialization.""" SpyderPluginWidget.__init__(self, parent) self.explorer = ProjectExplorerWidget( self, name_filters=self.get_option('name_filters'), show_all=self.get_option('show_all'), show_hscrollbar=self.get_option('show_hscrollbar'), options_button=self.options_button, single_click_to_open=CONF.get('explorer', 'single_click_to_open'), ) layout = QVBoxLayout() layout.addWidget(self.explorer) self.setLayout(layout) self.recent_projects = self.get_option('recent_projects', default=[]) self.current_active_project = None self.latest_project = None self.watcher = WorkspaceWatcher(self) self.lsp_ready = False self.explorer.setup_project(self.get_active_project_path()) self.watcher.connect_signals(self) #------ SpyderPluginWidget API --------------------------------------------- def get_plugin_title(self): """Return widget title""" return _("Project") def get_focus_widget(self): """ Return the widget to give focus to when this plugin's dockwidget is raised on top-level """ return self.explorer.treewidget def get_plugin_actions(self): """Return a list of actions related to plugin""" self.new_project_action = create_action( self, _("New Project..."), triggered=self.create_new_project) self.open_project_action = create_action( self, _("Open Project..."), triggered=lambda v: self.open_project()) self.close_project_action = create_action(self, _("Close Project"), triggered=self.close_project) self.delete_project_action = create_action( self, _("Delete Project"), triggered=self.delete_project) self.clear_recent_projects_action =\ create_action(self, _("Clear this list"), triggered=self.clear_recent_projects) self.edit_project_preferences_action =\ create_action(self, _("Project Preferences"), triggered=self.edit_project_preferences) self.recent_project_menu = QMenu(_("Recent Projects"), self) if self.main is not None: self.main.projects_menu_actions += [ self.new_project_action, MENU_SEPARATOR, self.open_project_action, self.close_project_action, self.delete_project_action, MENU_SEPARATOR, self.recent_project_menu, self._toggle_view_action ] self.setup_menu_actions() return [] def register_plugin(self): """Register plugin in Spyder's main window""" ipyconsole = self.main.ipyconsole treewidget = self.explorer.treewidget lspmgr = self.main.lspmanager self.add_dockwidget() self.explorer.sig_open_file.connect(self.main.open_file) self.register_widget_shortcuts(treewidget) treewidget.sig_delete_project.connect(self.delete_project) treewidget.sig_edit.connect(self.main.editor.load) treewidget.sig_removed.connect(self.main.editor.removed) treewidget.sig_removed_tree.connect(self.main.editor.removed_tree) treewidget.sig_renamed.connect(self.main.editor.renamed) treewidget.sig_renamed_tree.connect(self.main.editor.renamed_tree) treewidget.sig_create_module.connect(self.main.editor.new) treewidget.sig_new_file.connect(lambda t: self.main.editor.new(text=t)) treewidget.sig_open_interpreter.connect( ipyconsole.create_client_from_path) treewidget.redirect_stdio.connect( self.main.redirect_internalshell_stdio) treewidget.sig_run.connect(lambda fname: ipyconsole.run_script( fname, osp.dirname(fname), '', False, False, False, True)) # New project connections. Order matters! self.sig_project_loaded.connect( lambda v: self.main.workingdirectory.chdir(v)) self.sig_project_loaded.connect(lambda v: self.main.set_window_title()) self.sig_project_loaded.connect(lspmgr.reinitialize_all_clients) self.sig_project_loaded.connect( lambda v: self.main.editor.setup_open_files()) self.sig_project_loaded.connect(self.update_explorer) self.sig_project_closed[object].connect( lambda v: self.main.workingdirectory.chdir(self. get_last_working_dir())) self.sig_project_closed.connect(lambda v: self.main.set_window_title()) self.sig_project_closed.connect(lspmgr.reinitialize_all_clients) self.sig_project_closed.connect( lambda v: self.main.editor.setup_open_files()) self.recent_project_menu.aboutToShow.connect(self.setup_menu_actions) self.main.pythonpath_changed() self.main.restore_scrollbar_position.connect( self.restore_scrollbar_position) self.sig_pythonpath_changed.connect(self.main.pythonpath_changed) self.main.editor.set_projects(self) # Connect to file explorer to keep single click to open files in sync self.main.explorer.fileexplorer.sig_option_changed.connect( self.set_single_click_to_open) def set_single_click_to_open(self, option, value): """Set single click to open files and directories.""" if option == 'single_click_to_open': self.explorer.treewidget.set_single_click_to_open(value) def closing_plugin(self, cancelable=False): """Perform actions before parent main window is closed""" self.save_config() self.explorer.closing_widget() return True def switch_to_plugin(self): """Switch to plugin.""" # Unmaxizime currently maximized plugin if (self.main.last_plugin is not None and self.main.last_plugin._ismaximized and self.main.last_plugin is not self): self.main.maximize_dockwidget() # Show plugin only if it was already visible if self.get_option('visible_if_project_open'): if not self._toggle_view_action.isChecked(): self._toggle_view_action.setChecked(True) self._visibility_changed(True) # ------ Public API ------------------------------------------------------- def setup_menu_actions(self): """Setup and update the menu actions.""" self.recent_project_menu.clear() self.recent_projects_actions = [] if self.recent_projects: for project in self.recent_projects: if self.is_valid_project(project): name = project.replace(get_home_dir(), '~') action = create_action( self, name, icon=ima.icon('project'), triggered=( lambda _, p=project: self.open_project(path=p))) self.recent_projects_actions.append(action) else: self.recent_projects.remove(project) self.recent_projects_actions += [ None, self.clear_recent_projects_action ] else: self.recent_projects_actions = [self.clear_recent_projects_action] add_actions(self.recent_project_menu, self.recent_projects_actions) self.update_project_actions() def update_project_actions(self): """Update actions of the Projects menu""" if self.recent_projects: self.clear_recent_projects_action.setEnabled(True) else: self.clear_recent_projects_action.setEnabled(False) active = bool(self.get_active_project_path()) self.close_project_action.setEnabled(active) self.delete_project_action.setEnabled(active) self.edit_project_preferences_action.setEnabled(active) def edit_project_preferences(self): """Edit Spyder active project preferences""" from spyder.plugins.projects.confpage import ProjectPreferences if self.project_active: active_project = self.project_list[0] dlg = ProjectPreferences(self, active_project) # dlg.size_change.connect(self.set_project_prefs_size) # if self.projects_prefs_dialog_size is not None: # dlg.resize(self.projects_prefs_dialog_size) dlg.show() # dlg.check_all_settings() # dlg.pages_widget.currentChanged.connect(self.__preference_page_changed) dlg.exec_() @Slot() def create_new_project(self): """Create new project""" self.switch_to_plugin() active_project = self.current_active_project dlg = ProjectDialog(self) dlg.sig_project_creation_requested.connect(self._create_project) dlg.sig_project_creation_requested.connect(self.sig_project_created) if dlg.exec_(): if (active_project is None and self.get_option('visible_if_project_open')): self.show_explorer() self.sig_pythonpath_changed.emit() self.restart_consoles() def _create_project(self, path): """Create a new project.""" self.open_project(path=path) self.setup_menu_actions() self.add_to_recent(path) def open_project(self, path=None, restart_consoles=True, save_previous_files=True): """Open the project located in `path`""" self.switch_to_plugin() if path is None: basedir = get_home_dir() path = getexistingdirectory(parent=self, caption=_("Open project"), basedir=basedir) path = encoding.to_unicode_from_fs(path) if not self.is_valid_project(path): if path: QMessageBox.critical( self, _('Error'), _("<b>%s</b> is not a Spyder project!") % path) return else: path = encoding.to_unicode_from_fs(path) self.add_to_recent(path) # A project was not open before if self.current_active_project is None: if save_previous_files and self.main.editor is not None: self.main.editor.save_open_files() if self.main.editor is not None: self.main.editor.set_option('last_working_dir', getcwd_or_home()) if self.get_option('visible_if_project_open'): self.show_explorer() else: # We are switching projects if self.main.editor is not None: self.set_project_filenames( self.main.editor.get_open_filenames()) self.current_active_project = EmptyProject(path) self.latest_project = EmptyProject(path) self.set_option('current_project_path', self.get_active_project_path()) self.setup_menu_actions() self.sig_project_loaded.emit(path) self.sig_pythonpath_changed.emit() self.watcher.start(path) self.notify_project_open(path) if restart_consoles: self.restart_consoles() def close_project(self): """ Close current project and return to a window without an active project """ if self.current_active_project: self.switch_to_plugin() if self.main.editor is not None: self.set_project_filenames( self.main.editor.get_open_filenames()) path = self.current_active_project.root_path self.current_active_project = None self.set_option('current_project_path', None) self.setup_menu_actions() self.sig_project_closed.emit(path) self.sig_pythonpath_changed.emit() if self.dockwidget is not None: self.set_option('visible_if_project_open', self.dockwidget.isVisible()) self.dockwidget.close() self.explorer.clear() self.restart_consoles() self.watcher.stop() self.notify_project_close(path) def delete_project(self): """ Delete the current project without deleting the files in the directory. """ if self.current_active_project: self.switch_to_plugin() path = self.current_active_project.root_path buttons = QMessageBox.Yes | QMessageBox.No answer = QMessageBox.warning( self, _("Delete"), _("Do you really want to delete <b>{filename}</b>?<br><br>" "<b>Note:</b> This action will only delete the project. " "Its files are going to be preserved on disk.").format( filename=osp.basename(path)), buttons) if answer == QMessageBox.Yes: try: self.close_project() shutil.rmtree(osp.join(path, '.spyproject')) except EnvironmentError as error: QMessageBox.critical( self, _("Project Explorer"), _("<b>Unable to delete <i>{varpath}</i></b>" "<br><br>The error message was:<br>{error}").format( varpath=path, error=to_text_string(error))) def clear_recent_projects(self): """Clear the list of recent projects""" self.recent_projects = [] self.setup_menu_actions() def get_active_project(self): """Get the active project""" return self.current_active_project def reopen_last_project(self): """ Reopen the active project when Spyder was closed last time, if any """ current_project_path = self.get_option('current_project_path', default=None) # Needs a safer test of project existence! if current_project_path and \ self.is_valid_project(current_project_path): self.open_project(path=current_project_path, restart_consoles=False, save_previous_files=False) self.load_config() def get_project_filenames(self): """Get the list of recent filenames of a project""" recent_files = [] if self.current_active_project: recent_files = self.current_active_project.get_recent_files() elif self.latest_project: recent_files = self.latest_project.get_recent_files() return recent_files def set_project_filenames(self, recent_files): """Set the list of open file names in a project""" if (self.current_active_project and self.is_valid_project( self.current_active_project.root_path)): self.current_active_project.set_recent_files(recent_files) def get_active_project_path(self): """Get path of the active project""" active_project_path = None if self.current_active_project: active_project_path = self.current_active_project.root_path return active_project_path def get_pythonpath(self, at_start=False): """Get project path as a list to be added to PYTHONPATH""" if at_start: current_path = self.get_option('current_project_path', default=None) else: current_path = self.get_active_project_path() if current_path is None: return [] else: return [current_path] def get_last_working_dir(self): """Get the path of the last working directory""" return self.main.editor.get_option('last_working_dir', default=getcwd_or_home()) def save_config(self): """ Save configuration: opened projects & tree widget state. Also save whether dock widget is visible if a project is open. """ self.set_option('recent_projects', self.recent_projects) self.set_option('expanded_state', self.explorer.treewidget.get_expanded_state()) self.set_option('scrollbar_position', self.explorer.treewidget.get_scrollbar_position()) if self.current_active_project and self.dockwidget: self.set_option('visible_if_project_open', self.dockwidget.isVisible()) def load_config(self): """Load configuration: opened projects & tree widget state""" expanded_state = self.get_option('expanded_state', None) # Sometimes the expanded state option may be truncated in .ini file # (for an unknown reason), in this case it would be converted to a # string by 'userconfig': if is_text_string(expanded_state): expanded_state = None if expanded_state is not None: self.explorer.treewidget.set_expanded_state(expanded_state) def restore_scrollbar_position(self): """Restoring scrollbar position after main window is visible""" scrollbar_pos = self.get_option('scrollbar_position', None) if scrollbar_pos is not None: self.explorer.treewidget.set_scrollbar_position(scrollbar_pos) def update_explorer(self): """Update explorer tree""" self.explorer.setup_project(self.get_active_project_path()) def show_explorer(self): """Show the explorer""" if self.dockwidget is not None: if self.dockwidget.isHidden(): self.dockwidget.show() self.dockwidget.raise_() self.dockwidget.update() def restart_consoles(self): """Restart consoles when closing, opening and switching projects""" if self.main.ipyconsole is not None: self.main.ipyconsole.restart() def is_valid_project(self, path): """Check if a directory is a valid Spyder project""" spy_project_dir = osp.join(path, '.spyproject') if osp.isdir(path) and osp.isdir(spy_project_dir): return True else: return False def add_to_recent(self, project): """ Add an entry to recent projetcs We only maintain the list of the 10 most recent projects """ if project not in self.recent_projects: self.recent_projects.insert(0, project) self.recent_projects = self.recent_projects[:10] def register_lsp_server_settings(self, settings): """Enable LSP workspace functions.""" self.lsp_ready = True if self.current_active_project: path = self.get_active_project_path() self.notify_project_open(path) def stop_lsp_services(self): """Disable LSP workspace functions.""" self.lsp_ready = False def emit_request(self, method, params, requires_response): """Send request/notification/response to all LSP servers.""" params['requires_response'] = requires_response params['response_instance'] = self self.main.lspmanager.broadcast_request(method, params) @Slot(str, dict) def handle_response(self, method, params): """Method dispatcher for LSP requests.""" if method in self.handler_registry: handler_name = self.handler_registry[method] handler = getattr(self, handler_name) handler(params) @Slot(str, str, bool) @request(method=LSPRequestTypes.WORKSPACE_WATCHED_FILES_UPDATE, requires_response=False) def file_moved(self, src_file, dest_file, is_dir): """Notify LSP server about a file that is moved.""" # LSP specification only considers file updates if is_dir: return deletion_entry = {'file': src_file, 'kind': FileChangeType.DELETED} addition_entry = {'file': dest_file, 'kind': FileChangeType.CREATED} entries = [addition_entry, deletion_entry] params = {'params': entries} return params @request(method=LSPRequestTypes.WORKSPACE_WATCHED_FILES_UPDATE, requires_response=False) @Slot(str, bool) def file_created(self, src_file, is_dir): """Notify LSP server about file creation.""" if is_dir: return params = { 'params': [{ 'file': src_file, 'kind': FileChangeType.CREATED }] } return params @request(method=LSPRequestTypes.WORKSPACE_WATCHED_FILES_UPDATE, requires_response=False) @Slot(str, bool) def file_deleted(self, src_file, is_dir): """Notify LSP server about file deletion.""" if is_dir: return params = { 'params': [{ 'file': src_file, 'kind': FileChangeType.DELETED }] } return params @request(method=LSPRequestTypes.WORKSPACE_WATCHED_FILES_UPDATE, requires_response=False) @Slot(str, bool) def file_modified(self, src_file, is_dir): """Notify LSP server about file modification.""" if is_dir: return params = { 'params': [{ 'file': src_file, 'kind': FileChangeType.CHANGED }] } return params @request(method=LSPRequestTypes.WORKSPACE_FOLDERS_CHANGE, requires_response=False) def notify_project_open(self, path): """Notify LSP server about project path availability.""" params = {'folder': path, 'instance': self, 'kind': 'addition'} return params @request(method=LSPRequestTypes.WORKSPACE_FOLDERS_CHANGE, requires_response=False) def notify_project_close(self, path): """Notify LSP server to unregister project path.""" params = {'folder': path, 'instance': self, 'kind': 'deletion'} return params @handles(LSPRequestTypes.WORKSPACE_APPLY_EDIT) @request(method=LSPRequestTypes.WORKSPACE_APPLY_EDIT, requires_response=False) def handle_workspace_edit(self, params): """Apply edits to multiple files and notify server about success.""" edits = params['params'] response = { 'applied': False, 'error': 'Not implemented', 'language': edits['language'] } return response
class DirView(QTreeView): """Base file/directory tree view.""" sig_edit = Signal(str) sig_removed = Signal(str) sig_removed_tree = Signal(str) sig_renamed = Signal(str, str) sig_create_module = Signal(str) sig_run = Signal(str) sig_new_file = Signal(str) sig_open_terminal = Signal(str) sig_open_interpreter = Signal(str) redirect_stdio = Signal(bool) sig_add_to_project = Signal(str) def __init__(self, parent=None): super(DirView, self).__init__(parent) self.project_path = None self.name_filters = ['*.py'] self.parent_widget = parent self.show_all = None self.menu = None self.common_actions = None self.__expanded_state = None self._to_be_loaded = None self.fsmodel = None self.setup_fs_model() self._scrollbar_positions = None # --- Model def setup_fs_model(self): """Setup filesystem model.""" # On unix added Hidden to display .projectignore self.fsmodel = FileSystemModel(self) def install_model(self): """Install filesystem model.""" self.setModel(self.fsmodel) def setup_view(self): """Setup view.""" self.install_model() if not is_pyqt46: self.fsmodel.directoryLoaded.connect( lambda: self.resizeColumnToContents(0)) self.setAnimated(False) self.setSortingEnabled(True) self.sortByColumn(0, Qt.AscendingOrder) self.fsmodel.modelReset.connect(self.reset_icon_provider) self.reset_icon_provider() # Disable the view of .spyproject. self.filter_directories() def set_name_filters(self, name_filters): """Set name filters.""" self.name_filters = name_filters self.fsmodel.setNameFilters(name_filters) def set_show_all(self, state): """Toggle 'show all files' state.""" if state: self.fsmodel.setNameFilters([]) else: self.fsmodel.setNameFilters(self.name_filters) def get_filename(self, index): """Return filename associated with *index*.""" if index: return osp.normpath(to_text_string(self.fsmodel.filePath(index))) def get_index(self, filename): """Return index associated with filename.""" return self.fsmodel.index(filename) def get_selected_filenames(self): """Return selected filenames.""" if self.selectionMode() == self.ExtendedSelection: return [self.get_filename(idx) for idx in self.selectedIndexes()] else: return [self.get_filename(self.currentIndex())] def get_dirname(self, index): """Return dirname associated with *index*.""" fname = self.get_filename(index) if fname: if osp.isdir(fname): return fname else: return osp.dirname(fname) # --- Tree view widget def setup(self, name_filters=['*.py', '*.pyw'], show_all=False): """Setup tree widget.""" self.setup_view() self.set_name_filters(name_filters) self.show_all = show_all # Setup context menu self.menu = QMenu(self) self.common_actions = self.setup_common_actions() def reset_icon_provider(self): """ Reset file system model icon provider. The purpose of this is to refresh files/directories icons. """ self.fsmodel.setIconProvider(IconProvider(self)) # --- Context menu def setup_common_actions(self): """Setup context menu common actions.""" # Filters # filters_action = create_action( # self, # "Edit filename filters...", # None, # QIcon(), # ima.icon('filter'), # triggered=self.edit_filter # ) # Show all files all_action = create_action(self, "Show all files", toggled=self.toggle_all) all_action.setChecked(self.show_all) self.toggle_all(self.show_all) # return [filters_action, all_action] return [] @Slot() def edit_filter(self): """Edit name filters.""" dlg = InputDialog( title='Edit filename filters', text='Name filters:', ) if dlg.exec_(): filters = dlg.text.text() filters = [f.strip() for f in to_text_string(filters).split(',')] self.parent_widget.sig_option_changed.emit('name_filters', filters) self.set_name_filters(filters) @Slot(bool) def toggle_all(self, checked): """Toggle all files mode.""" self.parent_widget.sig_option_changed.emit('show_all', checked) self.show_all = checked self.set_show_all(checked) def create_file_new_actions(self, fnames): """Return actions for submenu 'New...'.""" if not fnames: return [] new_file_act = create_action( self, "File...", icon=QIcon(), # ima.icon('filenew'), triggered=lambda: self.new_file(fnames[-1])) new_folder_act = create_action( self, "Folder...", icon=QIcon(), # ima.icon('folder_new'), triggered=lambda: self.new_folder(fnames[-1])) return [new_file_act, new_folder_act] def create_file_import_actions(self, fnames): """Return actions for submenu 'Import...'.""" return [] def create_file_manage_actions(self, fnames): """Return file management actions.""" only_files = all(osp.isfile(_fn) for _fn in fnames) fname = fnames[0] # only_modules = all( # [ # osp.splitext(_fn)[1] in ('.py', '.pyw', '.ipy') # for _fn in fnames # ] # ) # only_notebooks = all( # [osp.splitext(_fn)[1] == '.ipynb' for _fn in fnames] # ) # only_valid = all([encoding.is_text_file(_fn) for _fn in # fnames]) # run_action = create_action( # self, # "Run", # icon=QIcon(), # ima.icon('run'), # triggered=self.run, # ) # edit_action = create_action( # self, # "Edit", # icon=QIcon(), # ima.icon('edit'), # triggered=self.clicked, # ) move_action = create_action( self, "Move...", icon=QIcon(), # "move.png", triggered=self.move, ) delete_action = create_action( self, "Delete...", icon=QIcon(), # ima.icon('editdelete'), triggered=self.delete) add_to_project_action = create_action( self, "Add to project...", icon=QIcon(), # ima.icon('rename'), triggered=lambda x, fname=fname: self.add_to_project(fname), ) rename_action = create_action( self, "Rename...", icon=QIcon(), # ima.icon('rename'), triggered=self.rename) # open_action = create_action(self, "Open", triggered=self.open) # ipynb_convert_action = create_action( # self, # "Convert to Python script", # icon=QIcon(), # ima.icon('python'), # triggered=self.convert_notebooks # ) actions = [] # if only_modules: # actions.append(run_action) # if only_valid and only_files: # actions.append(edit_action) # else: # actions.append(open_action) actions += [delete_action, rename_action] basedir = fixpath(osp.dirname(fnames[0])) if all(fixpath(osp.dirname(_fn)) == basedir for _fn in fnames): actions.append(move_action) if only_files and os.path.dirname(fname) != self.project_path: actions += [None, add_to_project_action] # if only_notebooks and nbexporter is not None: # actions.append(ipynb_convert_action) return actions def add_to_project(self, fname): self.sig_add_to_project.emit(fname) def create_folder_manage_actions(self, fnames): """Return folder management actions.""" actions = [] # if os.name == 'nt': # _title = "Open command prompt here" # else: # _title = "Open terminal here" # action = create_action( # self, # _title, # icon=QIcon(), # ima.icon('cmdprompt'), # triggered=lambda: self.open_terminal(fnames) # ) # actions.append(action) # _title = "Open Python console here" # action = create_action( # self, # _title, # icon=QIcon(), # ima.icon('python'), # triggered=lambda: self.open_interpreter(fnames) # ) # actions.append(action) return actions def create_context_menu_actions(self): """Create context menu actions.""" actions = [] fnames = self.get_selected_filenames() new_actions = self.create_file_new_actions(fnames) if len(new_actions) > 1: # Creating a submenu only if there is more than one entry new_act_menu = QMenu('New', self) add_actions(new_act_menu, new_actions) actions.append(new_act_menu) else: actions += new_actions import_actions = self.create_file_import_actions(fnames) if len(import_actions) > 1: # Creating a submenu only if there is more than one entry import_act_menu = QMenu('Import', self) add_actions(import_act_menu, import_actions) actions.append(import_act_menu) else: actions += import_actions if actions: actions.append(None) if fnames: actions += self.create_file_manage_actions(fnames) if actions: actions.append(None) if fnames and all(osp.isdir(_fn) for _fn in fnames): actions += self.create_folder_manage_actions(fnames) if actions: actions.append(None) actions += self.common_actions return actions def update_menu(self): """Update context menu""" self.menu.clear() add_actions(self.menu, self.create_context_menu_actions()) # --- Events def viewportEvent(self, event): """Reimplement Qt method""" # Prevent Qt from crashing or showing warnings like: # "QSortFilterProxyModel: index from wrong model passed to # mapFromSource", probably due to the fact that the file system model # is being built. See Issue 1250. # # This workaround was inspired by the following KDE bug: # https://bugs.kde.org/show_bug.cgi?id=172198 # # Apparently, this is a bug from Qt itself. self.executeDelayedItemsLayout() return QTreeView.viewportEvent(self, event) def contextMenuEvent(self, event): """Override Qt method""" self.update_menu() self.menu.popup(event.globalPos()) def keyPressEvent(self, event): """Reimplement Qt method.""" if event.key() in (Qt.Key_Enter, Qt.Key_Return): self.clicked() elif event.key() == Qt.Key_F2: self.rename() elif event.key() == Qt.Key_Delete: self.delete() elif event.key() == Qt.Key_Backspace: self.go_to_parent_directory() else: QTreeView.keyPressEvent(self, event) def mouseDoubleClickEvent(self, event): """Reimplement Qt method.""" QTreeView.mouseDoubleClickEvent(self, event) self.clicked() @Slot() def clicked(self): """Selected item was double-clicked or enter/return was pressed.""" fnames = self.get_selected_filenames() for fname in fnames: if osp.isdir(fname): self.directory_clicked(fname) else: self.open([fname]) def directory_clicked(self, dirname): """Directory was just clicked.""" pass # --- Drag def dragEnterEvent(self, event): """Drag and Drop - Enter event.""" event.setAccepted(event.mimeData().hasFormat("text/plain")) def dragMoveEvent(self, event): """Drag and Drop - Move event.""" if (event.mimeData().hasFormat("text/plain")): event.setDropAction(Qt.MoveAction) event.accept() else: event.ignore() def startDrag(self, dropActions): """Reimplement Qt Method - handle drag event.""" data = QMimeData() data.setUrls([QUrl(fname) for fname in self.get_selected_filenames()]) drag = QDrag(self) drag.setMimeData(data) drag.exec_() # --- File/Directory actions @Slot() def open(self, fnames=None): """Open files with the appropriate application.""" if fnames is None: fnames = self.get_selected_filenames() for fname in fnames: if osp.isfile(fname) and encoding.is_text_file(fname): self.parent_widget.sig_open_file.emit(fname) else: self.open_outside_spyder([fname]) def open_outside_spyder(self, fnames): """ Open file outside Spyder with the appropriate application. If this does not work, opening unknown file in Spyder, as text file. """ for path in sorted(fnames): path = file_uri(path) ok = programs.start_file(path) if not ok: self.sig_edit.emit(path) def open_terminal(self, fnames): """Open terminal.""" for path in sorted(fnames): self.sig_open_terminal.emit(path) def open_interpreter(self, fnames): """Open interpreter.""" for path in sorted(fnames): self.sig_open_interpreter.emit(path) @Slot() def run(self, fnames=None): """Run Python scripts.""" if fnames is None: fnames = self.get_selected_filenames() for fname in fnames: self.sig_run.emit(fname) def remove_tree(self, dirname): """ Remove whole directory tree. Reimplemented in project explorer widget. """ shutil.rmtree(dirname, onerror=misc.onerror) def delete_file(self, fname, multiple, yes_to_all): """Delete file.""" # if multiple: # buttons = ( # QMessageBox.Yes | QMessageBox.YesAll | QMessageBox.No | # QMessageBox.Cancel # ) # else: # buttons = QMessageBox.Yes | QMessageBox.No msg_box = MessageBoxQuestion( title="Delete", text="Do you really want to delete " "<b>{0}</b>?".format(osp.basename(fname)), ) if msg_box.exec_(): yes_to_all = True else: return False # if answer == QMessageBox.No: # return yes_to_all # elif answer == QMessageBox.Cancel: # return False # elif answer == QMessageBox.YesAll: # yes_to_all = True try: if osp.isfile(fname): misc.remove_file(fname) self.sig_removed.emit(fname) else: self.remove_tree(fname) self.sig_removed_tree.emit(fname) return yes_to_all except EnvironmentError as error: action_str = 'delete' msg_box = MessageBoxError( text="<b>Unable to %s <i>%s</i></b>" "<br><br>Error message:<br>%s" % (action_str, fname, to_text_string(error)), title='Project Explorer', ) msg_box.exec_() return False @Slot() def delete(self, fnames=None): """Delete files.""" if fnames is None: fnames = self.get_selected_filenames() multiple = len(fnames) > 1 yes_to_all = None for fname in fnames: yes_to_all = self.delete_file(fname, multiple, yes_to_all) if yes_to_all is not None and not yes_to_all: # Canceled break def convert_notebook(self, fname): """Convert an IPython notebook to a Python script in editor.""" try: script = nbexporter().from_filename(fname)[0] except Exception as e: msg_box = MessageBoxError( text="It was not possible to convert this " "notebook. The error is:\n\n" + to_text_string(e), title='Conversion error', ) msg_box.exec_() return self.sig_new_file.emit(script) @Slot() def convert_notebooks(self): """Convert IPython notebooks to Python scripts in editor.""" fnames = self.get_selected_filenames() if not isinstance(fnames, (tuple, list)): fnames = [fnames] for fname in fnames: self.convert_notebook(fname) def rename_file(self, fname): """Rename file.""" dlg = InputDialog( title='Rename', text='New name:', value=osp.basename(fname), ) if dlg.exec_(): path = dlg.text.text() path = osp.join(osp.dirname(fname), to_text_string(path)) if path == fname: return if osp.exists(path): msg_box = MessageBoxQuestion( self, title="Rename", text="Do you really want to rename <b>%s</b> and " "overwrite the existing file <b>%s</b>?" % (osp.basename(fname), osp.basename(path)), ) if not msg_box.exec_(): return try: misc.rename_file(fname, path) self.sig_renamed.emit(fname, path) return path except EnvironmentError as error: msg_box = MessageBoxError( text="<b>Unable to rename file <i>%s</i></b>" "<br><br>Error message:<br>%s" % (osp.basename(fname), to_text_string(error)), title='Rename error', ) msg_box.exec_() @Slot() def rename(self, fnames=None): """Rename files.""" if fnames is None: fnames = self.get_selected_filenames() if not isinstance(fnames, (tuple, list)): fnames = [fnames] for fname in fnames: self.rename_file(fname) @Slot() def move(self, fnames=None): """Move files/directories.""" if fnames is None: fnames = self.get_selected_filenames() orig = fixpath(osp.dirname(fnames[0])) while True: self.redirect_stdio.emit(False) folder = getexistingdirectory(self, "Select directory", orig) self.redirect_stdio.emit(True) if folder: folder = fixpath(folder) if folder != orig: break else: return for fname in fnames: basename = osp.basename(fname) try: misc.move_file(fname, osp.join(folder, basename)) except EnvironmentError as error: msg_box = MessageBoxError( text="<b>Unable to move <i>%s</i></b>" "<br><br>Error message:<br>%s" % (basename, to_text_string(error)), title='Error', ) msg_box.exec_() def create_new_folder(self, current_path, title, subtitle, is_package): """Create new folder.""" if current_path is None: current_path = '' if osp.isfile(current_path): current_path = osp.dirname(current_path) dlg = InputDialog(title=title, text=subtitle, value='') if dlg.exec_(): name = dlg.text.text() dirname = osp.join(current_path, to_text_string(name)) try: os.mkdir(dirname) except EnvironmentError as error: msg_box = MessageBoxError( text="<b>Unable " "to create folder <i>%s</i></b>" "<br><br>Error message:<br>%s" % (dirname, to_text_string(error)), title=title, ) msg_box.exec_() finally: if is_package: fname = osp.join(dirname, '__init__.py') try: with open(fname, 'wb') as f: f.write(to_binary_string('#')) return dirname except EnvironmentError as error: msg_box = MessageBoxError( text="<b>Unable " "to create file <i>%s</i></b>" "<br><br>Error message:<br>%s" % (fname, to_text_string(error)), title=title, ) msg_box.exec_() def new_folder(self, basedir): """New folder.""" title = 'New folder' subtitle = 'Folder name:' self.create_new_folder(basedir, title, subtitle, is_package=False) def new_package(self, basedir): """New package.""" title = 'New package' subtitle = 'Package name:' self.create_new_folder(basedir, title, subtitle, is_package=True) def create_new_file(self, current_path, title, filters, create_func): """ Create new file. Returns True if successful. """ if current_path is None: current_path = '' if osp.isfile(current_path): current_path = osp.dirname(current_path) self.redirect_stdio.emit(False) fname, _selfilter = getsavefilename(self, title, current_path, filters) self.redirect_stdio.emit(True) if fname: try: create_func(fname) return fname except EnvironmentError as error: msg_box = MessageBoxError( text="<b>Unable to create file <i>%s</i>" "</b><br><br>Error message:<br>%s" % (fname, to_text_string(error)), title='New file error', ) msg_box.exec_() def new_file(self, basedir): """New file.""" title = "New file" filters = "All files" + " (*)" def create_func(fname): """File creation callback.""" if osp.splitext(fname)[1] in ('.py', '.pyw', '.ipy'): create_script(fname) else: with open(fname, 'wb') as f: f.write(to_binary_string('')) fname = self.create_new_file(basedir, title, filters, create_func) if fname is not None: self.open([fname]) def new_module(self, basedir): """New module.""" title = "New module" filters = "Python scripts" + " (*.py *.pyw *.ipy)" def create_func(fname): self.sig_create_module.emit(fname) self.create_new_file(basedir, title, filters, create_func) def go_to_parent_directory(self): pass # ---- Settings def get_scrollbar_position(self): """Return scrollbar positions""" return (self.horizontalScrollBar().value(), self.verticalScrollBar().value()) def set_scrollbar_position(self, position): """Set scrollbar positions""" # Scrollbars will be restored after the expanded state self._scrollbar_positions = position if self._to_be_loaded is not None and len(self._to_be_loaded) == 0: self.restore_scrollbar_positions() def restore_scrollbar_positions(self): """Restore scrollbar positions once tree is loaded""" hor, ver = self._scrollbar_positions self.horizontalScrollBar().setValue(hor) self.verticalScrollBar().setValue(ver) def get_expanded_state(self): """Return expanded state""" self.save_expanded_state() return self.__expanded_state def set_expanded_state(self, state): """Set expanded state""" self.__expanded_state = state self.restore_expanded_state() def save_expanded_state(self): """Save all items expanded state""" model = self.model() # If model is not installed, 'model' will be None: this happens when # using the Project Explorer without having selected a workspace yet if model is not None: self.__expanded_state = [] for idx in model.persistentIndexList(): if self.isExpanded(idx): self.__expanded_state.append(self.get_filename(idx)) def restore_directory_state(self, fname): """Restore directory expanded state""" root = osp.normpath(to_text_string(fname)) if not osp.exists(root): # Directory has been (re)moved outside Spyder return for basename in os.listdir(root): path = osp.normpath(osp.join(root, basename)) if osp.isdir(path) and path in self.__expanded_state: self.__expanded_state.pop(self.__expanded_state.index(path)) if self._to_be_loaded is None: self._to_be_loaded = [] self._to_be_loaded.append(path) self.setExpanded(self.get_index(path), True) if not self.__expanded_state and not is_pyqt46: self.fsmodel.directoryLoaded.disconnect( self.restore_directory_state) def follow_directories_loaded(self, fname): """Follow directories loaded during startup""" if self._to_be_loaded is None: return path = osp.normpath(to_text_string(fname)) if path in self._to_be_loaded: self._to_be_loaded.remove(path) if (self._to_be_loaded is not None and len(self._to_be_loaded) == 0 and not is_pyqt46): self.fsmodel.directoryLoaded.disconnect( self.follow_directories_loaded) if self._scrollbar_positions is not None: # The tree view need some time to render branches: QTimer.singleShot(50, self.restore_scrollbar_positions) def restore_expanded_state(self): """Restore all items expanded state""" if self.__expanded_state is not None: # In the old explorer, the expanded state was a dictionnary: if isinstance(self.__expanded_state, list) and not is_pyqt46: self.fsmodel.directoryLoaded.connect( self.restore_directory_state) self.fsmodel.directoryLoaded.connect( self.follow_directories_loaded) def filter_directories(self): """Filter the directories to show""" index = self.get_index('.spyproject') if index is not None: self.setRowHidden(index.row(), index.parent(), True)
class BasePluginWidgetMixin(object): """ Implementation of the basic functionality for Spyder plugin widgets. """ _ALLOWED_AREAS = Qt.AllDockWidgetAreas _LOCATION = Qt.LeftDockWidgetArea _FEATURES = QDockWidget.DockWidgetClosable | QDockWidget.DockWidgetMovable def __init__(self, parent=None): super(BasePluginWidgetMixin, self).__init__() # Actions to add to the Options menu self._plugin_actions = None # Attribute to keep track if the plugin is undocked in a # separate window self._undocked_window = None self._ismaximized = False self._default_margins = None self._isvisible = False self.shortcut = None # Options buttons self.options_button = create_toolbutton(self, text=_('Options'), icon=ima.icon('tooloptions')) self.options_button.setPopupMode(QToolButton.InstantPopup) # 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;}") # Options menu self._options_menu = QMenu(self) # We decided to create our own toggle action instead of using # the one that comes with dockwidget because it's not possible # to raise and focus the plugin with it. self._toggle_view_action = None # Default actions for Options menu self._dock_action = create_action( self, _("Dock"), icon=ima.icon('dock'), tip=_("Dock the pane"), triggered=self._close_window) self._undock_action = create_action( self, _("Undock"), icon=ima.icon('undock'), tip=_("Undock the pane"), triggered=self._create_window) self._close_plugin_action = create_action( self, _("Close"), icon=ima.icon('close_pane'), tip=_("Close the pane"), triggered=self._plugin_closed) def _initialize_plugin_in_mainwindow_layout(self): """ If this is the first time the plugin is shown, perform actions to initialize plugin position in Spyder's window layout. Use on_first_registration to define the actions to be run by your plugin """ if self.get_option('first_time', True): try: self.on_first_registration() except NotImplementedError: return self.set_option('first_time', False) def _update_margins(self): """Update plugin margins""" layout = self.layout() if self._default_margins is None: self._default_margins = layout.getContentsMargins() if CONF.get('main', 'use_custom_margin'): margin = CONF.get('main', 'custom_margin') layout.setContentsMargins(*[margin]*4) else: layout.setContentsMargins(*self._default_margins) def _update_plugin_title(self): """Update plugin title, i.e. dockwidget or window title""" if self.dockwidget is not None: win = self.dockwidget elif self._undocked_window is not None: win = self._undocked_window else: return win.setWindowTitle(self.get_plugin_title()) def _create_dockwidget(self): """Add to parent QMainWindow as a dock widget""" # Creating dock widget dock = SpyderDockWidget(self.get_plugin_title(), self.main) # Set properties dock.setObjectName(self.__class__.__name__+"_dw") dock.setAllowedAreas(self._ALLOWED_AREAS) dock.setFeatures(self._FEATURES) dock.setWidget(self) self._update_margins() dock.visibilityChanged.connect(self._visibility_changed) dock.topLevelChanged.connect(self._on_top_level_changed) dock.sig_plugin_closed.connect(self._plugin_closed) self.dockwidget = dock # NOTE: Don't use the default option of CONF.get to assign a # None shortcut to plugins that don't have one. That will mess # the creation of our Keyboard Shortcuts prefs page try: context = '_' name = 'switch to {}'.format(self.CONF_SECTION) self.shortcut = CONF.get_shortcut(context, name, plugin_name=self.CONF_SECTION) except (configparser.NoSectionError, configparser.NoOptionError): pass if self.shortcut is not None and self.main is not None: sc = QShortcut(QKeySequence(self.shortcut), self.main, self.switch_to_plugin) self.register_shortcut(sc, "_", "Switch to {}".format( self.CONF_SECTION)) return (dock, self._LOCATION) def _switch_to_plugin(self): """Switch to plugin.""" if (self.main.last_plugin is not None and self.main.last_plugin._ismaximized and self.main.last_plugin is not self): self.main.maximize_dockwidget() if not self._toggle_view_action.isChecked(): self._toggle_view_action.setChecked(True) self._visibility_changed(True) @Slot() def _plugin_closed(self): """DockWidget was closed.""" if self._toggle_view_action: self._toggle_view_action.setChecked(False) def _get_font(self, rich_text=False): """Return plugin font.""" if rich_text: option = 'rich_font' font_size_delta = self.RICH_FONT_SIZE_DELTA else: option = 'font' font_size_delta = self.FONT_SIZE_DELTA return get_font(option=option, font_size_delta=font_size_delta) def set_plugin_font(self): """ Set plugin font option. Note: All plugins in Spyder use a global font. To define a different size, the plugin must define a 'FONT_SIZE_DELTA' class variable. """ raise Exception("Plugins font is based on the general settings, " "and cannot be set directly on the plugin." "This method is deprecated.") def _create_toggle_view_action(self): """Associate a toggle view action with each plugin""" title = self.get_plugin_title() if self.CONF_SECTION == 'editor': title = _('Editor') if self.shortcut is not None: action = create_action(self, title, toggled=lambda checked: self.toggle_view(checked), shortcut=QKeySequence(self.shortcut), context=Qt.WidgetShortcut) else: action = create_action(self, title, toggled=lambda checked: self.toggle_view(checked)) self._toggle_view_action = action @Slot() def _close_window(self): """Close QMainWindow instance that contains this plugin.""" if self._undocked_window is not None: self._undocked_window.close() self._undocked_window = None # Oddly, these actions can appear disabled after the Dock # action is pressed self._undock_action.setDisabled(False) self._close_plugin_action.setDisabled(False) @Slot() def _create_window(self): """Create a QMainWindow instance containing this plugin.""" self._undocked_window = window = PluginWindow(self) window.setAttribute(Qt.WA_DeleteOnClose) icon = self.get_plugin_icon() if is_text_string(icon): icon = self.get_icon(icon) window.setWindowIcon(icon) window.setWindowTitle(self.get_plugin_title()) window.setCentralWidget(self) window.resize(self.size()) self.refresh_plugin() self.dockwidget.setFloating(False) self.dockwidget.setVisible(False) window.show() @Slot(bool) def _on_top_level_changed(self, top_level): """Actions to perform when a plugin is undocked to be moved.""" if top_level: self._undock_action.setDisabled(True) else: self._undock_action.setDisabled(False) def _visibility_changed(self, enable): """Dock widget visibility has changed.""" if self.dockwidget is None: return if enable: self.dockwidget.raise_() widget = self.get_focus_widget() if widget is not None and self._undocked_window is not None: widget.setFocus() visible = self.dockwidget.isVisible() or self._ismaximized if self.DISABLE_ACTIONS_WHEN_HIDDEN: toggle_actions(self._plugin_actions, visible) self._isvisible = enable and visible if self._isvisible: self.refresh_plugin() def _refresh_actions(self): """Refresh Options menu.""" self._options_menu.clear() # Decide what additional actions to show if self._undocked_window is None: additional_actions = [MENU_SEPARATOR, self._undock_action, self._close_plugin_action] else: additional_actions = [MENU_SEPARATOR, self._dock_action] # Create actions list self._plugin_actions = self.get_plugin_actions() + additional_actions add_actions(self._options_menu, self._plugin_actions) if sys.platform == 'darwin': set_menu_icons(self._options_menu, True) def _setup(self): """ Setup Options menu, create toggle action and connect signals. """ # Creat toggle view action self._create_toggle_view_action() # Create Options menu self._plugin_actions = self.get_plugin_actions() + [MENU_SEPARATOR, self._undock_action] add_actions(self._options_menu, self._plugin_actions) self.options_button.setMenu(self._options_menu) self._options_menu.aboutToShow.connect(self._refresh_actions) # Show icons in Mac plugin menus if sys.platform == 'darwin': self._options_menu.aboutToHide.connect( lambda menu=self._options_menu: set_menu_icons(menu, False)) # Update title self.sig_update_plugin_title.connect(self._update_plugin_title) self.setWindowTitle(self.get_plugin_title()) def _register_shortcut(self, qaction_or_qshortcut, context, name, add_shortcut_to_tip=False): """Register a shortcut associated to a QAction or QShortcut.""" self.main.register_shortcut( qaction_or_qshortcut, context, name, add_shortcut_to_tip, self.CONF_SECTION) def _get_color_scheme(self): """Get the current color scheme.""" return get_color_scheme(CONF.get('appearance', 'selected')) def _add_dockwidget(self): """Add dockwidget to the main window and set it up.""" self.main.add_dockwidget(self) # This is not necessary for the Editor because it calls # _setup directly on init. if self.CONF_SECTION != 'editor': self._setup() def _tabify(self, core_plugin): """Tabify plugin next to a core plugin.""" self.main.tabify_plugins(core_plugin, self)
class LineEditChannel(LineEditBase): """ Custom line edit that uses different validators for text and url. More info: http://conda.pydata.org/docs/config.html#channel-locations-channels Valid entries: - defaults <- Special case - <some-channel-name> - https://conda.anaconda.org/<channel>/<package> - https://conda.anaconda.org/t/<token>/<package> - http://<some.custom.url>/<channel> - https://<some.custom.url>/<channel> - file:///<some-local-directory> """ VALID_RE = QRegExp('^[A-Za-z][A-Za-z0-9/_-]+$|' '^https?://.*|' '^file:///.*') sig_return_pressed = Signal() sig_escape_pressed = Signal() sig_copied = Signal() def __init__(self, *args, **kwargs): """Custom line edit that uses different validators for text and url.""" super(LineEditChannel, self).__init__(*args, **kwargs) self._validator = QRegExpValidator(self.VALID_RE) self.menu = QMenu(parent=self) self.setValidator(self._validator) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) def event(self, event): """Override Qt method.""" if (event.type() == QEvent.MouseButtonPress and event.buttons() & Qt.RightButton and not self.isEnabled()): self.show_menu(event.pos()) return True else: return super(LineEditChannel, self).event(event) def keyPressEvent(self, event): """Override Qt method.""" key = event.key() # Display a copy menu in case the widget is disabled. if event.matches(QKeySequence.Paste): clipboard = QApplication.clipboard() text = clipboard.text() if self.VALID_RE.exactMatch(text): self.setText(text) return else: if key in [Qt.Key_Return, Qt.Key_Enter]: self.sig_return_pressed.emit() elif key in [Qt.Key_Escape]: self.sig_escape_pressed.emit() super(LineEditChannel, self).keyPressEvent(event) def show_menu(self, pos): """Show copy menu for channel item.""" self.menu.clear() copy = QAction("&Copy", self.menu) copy.triggered.connect(self.copy_text) self.menu.addAction(copy) self.menu.setEnabled(True) self.menu.exec_(self.mapToGlobal(pos)) def copy_text(self): """Copy channel text to clipboard.""" clipboard = QApplication.clipboard() clipboard.setText(self.text()) self.sig_copied.emit()
class ListItemApplication(ListWidgetItemBase): """Item with custom widget for the applications list.""" ICON_SIZE = 64 def __init__( self, name=None, description=None, command=None, versions=None, image_path=None, prefix=None, needs_license=False, non_conda=False, ): """Item with custom widget for the applications list.""" super(ListItemApplication, self).__init__() self.api = AnacondaAPI() self.prefix = prefix self.name = name self.url = '' self.expired = False self.needs_license = needs_license self.description = description self.command = command self.versions = versions self.image_path = image_path if image_path else ANACONDA_ICON_256_PATH self.style_sheet = None self.timeout = 2000 self.non_conda = non_conda self._vscode_version_value = None # Widgets self.button_install = ButtonApplicationInstall("Install") # or Try! self.button_launch = ButtonApplicationLaunch("Launch") self.button_options = ButtonApplicationOptions() self.label_license = LabelApplicationLicense('') self.button_license = ButtonApplicationLicense('') self.label_icon = LabelApplicationIcon() self.label_name = LabelApplicationName(self.name) self.label_description = LabelApplicationDescription(self.description) self.button_version = ButtonApplicationVersion( to_text_string(self.version)) self.menu_options = QMenu('Application options') self.menu_versions = QMenu('Install specific version') self.pixmap = QPixmap(self.image_path) self.timer = QTimer() self.widget = WidgetApplication() self.frame_spinner = FrameApplicationSpinner() self.spinner = NavigatorSpinner(self.widget, total_width=16) lay = QHBoxLayout() lay.addWidget(self.spinner) self.frame_spinner.setLayout(lay) # Widget setup self.button_version.setFocusPolicy(Qt.NoFocus) self.button_version.setEnabled(True) self.label_description.setAlignment(Qt.AlignCenter) self.timer.setInterval(self.timeout) self.timer.setSingleShot(True) self.label_icon.setPixmap(self.pixmap) self.label_icon.setScaledContents(True) # important on High DPI! self.label_icon.setMaximumWidth(self.ICON_SIZE) self.label_icon.setMaximumHeight(self.ICON_SIZE) self.label_icon.setAlignment(Qt.AlignCenter) self.label_name.setAlignment(Qt.AlignCenter) self.label_name.setWordWrap(True) self.label_description.setWordWrap(True) self.label_description.setAlignment(Qt.AlignTop | Qt.AlignHCenter) self.frame_spinner.setVisible(False) # Layouts layout_spinner = QHBoxLayout() layout_spinner.addWidget(self.button_version, 0, Qt.AlignCenter) layout_spinner.addWidget(self.frame_spinner, 0, Qt.AlignCenter) layout_license = QHBoxLayout() layout_license.addStretch() layout_license.addWidget(self.label_license, 0, Qt.AlignCenter) layout_license.addWidget(self.button_license, 0, Qt.AlignCenter) layout_license.addStretch() layout_main = QVBoxLayout() layout_main.addWidget(self.button_options, 0, Qt.AlignRight) layout_main.addWidget(self.label_icon, 0, Qt.AlignCenter) layout_main.addWidget(self.label_name, 0, Qt.AlignCenter) layout_main.addLayout(layout_spinner) layout_main.addLayout(layout_license) layout_main.addWidget(self.label_description, 0, Qt.AlignCenter) layout_main.addWidget(self.button_launch, 0, Qt.AlignCenter) layout_main.addWidget(self.button_install, 0, Qt.AlignCenter) self.widget.setLayout(layout_main) self.widget.setStyleSheet(load_style_sheet()) self.setSizeHint(self.widget_size()) # This might help with visual quirks on the home screen self.widget.setMinimumSize(self.widget_size()) # Signals self.button_install.clicked.connect(self.install_application) self.button_launch.clicked.connect(self.launch_application) self.button_options.clicked.connect(self.actions_menu_requested) self.button_license.clicked.connect(self.launch_url) self.timer.timeout.connect(self._application_launched) # Setup self.update_status() # --- Callbacks # ------------------------------------------------------------------------- def _application_launched(self): self.button_launch.setDisabled(False) update_pointer() # --- Helpers # ------------------------------------------------------------------------- def update_style_sheet(self, style_sheet=None): """Update custom CSS stylesheet.""" if style_sheet: self.style_sheet = style_sheet else: self.style_sheet = load_style_sheet() self.menu_options.setStyleSheet(self.style_sheet) self.menu_versions.setStyleSheet(self.style_sheet) def ordered_widgets(self): """Return a list of the ordered widgets.""" return [ self.button_license, self.button_install, self.button_launch, self.button_options ] @staticmethod def widget_size(): """Return the size defined in the SASS file.""" return QSize(SASS_VARIABLES.WIDGET_APPLICATION_TOTAL_WIDTH, SASS_VARIABLES.WIDGET_APPLICATION_TOTAL_HEIGHT) def launch_url(self): """Launch signal for url click.""" self.widget.sig_url_clicked.emit(self.url) def actions_menu_requested(self): """Create and display menu for the currently selected application.""" self.menu_options.clear() self.menu_versions.clear() # Add versions menu versions = self.versions if self.versions else [] version_actions = [] for version in reversed(versions): action = create_action(self.widget, version, triggered=lambda value, version=version: self.install_application(version=version)) action.setCheckable(True) if self.version == version and self.installed: action.setChecked(True) action.setDisabled(True) version_actions.append(action) install_action = create_action( self.widget, 'Install application', triggered=lambda: self.install_application()) install_action.setEnabled(not self.installed) update_action = create_action( self.widget, 'Update application', triggered=lambda: self.update_application()) if versions and versions[-1] == self.version: update_action.setDisabled(True) else: update_action.setDisabled(False) if self.non_conda and self.name == GLOBAL_VSCODE_APP: update_action.setDisabled(True) remove_action = create_action( self.widget, 'Remove application', triggered=lambda: self.remove_application()) remove_action.setEnabled(self.installed) actions = [ install_action, update_action, remove_action, None, self.menu_versions ] add_actions(self.menu_options, actions) add_actions(self.menu_versions, version_actions) offset = QPoint(self.button_options.width(), 0) position = self.button_options.mapToGlobal(QPoint(0, 0)) self.menu_versions.setEnabled(len(versions) > 1) self.menu_options.move(position + offset) self.menu_options.exec_() def update_status(self): """Update status.""" # License check license_label_text = '' license_url_text = '' self.url = '' self.expired = False button_label = 'Install' if self.needs_license: # TODO: Fix this method to use the api license_info = self.api.get_package_license(self.name) license_days = self.api.get_days_left(license_info) end_date = license_info.get('end_date', '') self.expired = license_days == 0 plural = 's' if license_days != 1 else '' is_trial = license_info.get('type', '').lower() == 'trial' if self.installed and license_info: if is_trial and not self.expired: license_label_text = ('Trial, {days} day{plural} ' 'remaining'.format(days=license_days, plural=plural)) self.url = '' elif is_trial and self.expired: license_label_text = 'Trial expired, ' license_url_text = 'contact us' self.url = 'mailto:[email protected]' elif not is_trial and not self.expired: license_label_text = 'License expires {}'.format(end_date) self.url = '' elif not is_trial and self.expired: license_url_text = 'Renew license' self.url = 'mailto:[email protected]' elif self.installed and not bool(license_info): # Installed but no license found! license_url_text = 'No license found' self.url = 'mailto:[email protected]' else: if not self.expired: button_label = 'Install' else: button_label = 'Try' self.button_license.setText(license_url_text) self.button_license.setVisible(bool(self.url)) self.label_license.setText(license_label_text) self.label_license.setVisible(bool(license_label_text)) # Version and version updates if (self.versions and self.version != self.versions[-1] and self.installed): # The property is used with CSS to display updatable packages. self.button_version.setProperty('pressed', True) self.button_version.setToolTip('Version {0} available'.format( self.versions[-1])) else: self.button_version.setProperty('pressed', False) if not self.needs_license: self.button_install.setText(button_label) self.button_install.setVisible(not self.installed) self.button_launch.setVisible(self.installed) else: self.button_install.setText('Try' if self.expired else 'Install') self.button_launch.setVisible(not self.expired) self.button_install.setVisible(self.expired) self.button_launch.setEnabled(True) def update_versions(self, version=None, versions=None): """Update button visibility depending on update availability.""" logger.debug(str((self.name, self.dev_tool, self.installed))) if self.installed and version: self.button_options.setVisible(True) self.button_version.setText(version) self.button_version.setVisible(True) elif not self.installed and versions: self.button_install.setEnabled(True) self.button_version.setText(versions[-1]) self.button_version.setVisible(True) self.versions = versions self.version = version self.update_status() def set_loading(self, value): """Set loading status.""" self.button_install.setDisabled(value) self.button_options.setDisabled(value) self.button_launch.setDisabled(value) self.button_license.setDisabled(value) if value: self.spinner.start() else: self.spinner.stop() if self.version is None and self.versions is not None: version = self.versions[-1] else: version = self.version self.button_version.setText(version) self.button_launch.setDisabled(self.expired) self.frame_spinner.setVisible(value) self.button_version.setVisible(not value) # --- Helpers using api # ------------------------------------------------------------------------- def _vscode_version(self): """Query the vscode version for the default installation path.""" version = None if self._vscode_version_value is None: cmd = [self.api.vscode_executable(), '--version'] # print(cmd) import subprocess try: output = subprocess.check_output(cmd) if PY3: output = output.decode() output = [o for o in output.split('\n') if o and '.' in o] # print(output) if output: version = output[0] except Exception: pass # print(e) self._vscode_version_value = version else: version = self._vscode_version_value return version @property def installed(self): """Return the installed status of the package.""" version = None if self.non_conda and self.name == GLOBAL_VSCODE_APP: # TODO: Vscode program location, check existence version = self._vscode_version() elif self.prefix: version = self.api.conda_package_version(prefix=self.prefix, pkg=self.name, build=False) return bool(version) @property def version(self): """Return the current installed version or the highest version.""" version = None if self.non_conda and self.name == GLOBAL_VSCODE_APP: version = self._vscode_version() elif self.prefix: version = self.api.conda_package_version(prefix=self.prefix, pkg=self.name, build=False) if not version: version = self.versions[-1] return version # --- Application actions # ------------------------------------------------------------------------ def install_application(self, value=None, version=None, install=True): """ Update the application on the defined prefix environment. This is used for both normal install and specific version install. """ if not version: version = self.versions[-1] action = C.APPLICATION_INSTALL if install else C.APPLICATION_UPDATE self.widget.sig_conda_action_requested.emit( action, self.name, version, C.TAB_HOME, self.non_conda, ) self.set_loading(True) def remove_application(self): """Remove the application from the defined prefix environment.""" self.widget.sig_conda_action_requested.emit( C.APPLICATION_REMOVE, self.name, None, C.TAB_HOME, self.non_conda, ) self.set_loading(True) def update_application(self): """Update the application on the defined prefix environment.""" self.install_application(version=self.versions[-1], install=False) def launch_application(self): """Launch application installed in prefix environment.""" leave_path_alone = False if self.command is not None: if self.non_conda and self.name == GLOBAL_VSCODE_APP: leave_path_alone = True args = [self.command] else: args = self.command.split(' ') leave_path_alone = True self.button_launch.setDisabled(True) self.timer.setInterval(self.timeout) self.timer.start() update_pointer(Qt.BusyCursor) self.widget.sig_launch_action_requested.emit( self.name, args, leave_path_alone, self.prefix, C.TAB_HOME, self.non_conda, )
def __init__( self, name=None, description=None, command=None, versions=None, image_path=None, prefix=None, needs_license=False, non_conda=False, ): """Item with custom widget for the applications list.""" super(ListItemApplication, self).__init__() self.api = AnacondaAPI() self.prefix = prefix self.name = name self.url = '' self.expired = False self.needs_license = needs_license self.description = description self.command = command self.versions = versions self.image_path = image_path if image_path else ANACONDA_ICON_256_PATH self.style_sheet = None self.timeout = 2000 self.non_conda = non_conda self._vscode_version_value = None # Widgets self.button_install = ButtonApplicationInstall("Install") # or Try! self.button_launch = ButtonApplicationLaunch("Launch") self.button_options = ButtonApplicationOptions() self.label_license = LabelApplicationLicense('') self.button_license = ButtonApplicationLicense('') self.label_icon = LabelApplicationIcon() self.label_name = LabelApplicationName(self.name) self.label_description = LabelApplicationDescription(self.description) self.button_version = ButtonApplicationVersion( to_text_string(self.version)) self.menu_options = QMenu('Application options') self.menu_versions = QMenu('Install specific version') self.pixmap = QPixmap(self.image_path) self.timer = QTimer() self.widget = WidgetApplication() self.frame_spinner = FrameApplicationSpinner() self.spinner = NavigatorSpinner(self.widget, total_width=16) lay = QHBoxLayout() lay.addWidget(self.spinner) self.frame_spinner.setLayout(lay) # Widget setup self.button_version.setFocusPolicy(Qt.NoFocus) self.button_version.setEnabled(True) self.label_description.setAlignment(Qt.AlignCenter) self.timer.setInterval(self.timeout) self.timer.setSingleShot(True) self.label_icon.setPixmap(self.pixmap) self.label_icon.setScaledContents(True) # important on High DPI! self.label_icon.setMaximumWidth(self.ICON_SIZE) self.label_icon.setMaximumHeight(self.ICON_SIZE) self.label_icon.setAlignment(Qt.AlignCenter) self.label_name.setAlignment(Qt.AlignCenter) self.label_name.setWordWrap(True) self.label_description.setWordWrap(True) self.label_description.setAlignment(Qt.AlignTop | Qt.AlignHCenter) self.frame_spinner.setVisible(False) # Layouts layout_spinner = QHBoxLayout() layout_spinner.addWidget(self.button_version, 0, Qt.AlignCenter) layout_spinner.addWidget(self.frame_spinner, 0, Qt.AlignCenter) layout_license = QHBoxLayout() layout_license.addStretch() layout_license.addWidget(self.label_license, 0, Qt.AlignCenter) layout_license.addWidget(self.button_license, 0, Qt.AlignCenter) layout_license.addStretch() layout_main = QVBoxLayout() layout_main.addWidget(self.button_options, 0, Qt.AlignRight) layout_main.addWidget(self.label_icon, 0, Qt.AlignCenter) layout_main.addWidget(self.label_name, 0, Qt.AlignCenter) layout_main.addLayout(layout_spinner) layout_main.addLayout(layout_license) layout_main.addWidget(self.label_description, 0, Qt.AlignCenter) layout_main.addWidget(self.button_launch, 0, Qt.AlignCenter) layout_main.addWidget(self.button_install, 0, Qt.AlignCenter) self.widget.setLayout(layout_main) self.widget.setStyleSheet(load_style_sheet()) self.setSizeHint(self.widget_size()) # This might help with visual quirks on the home screen self.widget.setMinimumSize(self.widget_size()) # Signals self.button_install.clicked.connect(self.install_application) self.button_launch.clicked.connect(self.launch_application) self.button_options.clicked.connect(self.actions_menu_requested) self.button_license.clicked.connect(self.launch_url) self.timer.timeout.connect(self._application_launched) # Setup self.update_status()
class PluginWidget(QWidget, BasePluginMixin): """ Public interface for Spyder plugins. Warning: Don't override any methods present here! Signals: * sig_option_changed Example: plugin.sig_option_changed.emit('show_all', checked) * sig_show_message * sig_update_plugin_title """ sig_option_changed = Signal(str, object) sig_show_message = Signal(str, int) sig_update_plugin_title = Signal() def __init__(self, main=None): """Bind widget to a QMainWindow instance.""" super(PluginWidget, self).__init__(main) assert self.CONF_SECTION is not None self.dockwidget = None self.undocked_window = None # Check compatibility check_compatibility, message = self.check_compatibility() if not check_compatibility: self.show_compatibility_message(message) self.PLUGIN_PATH = os.path.dirname(inspect.getfile(self.__class__)) self.main = main self.default_margins = None self.plugin_actions = None self.ismaximized = False self.isvisible = False # Options button and menu self.options_button = create_toolbutton(self, text=_('Options'), icon=ima.icon('tooloptions')) self.options_button.setPopupMode(QToolButton.InstantPopup) # 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.options_menu = QMenu(self) # NOTE: Don't use the default option of CONF.get to assign a # None shortcut to plugins that don't have one. That will mess # the creation of our Keyboard Shortcuts prefs page try: self.shortcut = CONF.get('shortcuts', '_/switch to %s' % self.CONF_SECTION) except configparser.NoOptionError: pass # We decided to create our own toggle action instead of using # the one that comes with dockwidget because it's not possible # to raise and focus the plugin with it. self.toggle_view_action = None def initialize_plugin(self): """ Initialize plugin: connect signals, setup actions, etc. It must be run at the end of __init__ """ self.create_toggle_view_action() self.plugin_actions = self.get_plugin_actions() + [ MENU_SEPARATOR, self.undock_action ] add_actions(self.options_menu, self.plugin_actions) self.options_button.setMenu(self.options_menu) self.options_menu.aboutToShow.connect(self.refresh_actions) self.sig_show_message.connect(self.show_message) self.sig_update_plugin_title.connect(self.update_plugin_title) self.sig_option_changed.connect(self.set_option) self.setWindowTitle(self.get_plugin_title()) def register_shortcut(self, qaction_or_qshortcut, context, name, add_sc_to_tip=False): """ Register QAction or QShortcut to Spyder main application. if add_sc_to_tip is True, the shortcut is added to the action's tooltip """ self.main.register_shortcut(qaction_or_qshortcut, context, name, add_sc_to_tip) def register_widget_shortcuts(self, widget): """ Register widget shortcuts. Widget interface must have a method called 'get_shortcut_data' """ for qshortcut, context, name in widget.get_shortcut_data(): self.register_shortcut(qshortcut, context, name) def visibility_changed(self, enable): """ Dock widget visibility has changed. """ if self.dockwidget is None: return if enable: self.dockwidget.raise_() widget = self.get_focus_widget() if widget is not None and self.undocked_window is not None: widget.setFocus() visible = self.dockwidget.isVisible() or self.ismaximized if self.DISABLE_ACTIONS_WHEN_HIDDEN: toggle_actions(self.plugin_actions, visible) self.isvisible = enable and visible if self.isvisible: self.refresh_plugin() # To give focus to the plugin's widget def set_option(self, option, value): """ Set a plugin option in configuration file. Note: Use sig_option_changed to call it from widgets of the same or another plugin. """ CONF.set(self.CONF_SECTION, str(option), value) def get_option(self, option, default=NoDefault): """ Get a plugin's option from configuration file. """ return CONF.get(self.CONF_SECTION, option, default) def starting_long_process(self, message): """ Showing message in main window's status bar. This also changes mouse cursor to Qt.WaitCursor """ self.show_message(message) QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) QApplication.processEvents() def ending_long_process(self, message=""): """ Clear main window's status bar and restore mouse cursor. """ QApplication.restoreOverrideCursor() self.show_message(message, timeout=2000) QApplication.processEvents() def get_color_scheme(self): """ Get current color scheme. """ return get_color_scheme(CONF.get('appearance', 'selected')) def show_compatibility_message(self, message): """ Show compatibility message. """ messageBox = QMessageBox(self) messageBox.setWindowModality(Qt.NonModal) messageBox.setAttribute(Qt.WA_DeleteOnClose) messageBox.setWindowTitle('Compatibility Check') messageBox.setText(message) messageBox.setStandardButtons(QMessageBox.Ok) messageBox.show() def refresh_actions(self): """ Create options menu. """ self.options_menu.clear() # Decide what additional actions to show if self.undocked_window is None: additional_actions = [ MENU_SEPARATOR, self.undock_action, self.close_plugin_action ] else: additional_actions = [MENU_SEPARATOR, self.dock_action] # Create actions list self.plugin_actions = self.get_plugin_actions() + additional_actions add_actions(self.options_menu, self.plugin_actions)
def __init__(self, parent=None): super(BasePluginWidgetMixin, self).__init__() # Actions to add to the Options menu self._plugin_actions = None # Attribute to keep track if the plugin is undocked in a # separate window self._undocked_window = None self._ismaximized = False self._default_margins = None self._isvisible = False # Options buttons self.options_button = create_toolbutton(self, text=_('Options'), icon=ima.icon('tooloptions')) self.options_button.setPopupMode(QToolButton.InstantPopup) # 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;}") # Options menu self._options_menu = QMenu(self) # NOTE: Don't use the default option of CONF.get to assign a # None shortcut to plugins that don't have one. That will mess # the creation of our Keyboard Shortcuts prefs page try: self.shortcut = CONF.get('shortcuts', '_/switch to %s' % self.CONF_SECTION) except configparser.NoOptionError: pass # We decided to create our own toggle action instead of using # the one that comes with dockwidget because it's not possible # to raise and focus the plugin with it. self._toggle_view_action = None # Default actions for Options menu self._dock_action = create_action(self, _("Dock"), icon=ima.icon('dock'), tip=_("Dock the pane"), triggered=self._close_window) self._undock_action = create_action(self, _("Undock"), icon=ima.icon('undock'), tip=_("Undock the pane"), triggered=self._create_window) self._close_plugin_action = create_action( self, _("Close"), icon=ima.icon('close_pane'), tip=_("Close the pane"), triggered=self._plugin_closed)
class PlotWindow(QMdiSubWindow): """ Displayed plotting subwindow available in the ``QMdiArea``. """ window_removed = Signal() color_changed = Signal(PlotDataItem, QColor) width_changed = Signal(int) def __init__(self, model, *args, **kwargs): super(PlotWindow, self).__init__(*args, **kwargs) # Hide the icon in the title bar self.setWindowIcon(qta.icon('fa.circle', opacity=0)) # The central widget of the sub window will be a main window so that it # can support having tab bars self._central_widget = QMainWindow() self.setWidget(self._central_widget) loadUi(os.path.join(os.path.dirname(__file__), "ui", "plot_window.ui"), self._central_widget) # The central widget of the main window widget will be the plot self._model = model self._current_item_index = None self._plot_widget = PlotWidget(model=self._model) self._plot_widget.plotItem.setMenuEnabled(False) self._central_widget.setCentralWidget(self._plot_widget) # Setup action group for interaction modes mode_group = QActionGroup(self.tool_bar) mode_group.addAction(self._central_widget.pan_mode_action) self._central_widget.pan_mode_action.setChecked(True) mode_group.addAction(self._central_widget.zoom_mode_action) def _toggle_mode(state): view_state = self.plot_widget.plotItem.getViewBox().state.copy() view_state.update({ 'mouseMode': pg.ViewBox.RectMode if state else pg.ViewBox.PanMode }) self.plot_widget.plotItem.getViewBox().setState(view_state) # Setup plot settings options menu self.plot_settings_button = self.tool_bar.widgetForAction( self._central_widget.plot_settings_action) self.plot_settings_button.setPopupMode(QToolButton.InstantPopup) self.plot_settings_menu = QMenu(self.plot_settings_button) self.plot_settings_button.setMenu(self.plot_settings_menu) self.color_change_action = QAction("Line Color") self.plot_settings_menu.addAction(self.color_change_action) self.line_width_menu = QMenu("Line Widths") self.plot_settings_menu.addMenu(self.line_width_menu) # Setup the line width plot setting options for i in range(1, 4): act = QAction(str(i), self.line_width_menu) self.line_width_menu.addAction(act) act.triggered.connect( lambda *args, size=i: self._on_change_width(size)) # Setup connections self._central_widget.pan_mode_action.triggered.connect( lambda: _toggle_mode(False)) self._central_widget.zoom_mode_action.triggered.connect( lambda: _toggle_mode(True)) self._central_widget.linear_region_action.triggered.connect( self.plot_widget._on_add_linear_region) self._central_widget.remove_region_action.triggered.connect( self.plot_widget._on_remove_linear_region) self.color_change_action.triggered.connect(self._on_change_color) self._central_widget.export_plot_action.triggered.connect( self._on_export_plot) self._central_widget.reset_view_action.triggered.connect( lambda: self._on_reset_view()) @property def tool_bar(self): """ Return the tool bar for the embedded plot widget. """ return self._central_widget.tool_bar @property def current_item(self): """ The currently selected plot data item. """ if self._current_item_index is not None: return self.proxy_model.item_from_index(self._current_item_index) @property def plot_widget(self): """ Return the embedded plot widget """ return self._plot_widget @property def proxy_model(self): """ The proxy model defined in the internal plot widget. """ return self.plot_widget.proxy_model def closeEvent(self, event): """ Called by qt when window closes, upon which it emits the window_removed signal. Parameters ---------- event : ignored in this implementation. """ self.window_removed.emit() def _on_current_item_changed(self, current_idx, prev_idx): self._current_item_index = current_idx def _on_reset_view(self): """ Resets the visible range of the plot taking into consideration only the PlotDataItem objects currently attached. """ self.plot_widget.autoRange(items=[ item for item in self.plot_widget.listDataItems() if isinstance(item, PlotDataItem) ]) self.plot_widget.sigRangeChanged.emit(*self.plot_widget.viewRange()) def _on_change_color(self): """ Listens for color changed events in plot windows, gets the currently selected item in the data list view, and changes the stored color value. """ # If there is no currently selected rows, raise an error if self.current_item is None: message_box = QMessageBox() message_box.setText("No item selected, cannot change color.") message_box.setIcon(QMessageBox.Warning) message_box.setInformativeText( "There is currently no item selected. Please select an item " "before changing its plot color.") message_box.exec() return color = QColorDialog.getColor(options=QColorDialog.ShowAlphaChannel) if color.isValid(): self.current_item.color = color.toRgb() self.color_changed.emit(self.current_item, self.current_item.color) def _on_change_width(self, size): self.plot_widget.change_width(size) self.width_changed.emit(size) def _on_export_plot(self): file_path, key = compat.getsavefilename( filters=";;".join(EXPORT_FILTERS.keys())) if key == '': return exporter = EXPORT_FILTERS[key](self.plot_widget.plotItem) # TODO: Current issue in pyqtgraph where the user cannot explicitly # define the output size. Fix incoming. # plot_size_dialog = PlotSizeDialog(self) # plot_size_dialog.height_line_edit.setText( # str(int(exporter.params.param('height').value()))) # plot_size_dialog.width_line_edit.setText( # str(int(exporter.params.param('width').value()))) # # if key != "*.svg": # if plot_size_dialog.exec_(): # exporter.params.param('height').setValue(int(exporter.params.param('height').value()), # blockSignal=exporter.heightChanged) # exporter.params.param('width').setValue(int(exporter.params.param('height').value()), # blockSignal=exporter.widthChanged) # else: # return exporter.export(file_path)
class NotebookPlugin(SpyderPluginWidget): """IPython Notebook plugin.""" CONF_SECTION = 'notebook' CONF_DEFAULTS = [( CONF_SECTION, { 'recent_notebooks': [], # Items in "Open recent" menu 'opened_notebooks': [] })] # Notebooks to open at start focus_changed = Signal() def __init__(self, parent, testing=False): """Constructor.""" if testing: self.CONF_FILE = False SpyderPluginWidget.__init__(self, parent) self.testing = testing self.fileswitcher_dlg = None self.main = parent self.recent_notebooks = self.get_option('recent_notebooks', default=[]) self.recent_notebook_menu = QMenu(_("Open recent"), self) layout = QVBoxLayout() new_notebook_btn = create_toolbutton(self, icon=ima.icon('options_more'), tip=_('Open a new notebook'), triggered=self.create_new_client) menu_btn = create_toolbutton(self, icon=ima.icon('tooloptions'), tip=_('Options')) self.menu_actions = self.get_plugin_actions() menu_btn.setMenu(self._options_menu) menu_btn.setPopupMode(menu_btn.InstantPopup) corner_widgets = {Qt.TopRightCorner: [new_notebook_btn, menu_btn]} dark_theme = is_dark_interface() self.tabwidget = NotebookTabWidget(self, menu=self._options_menu, actions=self.menu_actions, corner_widgets=corner_widgets, dark_theme=dark_theme) self.tabwidget.currentChanged.connect(self.refresh_plugin) layout.addWidget(self.tabwidget) self.setLayout(layout) # ------ SpyderPluginMixin API -------------------------------------------- def on_first_registration(self): """Action to be performed on first plugin registration.""" self.main.tabify_plugins(self.main.editor, self) def update_font(self): """Update font from Preferences.""" # For now we're passing. We need to create an nbextension for # this. pass # ------ SpyderPluginWidget API ------------------------------------------- def get_plugin_title(self): """Return widget title.""" title = _('Notebook') return title def get_plugin_icon(self): """Return widget icon.""" return ima.icon('ipython_console') def get_focus_widget(self): """Return the widget to give focus to.""" client = self.tabwidget.currentWidget() if client is not None: return client.notebookwidget def closing_plugin(self, cancelable=False): """ Perform actions before parent main window is closed. This function closes all tabs. It stores the file names of all opened notebooks that are not temporary and all notebooks in the 'Open recent' menu in the config. """ opened_notebooks = [] for client_index in range(self.tabwidget.count()): client = self.tabwidget.widget(client_index) if (not self.tabwidget.is_welcome_client(client) and not self.tabwidget.is_newly_created(client)): opened_notebooks.append(client.filename) client.close() self.set_option('recent_notebooks', self.recent_notebooks) self.set_option('opened_notebooks', opened_notebooks) return True def refresh_plugin(self): """Refresh tabwidget.""" nb = None if self.tabwidget.count(): client = self.tabwidget.currentWidget() nb = client.notebookwidget nb.setFocus() else: nb = None self.update_notebook_actions() def get_plugin_actions(self): """Return a list of actions related to plugin.""" create_nb_action = create_action(self, _("New notebook"), icon=ima.icon('filenew'), triggered=self.create_new_client) self.save_as_action = create_action(self, _("Save as..."), icon=ima.icon('filesaveas'), triggered=self.save_as) open_action = create_action(self, _("Open..."), icon=ima.icon('fileopen'), triggered=self.open_notebook) self.open_console_action = create_action( self, _("Open console"), icon=ima.icon('ipython_console'), triggered=self.open_console) self.clear_recent_notebooks_action =\ create_action(self, _("Clear this list"), triggered=self.clear_recent_notebooks) # Plugin actions self.menu_actions = [ create_nb_action, open_action, self.recent_notebook_menu, MENU_SEPARATOR, self.save_as_action, MENU_SEPARATOR, self.open_console_action ] self.setup_menu_actions() return self.menu_actions def register_plugin(self): """Register plugin in Spyder's main window.""" super().register_plugin() self.focus_changed.connect(self.main.plugin_focus_changed) self.ipyconsole = self.main.ipyconsole # Open initial tabs filenames = self.get_option('opened_notebooks') if filenames: self.open_notebook(filenames) else: self.tabwidget.maybe_create_welcome_client() self.create_new_client() self.tabwidget.setCurrentIndex(0) # bring welcome tab to top # Connect to switcher self.switcher = self.main.switcher self.switcher.sig_mode_selected.connect(self.handle_switcher_modes) self.switcher.sig_item_selected.connect(self.handle_switcher_selection) self.recent_notebook_menu.aboutToShow.connect(self.setup_menu_actions) def check_compatibility(self): """Check compatibility for PyQt and sWebEngine.""" message = '' value = True if PYQT4 or PYSIDE: message = _("You are working with Qt4 and in order to use this " "plugin you need to have Qt5.<br><br>" "Please update your Qt and/or PyQt packages to " "meet this requirement.") value = False return value, message # ------ Public API (for clients) ----------------------------------------- def setup_menu_actions(self): """Setup and update the menu actions.""" self.recent_notebook_menu.clear() self.recent_notebooks_actions = [] if self.recent_notebooks: for notebook in self.recent_notebooks: name = notebook action = \ create_action(self, name, icon=ima.icon('filenew'), triggered=lambda v, path=notebook: self.create_new_client(filename=path)) self.recent_notebooks_actions.append(action) self.recent_notebooks_actions += \ [None, self.clear_recent_notebooks_action] else: self.recent_notebooks_actions = \ [self.clear_recent_notebooks_action] add_actions(self.recent_notebook_menu, self.recent_notebooks_actions) self.update_notebook_actions() def update_notebook_actions(self): """Update actions of the recent notebooks menu.""" if self.recent_notebooks: self.clear_recent_notebooks_action.setEnabled(True) else: self.clear_recent_notebooks_action.setEnabled(False) try: client = self.tabwidget.currentWidget() except AttributeError: # tabwidget is not yet constructed client = None if client and not self.tabwidget.is_welcome_client(client): self.save_as_action.setEnabled(True) self.open_console_action.setEnabled(True) else: self.save_as_action.setEnabled(False) self.open_console_action.setEnabled(False) def add_to_recent(self, notebook): """ Add an entry to recent notebooks. We only maintain the list of the 20 most recent notebooks. """ if notebook not in self.recent_notebooks: self.recent_notebooks.insert(0, notebook) self.recent_notebooks = self.recent_notebooks[:20] def clear_recent_notebooks(self): """Clear the list of recent notebooks.""" self.recent_notebooks = [] self.setup_menu_actions() def create_new_client(self, filename=None): """Create a new notebook or load a pre-existing one.""" # Save spyder_pythonpath before creating a client # because it's needed by our kernel spec. if not self.testing: self.set_option('main/spyder_pythonpath', self.main.get_spyder_pythonpath()) client = self.tabwidget.create_new_client(filename) if not self.tabwidget.is_newly_created(client): self.add_to_recent(filename) self.setup_menu_actions() def open_notebook(self, filenames=None): """Open a notebook from file.""" # Save spyder_pythonpath before creating a client # because it's needed by our kernel spec. if not self.testing: self.set_option('main/spyder_pythonpath', self.main.get_spyder_pythonpath()) filenames = self.tabwidget.open_notebook(filenames) for filename in filenames: self.add_to_recent(filename) self.setup_menu_actions() def save_as(self): """Save current notebook to different file.""" self.tabwidget.save_as() def open_console(self, client=None): """Open an IPython console for the given client or the current one.""" if not client: client = self.tabwidget.currentWidget() if self.ipyconsole is not None: kernel_id = client.get_kernel_id() if not kernel_id: QMessageBox.critical( self, _('Error opening console'), _('There is no kernel associated to this notebook.')) return self.ipyconsole._create_client_for_kernel(kernel_id, None, None, None) ipyclient = self.ipyconsole.get_current_client() ipyclient.allow_rename = False self.ipyconsole.rename_client_tab(ipyclient, client.get_short_name()) # ------ Public API (for FileSwitcher) ------------------------------------ def handle_switcher_modes(self, mode): """ Populate switcher with opened notebooks. List the file names of the opened notebooks with their directories in the switcher. Only handle file mode, where `mode` is empty string. """ if mode != '': return clients = [ self.tabwidget.widget(i) for i in range(self.tabwidget.count()) ] paths = [client.get_filename() for client in clients] is_unsaved = [False for client in clients] short_paths = shorten_paths(paths, is_unsaved) icon = QIcon(os.path.join(PACKAGE_PATH, 'images', 'icon.svg')) section = self.get_plugin_title() for path, short_path, client in zip(paths, short_paths, clients): title = osp.basename(path) description = osp.dirname(path) if len(path) > 75: description = short_path is_last_item = (client == clients[-1]) self.switcher.add_item(title=title, description=description, icon=icon, section=section, data=client, last_item=is_last_item) def handle_switcher_selection(self, item, mode, search_text): """ Handle user selecting item in switcher. If the selected item is not in the section of the switcher that corresponds to this plugin, then ignore it. Otherwise, switch to selected item in notebook plugin and hide the switcher. """ if item.get_section() != self.get_plugin_title(): return client = item.get_data() index = self.tabwidget.indexOf(client) self.tabwidget.setCurrentIndex(index) self.switch_to_plugin() self.switcher.hide()
class OneColumnTree(QTreeWidget): """One-column tree widget with context menu, ...""" def __init__(self, parent): QTreeWidget.__init__(self, parent) self.setItemsExpandable(True) self.setColumnCount(1) self.itemActivated.connect(self.activated) self.itemClicked.connect(self.clicked) # Setup context menu self.menu = QMenu(self) self.collapse_all_action = None self.collapse_selection_action = None self.expand_all_action = None self.expand_selection_action = None self.common_actions = self.setup_common_actions() self.__expanded_state = None self.itemSelectionChanged.connect(self.item_selection_changed) self.item_selection_changed() def activated(self, item): """Double-click event""" raise NotImplementedError def clicked(self, item): pass def set_title(self, title): self.setHeaderLabels([title]) def setup_common_actions(self): """Setup context menu common actions""" self.collapse_all_action = create_action(self, text=_('Collapse all'), icon=ima.icon('collapse'), triggered=self.collapseAll) self.expand_all_action = create_action(self, text=_('Expand all'), icon=ima.icon('expand'), triggered=self.expandAll) self.restore_action = create_action( self, text=_('Restore'), tip=_('Restore original tree layout'), icon=ima.icon('restore'), triggered=self.restore) self.collapse_selection_action = create_action( self, text=_('Collapse section'), icon=ima.icon('collapse_selection'), triggered=self.collapse_selection) self.expand_selection_action = create_action( self, text=_('Expand section'), icon=ima.icon('expand_selection'), triggered=self.expand_selection) return [ self.collapse_all_action, self.expand_all_action, None, self.collapse_selection_action, self.expand_selection_action ] def get_menu_actions(self): """Returns a list of menu actions""" items = self.selectedItems() actions = self.get_actions_from_items(items) if actions: actions.append(None) actions += self.common_actions return actions def update_menu(self): self.menu.clear() actions = self.get_menu_actions() add_actions(self.menu, actions) def get_actions_from_items(self, items): # Right here: add other actions if necessary # (reimplement this method) return [] @Slot() def restore(self): self.collapseAll() for item in self.get_top_level_items(): self.expandItem(item) def is_item_expandable(self, item): """To be reimplemented in child class See example in project explorer widget""" return True def __expand_item(self, item): if self.is_item_expandable(item): self.expandItem(item) for index in range(item.childCount()): child = item.child(index) self.__expand_item(child) @Slot() def expand_selection(self): items = self.selectedItems() if not items: items = self.get_top_level_items() for item in items: self.__expand_item(item) if items: self.scrollToItem(items[0]) def __collapse_item(self, item): self.collapseItem(item) for index in range(item.childCount()): child = item.child(index) self.__collapse_item(child) @Slot() def collapse_selection(self): items = self.selectedItems() if not items: items = self.get_top_level_items() for item in items: self.__collapse_item(item) if items: self.scrollToItem(items[0]) def item_selection_changed(self): """Item selection has changed""" is_selection = len(self.selectedItems()) > 0 self.expand_selection_action.setEnabled(is_selection) self.collapse_selection_action.setEnabled(is_selection) def get_top_level_items(self): """Iterate over top level items""" return [ self.topLevelItem(_i) for _i in range(self.topLevelItemCount()) ] def get_items(self): """Return items (excluding top level items)""" itemlist = [] def add_to_itemlist(item): for index in range(item.childCount()): citem = item.child(index) itemlist.append(citem) add_to_itemlist(citem) for tlitem in self.get_top_level_items(): add_to_itemlist(tlitem) return itemlist def get_scrollbar_position(self): return (self.horizontalScrollBar().value(), self.verticalScrollBar().value()) def set_scrollbar_position(self, position): hor, ver = position self.horizontalScrollBar().setValue(hor) self.verticalScrollBar().setValue(ver) def get_expanded_state(self): self.save_expanded_state() return self.__expanded_state def set_expanded_state(self, state): self.__expanded_state = state self.restore_expanded_state() def save_expanded_state(self): """Save all items expanded state""" self.__expanded_state = {} def add_to_state(item): user_text = get_item_user_text(item) self.__expanded_state[hash(user_text)] = item.isExpanded() def browse_children(item): add_to_state(item) for index in range(item.childCount()): citem = item.child(index) user_text = get_item_user_text(citem) self.__expanded_state[hash(user_text)] = citem.isExpanded() browse_children(citem) for tlitem in self.get_top_level_items(): browse_children(tlitem) def restore_expanded_state(self): """Restore all items expanded state""" if self.__expanded_state is None: return for item in self.get_items() + self.get_top_level_items(): user_text = get_item_user_text(item) is_expanded = self.__expanded_state.get(hash(user_text)) if is_expanded is not None: item.setExpanded(is_expanded) def sort_top_level_items(self, key): """Sorting tree wrt top level items""" self.save_expanded_state() items = sorted([ self.takeTopLevelItem(0) for index in range(self.topLevelItemCount()) ], key=key) for index, item in enumerate(items): self.insertTopLevelItem(index, item) self.restore_expanded_state() def contextMenuEvent(self, event): """Override Qt method""" self.update_menu() self.menu.popup(event.globalPos())
class MxTreeView(QTreeView): def __init__(self, parent=None): super().__init__(parent) self.activated.connect(self.activated_callback) # self.doubleClicked.connect(self.doubleClicked_callback) self.mx_widget = parent.mx_widget self.plugin = parent.plugin if spyder.version_info > (5,): self.container = self.plugin.get_container() else: self.container = self.plugin self.shell = None self.reply = None # To write dialog box result self.setAlternatingRowColors(False) # Context menu self.contextMenu = QMenu(self) self.action_update_properties = self.contextMenu.addAction( "Show Properties" ) self.action_import_names = self.contextMenu.addAction( "Import Names" ) self.action_analyze_selected = self.contextMenu.addAction( "Analyze Selected" ) self.action_update_formulas = self.contextMenu.addAction( "Show Formulas" ) self.action_new_model = self.contextMenu.addAction( "Create New Model" ) self.action_new_space = self.contextMenu.addAction( "Create New Space" ) self.action_new_cells = self.contextMenu.addAction( "Create New Cells" ) self.action_read_model = self.contextMenu.addAction( "Read Model" ) self.action_write_model = self.contextMenu.addAction( "Write Model" ) self.action_delete_selected = self.contextMenu.addAction( "Delete Selected" ) self.action_delete_model = self.contextMenu.addAction( "Delete Model" ) def activated_callback(self, index): if index.isValid(): item = self.currentIndex().internalPointer() if not isinstance(item, ViewItem): self.shell.update_mxproperty(item.itemData['fullname']) # self.plugin.dataview.update_object(item.itemData['fullname']) # def doubleClicked_callback(self, index): # if index.isValid() and index.column() == TreeCol.IS_DERIVED: # answer = QMessageBox.warning(self.parent(), _("Warning"), # str(index.row()), # QMessageBox.Yes | QMessageBox.No) def contextMenuEvent(self, event): action = self.contextMenu.exec_(self.mapToGlobal(event.pos())) if action == self.action_update_formulas: index = self.currentIndex() if index.isValid(): item = index.internalPointer() if isinstance(item, SpaceItem): pass else: if index.parent().isValid(): item = index.parent().internalPointer() else: return self.shell.update_codelist(item.itemData['fullname']) elif action == self.action_update_properties: index = self.currentIndex() if index.isValid(): item = self.currentIndex().internalPointer() if not isinstance(item, ViewItem): self.shell.update_mxproperty(item.itemData['fullname']) elif action == self.action_import_names: index = self.currentIndex() if index.isValid(): item = self.currentIndex().internalPointer() if isinstance(item, SpaceItem): has_children = True elif isinstance(item, CellsItem) or isinstance(item, RefItem): has_children = False else: return else: return if has_children: dialog = ImportNamesDialog(self) dialog.exec() if self.reply['accepted']: import_selected = self.reply['import_selected'] import_children = self.reply['import_children'] replace_existing = self.reply['replace_existing'] self.reply = None else: self.reply = None return else: import_selected = True import_children = False replace_existing = True self.shell.import_names(item.itemData['fullname'], import_selected, import_children, replace_existing ) elif action == self.action_analyze_selected: index = self.currentIndex() if index.isValid(): item = self.currentIndex().internalPointer() if isinstance(item, CellsItem): self.shell.mxanalyzer.update_object(item.itemData) elif action == self.action_new_model: dialog = NewModelDialog(self) dialog.exec() if self.reply['accepted']: name = self.reply['name'] define_var = self.reply['define_var'] if define_var: varname = self.reply['varname'] else: varname = '' self.reply = None self.shell.new_model(name, define_var, varname) else: self.reply = None elif action == self.action_new_space: if self.model(): parentList = self.model().rootItem.getSpaceContainerList() else: parentList = [] # Find current item index = self.currentIndex() if index.isValid(): name = index.internalPointer().itemData['fullname'] try: currIndex = parentList.index(name) except ValueError: currIndex = 0 else: currIndex = 0 if self.model(): model = self.model().rootItem.itemData['name'] else: model = '' dialog = NewSpaceDialog(self, parentList, currIndex) dialog.exec() if self.reply['accepted']: name = self.reply['name'] parent = self.reply['parent'] bases = self.reply['bases'] define_var = self.reply['define_var'] if define_var: varname = self.reply['varname'] else: varname = '' self.reply = None self.shell.new_space( model, parent, name, bases, define_var, varname) else: self.reply = None elif action == self.action_new_cells: if self.model(): parentList = self.model().rootItem.getChildSpaceList() else: parentList = [] # Find current item index = self.currentIndex() if index.isValid(): name = index.internalPointer().itemData['namedid'] try: currIndex = parentList.index(name) except ValueError: currIndex = 0 else: currIndex = 0 if self.model(): model = self.model().rootItem.itemData['name'] else: model = '' dialog = NewCellsDialog(self, parentList, currIndex) dialog.exec() if self.reply['accepted']: name = self.reply['name'] parent = self.reply['parent'] formula = self.reply['formula'] define_var = self.reply['define_var'] if define_var: varname = self.reply['varname'] else: varname = '' self.reply = None self.shell.new_cells( model, parent, name, formula, define_var, varname) else: self.reply = None elif action == self.action_read_model: dialog = ReadModelDialog(self, modelpath=self.mx_widget.last_modelpath) dialog.exec() if self.reply['accepted']: modelpath = self.reply['directory'] name = self.reply['name'] define_var = self.reply['define_var'] if define_var: varname = self.reply['varname'] else: varname = '' self.reply = None self.shell.read_model(modelpath, name, define_var, varname) self.mx_widget.last_modelpath = modelpath else: self.reply = None elif action == self.action_write_model: model = self.container.current_widget().model_selector.get_selected_model() if not model: QMessageBox.critical(self, "Error", "No model exits.") return dialog = WriteModelDialog(self, modelpath=self.mx_widget.last_modelpath) dialog.exec() if self.reply['accepted']: modelpath = self.reply['directory'] + "/" + self.reply['name'] backup = self.reply['backup'] zipmodel = self.reply['zipmodel'] self.reply = None self.shell.write_model(model, modelpath, backup, zipmodel) self.mx_widget.last_modelpath = modelpath else: self.reply = None elif action == self.action_delete_model: model = self.container.current_widget().model_selector.get_selected_model() if model: answer = QMessageBox.question( self, _("Delete Model"), _("Do you want to delete %s?" % model), QMessageBox.Yes | QMessageBox.No) if answer == QMessageBox.Yes: self.shell.del_model(model) else: return else: QMessageBox.critical(self, "Error", "No model exits.") elif action == self.action_delete_selected: index = self.currentIndex() if index.isValid(): item = index.internalPointer() if isinstance(item, ViewItem) or isinstance(item, ItemSpaceItem): pass else: if index.parent().isValid(): parent = index.parent().internalPointer().fullname else: parent = self.container.current_widget().model_selector.get_selected_model() assert parent answer = QMessageBox.question( self, _("Delete Selected"), _("Do you want to delete %s?" % item.name), QMessageBox.Yes | QMessageBox.No) if answer == QMessageBox.Yes: self.shell.del_object(parent, item.name) QMessageBox.information( self, "Notice", "'%s' is deleted from '%s'" % (item.name, parent))
def setup_options_actions(self, parent): """ Setup the actions for the Options menu. The actions are handled by the parent widget """ container_widget = QWidget(self) layout = QHBoxLayout(container_widget) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) container_widget.setLayout(layout) self.setCornerWidget(container_widget, Qt.TopRightCorner) run_button = QPushButton(container_widget) run_button.setObjectName(self.RUN_BUTTON_OBJECT_NAME) run_button.setIcon(get_icon("mdi.play", PLAY_BUTTON_GREEN_COLOR, 1.6)) run_button.clicked.connect(parent.execute_current_async) layout.addWidget(run_button) abort_button = QPushButton(container_widget) abort_button.setObjectName(self.ABORT_BUTTON_OBJECT_NAME) abort_button.setIcon( get_icon("mdi.square", ABORT_BUTTON_RED_COLOR, 1.1)) abort_button.clicked.connect(parent.abort_current) layout.addWidget(abort_button) options_button = QPushButton(container_widget) options_button.setObjectName(self.OPTIONS_BUTTON_OBJECT_NAME) options_button.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum) options_button.setText("Options") options_menu = QMenu("", self) options_button.setMenu(options_menu) layout.addWidget(options_button) self.tabCloseRequested.connect(parent.close_tab) run_action = create_action(self, "Run", on_triggered=parent.execute_current_async, shortcut=("Ctrl+Enter", "Ctrl+Return"), shortcut_context=Qt.ApplicationShortcut, shortcut_visible_in_context_menu=True) run_all_action = create_action(self, "Run All", on_triggered=parent.execute_async, shortcut=("Ctrl+Shift+Enter", "Ctrl+Shift+Return"), shortcut_context=Qt.ApplicationShortcut, shortcut_visible_in_context_menu=True) abort_action = create_action(self, "Abort", on_triggered=parent.abort_current, shortcut="Ctrl+D", shortcut_context=Qt.ApplicationShortcut, shortcut_visible_in_context_menu=True) # menu action to toggle the find/replace dialog toggle_find_replace = create_action( self, 'Find/Replace...', on_triggered=parent.toggle_find_replace_dialog, shortcut='Ctrl+F', shortcut_visible_in_context_menu=True) toggle_comment_action = create_action( self, "Comment/Uncomment", on_triggered=parent.toggle_comment_current, shortcut="Ctrl+/", shortcut_context=Qt.ApplicationShortcut, shortcut_visible_in_context_menu=True) tabs_to_spaces_action = create_action( self, 'Tabs to Spaces', on_triggered=parent.tabs_to_spaces_current, shortcut_visible_in_context_menu=True) spaces_to_tabs_action = create_action( self, 'Spaces to Tabs', on_triggered=parent.spaces_to_tabs_current, shortcut_visible_in_context_menu=True) toggle_whitespace_action = create_action( self, 'Toggle Whitespace Visible', on_triggered=parent.toggle_whitespace_visible_all, shortcut_visible_in_context_menu=True) # Store actions for adding to menu bar; None will add a separator editor_actions = [ run_action, run_all_action, abort_action, None, toggle_find_replace, None, toggle_comment_action, toggle_whitespace_action, None, tabs_to_spaces_action, spaces_to_tabs_action ] add_actions(options_menu, editor_actions)
def __init__(self, title, parent, key_defs, key=""): QDialog.__init__(self, parent) self.setWindowTitle(title) self.current_key = key self._key_defs = key_defs self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint) self.setWindowFlags(self.windowFlags() & ~Qt.WindowCloseButtonHint) self._tab_map = {} self._tab_order = [] layout = QVBoxLayout() self._tabs = QTabWidget() layout.addWidget(self._tabs) layout.setSizeConstraint(QLayout.SetFixedSize) # not resizable!!! self._button_layout = QHBoxLayout() self._reset_button = QToolButton() self._reset_button.setIcon(resourceIcon("format_color_reset.svg")) self._reset_button.setToolTip("Reset all settings back to default") self._reset_button.clicked.connect(self.resetSettings) self._undo_button = QToolButton() self._undo_button.setIcon(resourceIcon("undo.svg")) self._undo_button.setToolTip("Undo") self._undo_button.clicked.connect(self.undoSettings) self._redo_button = QToolButton() self._redo_button.setIcon(resourceIcon("redo.svg")) self._redo_button.setToolTip("Redo") self._redo_button.clicked.connect(self.redoSettings) self._redo_button.setEnabled(False) self._copy_from_button = QToolButton() self._copy_from_button.setIcon(resourceIcon("download.svg")) self._copy_from_button.setToolTip("Copy settings from another key") self._copy_from_button.setPopupMode(QToolButton.InstantPopup) self._copy_from_button.setEnabled(False) self._copy_to_button = QToolButton() self._copy_to_button.setIcon(resourceIcon("upload.svg")) self._copy_to_button.setToolTip( "Copy current plot settings to other keys") self._copy_to_button.setPopupMode(QToolButton.InstantPopup) self._copy_to_button.clicked.connect(self.initiateCopyStyleToDialog) self._copy_to_button.setEnabled(True) tool_menu = QMenu(self._copy_from_button) self._popup_list = QListWidget(tool_menu) self._popup_list.setSortingEnabled(True) self._popup_list.itemClicked.connect(self.keySelected) action = QWidgetAction(tool_menu) action.setDefaultWidget(self._popup_list) tool_menu.addAction(action) self._copy_from_button.setMenu(tool_menu) self._apply_button = QPushButton("Apply") self._apply_button.setToolTip("Apply the new settings") self._apply_button.clicked.connect(self.applySettings) self._apply_button.setDefault(True) self._close_button = QPushButton("Close") self._close_button.setToolTip("Hide this dialog") self._close_button.clicked.connect(self.hide) self._button_layout.addWidget(self._reset_button) self._button_layout.addStretch() self._button_layout.addWidget(self._undo_button) self._button_layout.addWidget(self._redo_button) self._button_layout.addWidget(self._copy_from_button) self._button_layout.addWidget(self._copy_to_button) self._button_layout.addStretch() self._button_layout.addWidget(self._apply_button) self._button_layout.addWidget(self._close_button) layout.addStretch() layout.addLayout(self._button_layout) self.setLayout(layout)
def _showHeaderMenu(self, point): column = self.headers.logicalIndexAt(point.x()) if column == -1: return menu = QMenu(self) # Actions cols = self.table.selectionModel().selectedColumns() if len(cols) != 2 or column not in [col.column() for col in cols]: self.table.selectColumn(column) menu.aboutToHide.connect(lambda: self.table.clearSelection()) save = QAction("Save", menu) save.triggered.connect(lambda: self._saveConfiguration(column)) save_all = QAction("Save all", menu) save_all.triggered.connect(lambda: self._saveChanges()) save_all.setShortcut(QKeySequence.Save) rename = QAction("Rename", menu) rename.triggered.connect(lambda: self._renameConfiguration(column)) close = QAction("Close", menu) close.triggered.connect(lambda: self._closeConfiguration(column)) close.setShortcut(QKeySequence.Close) close_right = QAction("Close to the right", menu) close_right.triggered.connect( lambda: self._closeConfigurationsToTheRight(column)) close_others = QAction("Close other", menu) close_others.triggered.connect( lambda: self._closeOtherConfigurations(column)) close_all = QAction("Close all", menu) close_all.triggered.connect(lambda: self._closeAllConfigurations()) tune = QAction("Tune", menu) tune.triggered.connect(lambda: self._tuneConfiguration(column)) menu.addActions([save, save_all]) menu.addSeparator() menu.addActions([rename]) menu.addSeparator() menu.addActions([close, close_right, close_others, close_all]) menu.addSeparator() menu.addActions([tune]) else: bar = QAction("Interpolate", menu) bar.triggered.connect(lambda: self._barConfiguration(cols)) menu.addAction(bar) vheader_offset = self.table.verticalHeader().width() point.setX(point.x() + vheader_offset) menu.popup(self.mapToGlobal(point))
def openMenu(self, position): indexes = self.ui.treeWidget.selectedIndexes() item = self.ui.treeWidget.itemAt(position) db_origin = "" #if item.parent(): # db_origin = item.parent().text(0) collec = str(item.text(0).encode("utf-8")) if len(indexes) > 0: level = 0 index = indexes[0] while index.parent().isValid(): index = index.parent() level = level + 1 menu = QMenu() #print((collec, db_origin)) if level == 0: pass else: #keyarray = GetKeys(collec, db_origin) #if "Open" in keyarray: if self.ui.combobox.currentText() == u"K线": menu.addAction(QAction("Kline", menu, checkable=True)) menu.addAction(QAction("Open", menu, checkable=True)) menu.addAction( QAction("Close", menu, checkable=True) ) #open up different menu with different kind of graphs menu.addAction(QAction("High", menu, checkable=True)) menu.addAction(QAction("Low", menu, checkable=True)) menu.addAction(QAction("Volume", menu, checkable=True)) #menu.addAction(QAction("P_change", menu, checkable=True)) #menu.addAction(QAction("Turnover",menu,checkable=True)) if self.ui.combobox.currentText() == u"复权": menu.addAction(QAction("Kline", menu, checkable=True)) menu.addAction(QAction("Open", menu, checkable=True)) menu.addAction(QAction("Close", menu, checkable=True)) menu.addAction(QAction("High", menu, checkable=True)) menu.addAction(QAction("Low", menu, checkable=True)) menu.addAction(QAction("Volume", menu, checkable=True)) menu.addAction(QAction("Amount", menu, checkable=True)) if self.ui.combobox.currentText() == u"分笔数据": menu.addAction(QAction("分笔", menu, checkable=True)) #for g in keyarray: #menu.addAction(QAction(g, menu, checkable=True)) menu.triggered.connect( lambda action: self.methodSelected(action, collec)) menu.exec_(self.ui.treeWidget.viewport().mapToGlobal(position))
class EnvironmentsTab(WidgetBase): """Conda environments tab.""" BLACKLIST = ['anaconda-navigator', '_license'] # Hide in package manager # --- Signals # ------------------------------------------------------------------------- sig_ready = Signal() # name, prefix, sender sig_item_selected = Signal(object, object, object) # sender, func_after_dlg_accept, func_callback_on_finished sig_create_requested = Signal() sig_clone_requested = Signal() sig_import_requested = Signal() sig_remove_requested = Signal() # button_widget, sender_constant sig_channels_requested = Signal(object, object) # sender_constant sig_update_index_requested = Signal(object) sig_cancel_requested = Signal(object) # conda_packages_action_dict, pip_packages_action_dict sig_packages_action_requested = Signal(object, object) def __init__(self, parent=None): """Conda environments tab.""" super(EnvironmentsTab, self).__init__(parent) # Variables self.api = AnacondaAPI() self.current_prefix = None self.style_sheet = None # Widgets self.frame_header_left = FrameTabHeader() self.frame_list = FrameEnvironmentsList(self) self.frame_widget = FrameEnvironmentsPackages(self) self.text_search = LineEditSearch() self.list = ListWidgetEnv() self.menu_list = QMenu() self.button_create = ButtonToolNormal(text="Create") self.button_clone = ButtonToolNormal(text="Clone") self.button_import = ButtonToolNormal(text="Import") self.button_remove = ButtonToolNormal(text="Remove") self.button_toggle_collapse = ButtonToggleCollapse() self.widget = CondaPackagesWidget(parent=self) # Widgets setup self.frame_list.is_expanded = True self.text_search.setPlaceholderText("Search Environments") self.list.setContextMenuPolicy(Qt.CustomContextMenu) self.button_create.setObjectName("create") # Needed for QSS selectors self.button_clone.setObjectName("clone") self.button_import.setObjectName("import") self.button_remove.setObjectName("remove") self.widget.textbox_search.set_icon_visibility(False) # Layouts layout_header_left = QVBoxLayout() layout_header_left.addWidget(self.text_search) self.frame_header_left.setLayout(layout_header_left) layout_buttons = QHBoxLayout() layout_buttons.addWidget(self.button_create) layout_buttons.addWidget(self.button_clone) layout_buttons.addWidget(self.button_import) layout_buttons.addWidget(self.button_remove) layout_list_buttons = QVBoxLayout() layout_list_buttons.addWidget(self.frame_header_left) layout_list_buttons.addWidget(self.list) layout_list_buttons.addLayout(layout_buttons) self.frame_list.setLayout(layout_list_buttons) layout_widget = QHBoxLayout() layout_widget.addWidget(self.widget) self.frame_widget.setLayout(layout_widget) layout_main = QHBoxLayout() layout_main.addWidget(self.frame_list, 10) layout_main.addWidget(self.button_toggle_collapse, 1) layout_main.addWidget(self.frame_widget, 30) self.setLayout(layout_main) # Signals for buttons and boxes self.button_toggle_collapse.clicked.connect(self.expand_collapse) self.button_create.clicked.connect(self.sig_create_requested) self.button_clone.clicked.connect(self.sig_clone_requested) self.button_import.clicked.connect(self.sig_import_requested) self.button_remove.clicked.connect(self.sig_remove_requested) self.text_search.textChanged.connect(self.filter_list) # Signals for list self.list.sig_item_selected.connect(self._item_selected) # Signals for packages widget self.widget.sig_ready.connect(self.sig_ready) self.widget.sig_channels_requested.connect(self.sig_channels_requested) self.widget.sig_update_index_requested.connect( self.sig_update_index_requested) self.widget.sig_cancel_requested.connect(self.sig_cancel_requested) self.widget.sig_packages_action_requested.connect( self.sig_packages_action_requested) # --- Setup methods # ------------------------------------------------------------------------- def setup(self, conda_data): """Setup tab content and populates the list of environments.""" self.set_widgets_enabled(False) conda_processed_info = conda_data.get('processed_info') environments = conda_processed_info.get('__environments') packages = conda_data.get('packages') self.current_prefix = conda_processed_info.get('default_prefix') self.set_environments(environments) self.set_packages(packages) def set_environments(self, environments): """Populate the list of environments.""" self.list.clear() selected_item_row = 0 for i, (env_prefix, env_name) in enumerate(environments.items()): item = ListItemEnv(prefix=env_prefix, name=env_name) item.button_options.clicked.connect(self.show_environment_menu) if env_prefix == self.current_prefix: selected_item_row = i self.list.addItem(item) self.list.setCurrentRow(selected_item_row, loading=True) self.filter_list() self.list.scrollToItem(self.list.item(selected_item_row)) def _set_packages(self, worker, output, error): """Set packages callback.""" packages, model_data = output self.widget.setup(packages, model_data) self.set_widgets_enabled(True) self.set_loading(prefix=self.current_prefix, value=False) def set_packages(self, packages): """Set packages widget content.""" worker = self.api.process_packages(packages, prefix=self.current_prefix, blacklist=self.BLACKLIST) worker.sig_chain_finished.connect(self._set_packages) def show_environment_menu(self, value=None, position=None): """Show the environment actions menu.""" self.menu_list.clear() menu_item = self.menu_list.addAction('Open Terminal') menu_item.triggered.connect( lambda: self.open_environment_in('terminal')) for word in ['Python', 'IPython', 'Jupyter Notebook']: menu_item = self.menu_list.addAction("Open with " + word) menu_item.triggered.connect( lambda x, w=word: self.open_environment_in(w.lower())) current_item = self.list.currentItem() prefix = current_item.prefix if isinstance(position, bool) or position is None: width = current_item.button_options.width() position = QPoint(width, 0) point = QPoint(0, 0) parent_position = current_item.button_options.mapToGlobal(point) self.menu_list.move(parent_position + position) # Disabled actions depending on the environment installed packages actions = self.menu_list.actions() actions[2].setEnabled(launch.check_prog('ipython', prefix)) actions[3].setEnabled(launch.check_prog('notebook', prefix)) self.menu_list.exec_() def open_environment_in(self, which): """Open selected environment in console terminal.""" prefix = self.list.currentItem().prefix logger.debug("%s, %s", which, prefix) if which == 'terminal': launch.console(prefix) else: launch.py_in_console(prefix, which) # --- Common Helpers (# FIXME: factor out to common base widget) # ------------------------------------------------------------------------- def _item_selected(self, item): """Callback to emit signal as user selects an item from the list.""" self.set_loading(prefix=item.prefix) self.sig_item_selected.emit(item.name, item.prefix, C.TAB_ENVIRONMENT) def add_temporal_item(self, name): """Creates a temporal item on list while creation becomes effective.""" item_names = [item.name for item in self.list.items()] item_names.append(name) index = list(sorted(item_names)).index(name) + 1 item = ListItemEnv(name=name) self.list.insertItem(index, item) self.list.setCurrentRow(index) self.list.scrollToItem(item) item.set_loading(True) def expand_collapse(self): """Expand or collapse the list selector.""" if self.frame_list.is_expanded: self.frame_list.hide() self.frame_list.is_expanded = False else: self.frame_list.show() self.frame_list.is_expanded = True def filter_list(self, text=None): """Filter items in list by name.""" text = self.text_search.text().lower() for i in range(self.list.count()): item = self.list.item(i) item.setHidden(text not in item.name.lower()) if not item.widget.isVisible(): item.widget.repaint() def ordered_widgets(self, next_widget=None): """Return a list of the ordered widgets.""" if next_widget is not None: self.widget.table_last_row.add_focus_widget(next_widget) ordered_widgets = [self.text_search] ordered_widgets += self.list.ordered_widgets() ordered_widgets += [ self.button_create, self.button_clone, self.button_import, self.button_remove, self.widget.combobox_filter, self.widget.button_channels, self.widget.button_update, self.widget.textbox_search, # self.widget.table_first_row, self.widget.table, self.widget.table_last_row, self.widget.button_apply, self.widget.button_clear, self.widget.button_cancel, ] return ordered_widgets def refresh(self): """Refresh the enabled/disabled status of the widget and subwidgets.""" is_root = self.current_prefix == self.api.ROOT_PREFIX self.button_clone.setDisabled(is_root) self.button_remove.setDisabled(is_root) def set_loading(self, prefix=None, value=True): """Set the item given by `prefix` to loading state.""" for row, item in enumerate(self.list.items()): if item.prefix == prefix: item.set_loading(value) self.list.setCurrentRow(row) break def set_widgets_enabled(self, value): """Change the enabled status of widgets and subwidgets.""" self.list.setEnabled(value) self.button_create.setEnabled(value) self.button_clone.setEnabled(value) self.button_import.setEnabled(value) self.button_remove.setEnabled(value) self.widget.set_widgets_enabled(value) if value: self.refresh() def update_status(self, action='', message='', value=None, max_value=None): """Update widget status and progress bar.""" self.widget.update_status(action=action, message=message, value=value, max_value=max_value) def update_style_sheet(self, style_sheet=None): """Update custom CSS stylesheet.""" if style_sheet is None: self.style_sheet = load_style_sheet() else: self.style_sheet = style_sheet self.setStyleSheet(self.style_sheet) self.list.update_style_sheet(self.style_sheet) self.menu_list.setStyleSheet(self.style_sheet)
class ShellBaseWidget(ConsoleBaseWidget, SaveHistoryMixin, BrowseHistoryMixin): """ Shell base widget """ redirect_stdio = Signal(bool) sig_keyboard_interrupt = Signal() execute = Signal(str) append_to_history = Signal(str, str) def __init__(self, parent, history_filename, profile=False, initial_message=None, default_foreground_color=None, error_foreground_color=None, traceback_foreground_color=None, prompt_foreground_color=None, background_color=None): """ parent : specifies the parent widget """ ConsoleBaseWidget.__init__(self, parent) SaveHistoryMixin.__init__(self, history_filename) BrowseHistoryMixin.__init__(self) # Prompt position: tuple (line, index) self.current_prompt_pos = None self.new_input_line = True # History assert is_text_string(history_filename) self.history = self.load_history() # Session self.historylog_filename = CONF.get('main', 'historylog_filename', get_conf_path('history.log')) # Context menu self.menu = None self.setup_context_menu() # Simple profiling test self.profile = profile # Buffer to increase performance of write/flush operations self.__buffer = [] if initial_message: self.__buffer.append(initial_message) self.__timestamp = 0.0 self.__flushtimer = QTimer(self) self.__flushtimer.setSingleShot(True) self.__flushtimer.timeout.connect(self.flush) # Give focus to widget self.setFocus() # Cursor width self.setCursorWidth(CONF.get('main', 'cursor/width')) # Adjustments to completion_widget to use it here self.completion_widget.currentRowChanged.disconnect() def toggle_wrap_mode(self, enable): """Enable/disable wrap mode""" self.set_wrap_mode('character' if enable else None) def set_font(self, font): """Set shell styles font""" self.setFont(font) self.set_pythonshell_font(font) cursor = self.textCursor() cursor.select(QTextCursor.Document) charformat = QTextCharFormat() charformat.setFontFamily(font.family()) charformat.setFontPointSize(font.pointSize()) cursor.mergeCharFormat(charformat) #------ Context menu def setup_context_menu(self): """Setup shell context menu""" self.menu = QMenu(self) self.cut_action = create_action(self, _("Cut"), shortcut=keybinding('Cut'), icon=ima.icon('editcut'), triggered=self.cut) self.copy_action = create_action(self, _("Copy"), shortcut=keybinding('Copy'), icon=ima.icon('editcopy'), triggered=self.copy) paste_action = create_action(self, _("Paste"), shortcut=keybinding('Paste'), icon=ima.icon('editpaste'), triggered=self.paste) save_action = create_action(self, _("Save history log..."), icon=ima.icon('filesave'), tip=_("Save current history log (i.e. all " "inputs and outputs) in a text file"), triggered=self.save_historylog) self.delete_action = create_action(self, _("Delete"), shortcut=keybinding('Delete'), icon=ima.icon('editdelete'), triggered=self.delete) selectall_action = create_action(self, _("Select All"), shortcut=keybinding('SelectAll'), icon=ima.icon('selectall'), triggered=self.selectAll) add_actions(self.menu, (self.cut_action, self.copy_action, paste_action, self.delete_action, None, selectall_action, None, save_action) ) def contextMenuEvent(self, event): """Reimplement Qt method""" state = self.has_selected_text() self.copy_action.setEnabled(state) self.cut_action.setEnabled(state) self.delete_action.setEnabled(state) self.menu.popup(event.globalPos()) event.accept() #------ Input buffer def get_current_line_from_cursor(self): return self.get_text('cursor', 'eof') def _select_input(self): """Select current line (without selecting console prompt)""" line, index = self.get_position('eof') if self.current_prompt_pos is None: pline, pindex = line, index else: pline, pindex = self.current_prompt_pos self.setSelection(pline, pindex, line, index) @Slot() def clear_terminal(self): """ Clear terminal window Child classes reimplement this method to write prompt """ self.clear() # The buffer being edited def _set_input_buffer(self, text): """Set input buffer""" if self.current_prompt_pos is not None: self.replace_text(self.current_prompt_pos, 'eol', text) else: self.insert(text) self.set_cursor_position('eof') def _get_input_buffer(self): """Return input buffer""" input_buffer = '' if self.current_prompt_pos is not None: input_buffer = self.get_text(self.current_prompt_pos, 'eol') input_buffer = input_buffer.replace(os.linesep, '\n') return input_buffer input_buffer = Property("QString", _get_input_buffer, _set_input_buffer) #------ Prompt def new_prompt(self, prompt): """ Print a new prompt and save its (line, index) position """ if self.get_cursor_line_column()[1] != 0: self.write('\n') self.write(prompt, prompt=True) # now we update our cursor giving end of prompt self.current_prompt_pos = self.get_position('cursor') self.ensureCursorVisible() self.new_input_line = False def check_selection(self): """ Check if selected text is r/w, otherwise remove read-only parts of selection """ if self.current_prompt_pos is None: self.set_cursor_position('eof') else: self.truncate_selection(self.current_prompt_pos) #------ Copy / Keyboard interrupt @Slot() def copy(self): """Copy text to clipboard... or keyboard interrupt""" if self.has_selected_text(): ConsoleBaseWidget.copy(self) elif not sys.platform == 'darwin': self.interrupt() def interrupt(self): """Keyboard interrupt""" self.sig_keyboard_interrupt.emit() @Slot() def cut(self): """Cut text""" self.check_selection() if self.has_selected_text(): ConsoleBaseWidget.cut(self) @Slot() def delete(self): """Remove selected text""" self.check_selection() if self.has_selected_text(): ConsoleBaseWidget.remove_selected_text(self) @Slot() def save_historylog(self): """Save current history log (all text in console)""" title = _("Save history log") self.redirect_stdio.emit(False) filename, _selfilter = getsavefilename(self, title, self.historylog_filename, "%s (*.log)" % _("History logs")) self.redirect_stdio.emit(True) if filename: filename = osp.normpath(filename) try: encoding.write(to_text_string(self.get_text_with_eol()), filename) self.historylog_filename = filename CONF.set('main', 'historylog_filename', filename) except EnvironmentError: pass #------ Basic keypress event handler def on_enter(self, command): """on_enter""" self.execute_command(command) def execute_command(self, command): self.execute.emit(command) self.add_to_history(command) self.new_input_line = True def on_new_line(self): """On new input line""" self.set_cursor_position('eof') self.current_prompt_pos = self.get_position('cursor') self.new_input_line = False @Slot() def paste(self): """Reimplemented slot to handle multiline paste action""" if self.new_input_line: self.on_new_line() ConsoleBaseWidget.paste(self) def keyPressEvent(self, event): """ Reimplement Qt Method Basic keypress event handler (reimplemented in InternalShell to add more sophisticated features) """ if self.preprocess_keyevent(event): # Event was accepted in self.preprocess_keyevent return self.postprocess_keyevent(event) def preprocess_keyevent(self, event): """Pre-process keypress event: return True if event is accepted, false otherwise""" # Copy must be done first to be able to copy read-only text parts # (otherwise, right below, we would remove selection # if not on current line) ctrl = event.modifiers() & Qt.ControlModifier meta = event.modifiers() & Qt.MetaModifier # meta=ctrl in OSX if event.key() == Qt.Key_C and \ ((Qt.MetaModifier | Qt.ControlModifier) & event.modifiers()): if meta and sys.platform == 'darwin': self.interrupt() elif ctrl: self.copy() event.accept() return True if self.new_input_line and ( len(event.text()) or event.key() in \ (Qt.Key_Up, Qt.Key_Down, Qt.Key_Left, Qt.Key_Right) ): self.on_new_line() return False def postprocess_keyevent(self, event): """Post-process keypress event: in InternalShell, this is method is called when shell is ready""" event, text, key, ctrl, shift = restore_keyevent(event) # Is cursor on the last line? and after prompt? if len(text): #XXX: Shouldn't it be: `if len(unicode(text).strip(os.linesep))` ? if self.has_selected_text(): self.check_selection() self.restrict_cursor_position(self.current_prompt_pos, 'eof') cursor_position = self.get_position('cursor') if key in (Qt.Key_Return, Qt.Key_Enter): if self.is_cursor_on_last_line(): self._key_enter() # add and run selection else: self.insert_text(self.get_selected_text(), at_end=True) elif key == Qt.Key_Insert and not shift and not ctrl: self.setOverwriteMode(not self.overwriteMode()) elif key == Qt.Key_Delete: if self.has_selected_text(): self.check_selection() self.remove_selected_text() elif self.is_cursor_on_last_line(): self.stdkey_clear() elif key == Qt.Key_Backspace: self._key_backspace(cursor_position) elif key == Qt.Key_Tab: self._key_tab() elif key == Qt.Key_Space and ctrl: self._key_ctrl_space() elif key == Qt.Key_Left: if self.current_prompt_pos == cursor_position: # Avoid moving cursor on prompt return method = self.extend_selection_to_next if shift \ else self.move_cursor_to_next method('word' if ctrl else 'character', direction='left') elif key == Qt.Key_Right: if self.is_cursor_at_end(): return method = self.extend_selection_to_next if shift \ else self.move_cursor_to_next method('word' if ctrl else 'character', direction='right') elif (key == Qt.Key_Home) or ((key == Qt.Key_Up) and ctrl): self._key_home(shift, ctrl) elif (key == Qt.Key_End) or ((key == Qt.Key_Down) and ctrl): self._key_end(shift, ctrl) elif key == Qt.Key_Up: if not self.is_cursor_on_last_line(): self.set_cursor_position('eof') y_cursor = self.get_coordinates(cursor_position)[1] y_prompt = self.get_coordinates(self.current_prompt_pos)[1] if y_cursor > y_prompt: self.stdkey_up(shift) else: self.browse_history(backward=True) elif key == Qt.Key_Down: if not self.is_cursor_on_last_line(): self.set_cursor_position('eof') y_cursor = self.get_coordinates(cursor_position)[1] y_end = self.get_coordinates('eol')[1] if y_cursor < y_end: self.stdkey_down(shift) else: self.browse_history(backward=False) elif key in (Qt.Key_PageUp, Qt.Key_PageDown): #XXX: Find a way to do this programmatically instead of calling # widget keyhandler (this won't work if the *event* is coming from # the event queue - i.e. if the busy buffer is ever implemented) ConsoleBaseWidget.keyPressEvent(self, event) elif key == Qt.Key_Escape and shift: self.clear_line() elif key == Qt.Key_Escape: self._key_escape() elif key == Qt.Key_L and ctrl: self.clear_terminal() elif key == Qt.Key_V and ctrl: self.paste() elif key == Qt.Key_X and ctrl: self.cut() elif key == Qt.Key_Z and ctrl: self.undo() elif key == Qt.Key_Y and ctrl: self.redo() elif key == Qt.Key_A and ctrl: self.selectAll() elif key == Qt.Key_Question and not self.has_selected_text(): self._key_question(text) elif key == Qt.Key_ParenLeft and not self.has_selected_text(): self._key_parenleft(text) elif key == Qt.Key_Period and not self.has_selected_text(): self._key_period(text) elif len(text) and not self.isReadOnly(): self.hist_wholeline = False self.insert_text(text) self._key_other(text) else: # Let the parent widget handle the key press event ConsoleBaseWidget.keyPressEvent(self, event) #------ Key handlers def _key_enter(self): command = self.input_buffer self.insert_text('\n', at_end=True) self.on_enter(command) self.flush() def _key_other(self, text): raise NotImplementedError def _key_backspace(self, cursor_position): raise NotImplementedError def _key_tab(self): raise NotImplementedError def _key_ctrl_space(self): raise NotImplementedError def _key_home(self, shift, ctrl): if self.is_cursor_on_last_line(): self.stdkey_home(shift, ctrl, self.current_prompt_pos) def _key_end(self, shift, ctrl): if self.is_cursor_on_last_line(): self.stdkey_end(shift, ctrl) def _key_pageup(self): raise NotImplementedError def _key_pagedown(self): raise NotImplementedError def _key_escape(self): raise NotImplementedError def _key_question(self, text): raise NotImplementedError def _key_parenleft(self, text): raise NotImplementedError def _key_period(self, text): raise NotImplementedError #------ History Management def load_history(self): """Load history from a .py file in user home directory""" if osp.isfile(self.history_filename): rawhistory, _ = encoding.readlines(self.history_filename) rawhistory = [line.replace('\n', '') for line in rawhistory] if rawhistory[1] != self.INITHISTORY[1]: rawhistory[1] = self.INITHISTORY[1] else: rawhistory = self.INITHISTORY history = [line for line in rawhistory \ if line and not line.startswith('#')] # Truncating history to X entries: while len(history) >= CONF.get('historylog', 'max_entries'): del history[0] while rawhistory[0].startswith('#'): del rawhistory[0] del rawhistory[0] # Saving truncated history: try: encoding.writelines(rawhistory, self.history_filename) except EnvironmentError: pass return history #------ Simulation standards input/output def write_error(self, text): """Simulate stderr""" self.flush() self.write(text, flush=True, error=True) if get_debug_level(): STDERR.write(text) def write(self, text, flush=False, error=False, prompt=False): """Simulate stdout and stderr""" if prompt: self.flush() if not is_string(text): # This test is useful to discriminate QStrings from decoded str text = to_text_string(text) self.__buffer.append(text) ts = time.time() if flush or prompt: self.flush(error=error, prompt=prompt) elif ts - self.__timestamp > 0.05: self.flush(error=error) self.__timestamp = ts # Timer to flush strings cached by last write() operation in series self.__flushtimer.start(50) def flush(self, error=False, prompt=False): """Flush buffer, write text to console""" # Fix for spyder-ide/spyder#2452 if PY3: try: text = "".join(self.__buffer) except TypeError: text = b"".join(self.__buffer) try: text = text.decode( locale.getdefaultlocale()[1] ) except: pass else: text = "".join(self.__buffer) self.__buffer = [] self.insert_text(text, at_end=True, error=error, prompt=prompt) QCoreApplication.processEvents() self.repaint() # Clear input buffer: self.new_input_line = True #------ Text Insertion def insert_text(self, text, at_end=False, error=False, prompt=False): """ Insert text at the current cursor position or at the end of the command line """ if at_end: # Insert text at the end of the command line self.append_text_to_shell(text, error, prompt) else: # Insert text at current cursor position ConsoleBaseWidget.insert_text(self, text) #------ Re-implemented Qt Methods def focusNextPrevChild(self, next): """ Reimplemented to stop Tab moving to the next window """ if next: return False return ConsoleBaseWidget.focusNextPrevChild(self, next) #------ Drag and drop def dragEnterEvent(self, event): """Drag and Drop - Enter event""" event.setAccepted(event.mimeData().hasFormat("text/plain")) def dragMoveEvent(self, event): """Drag and Drop - Move event""" if (event.mimeData().hasFormat("text/plain")): event.setDropAction(Qt.MoveAction) event.accept() else: event.ignore() def dropEvent(self, event): """Drag and Drop - Drop event""" if (event.mimeData().hasFormat("text/plain")): text = to_text_string(event.mimeData().text()) if self.new_input_line: self.on_new_line() self.insert_text(text, at_end=True) self.setFocus() event.setDropAction(Qt.MoveAction) event.accept() else: event.ignore() def drop_pathlist(self, pathlist): """Drop path list""" raise NotImplementedError
def right_click(self, position=None): _duplicate_row = -1 _plot_sofq = -1 _remove_row = -1 _new_row = -1 _copy = -1 _paste = -1 _cut = -1 _refresh_table = -1 _clear_table = -1 # _import = -1 # _export = -1 _check_all = -1 _uncheck_all = -1 _undo = -1 _redo = -1 _plot_sofq_diff_first_run_row = -1 _plot_sofq_diff_average_row = -1 _plot_cryostat = -1 _plot_furnace = -1 _invert_selection = -1 menu = QMenu(self.parent) if self.parent.table_selection_buffer == {}: paste_status = False else: paste_status = True if (self.parent.postprocessing_ui.table.rowCount() > 0): _undo = menu.addAction("Undo") _undo.setEnabled(self.parent.undo_button_enabled) _redo = menu.addAction("Redo") _redo.setEnabled(self.parent.redo_button_enabled) menu.addSeparator() _copy = menu.addAction("Copy") _paste = menu.addAction("Paste") self._paste_menu = _paste _paste.setEnabled(paste_status) _cut = menu.addAction("Clear") menu.addSeparator() _check_all = menu.addAction("Check All") _uncheck_all = menu.addAction("Unchecked All") menu.addSeparator() _invert_selection = menu.addAction("Inverse Selection") menu.addSeparator() _new_row = menu.addAction("Insert Blank Row") if (self.parent.postprocessing_ui.table.rowCount() > 0): _duplicate_row = menu.addAction("Duplicate Row") _remove_row = menu.addAction("Remove Row(s)") menu.addSeparator() _plot_menu = menu.addMenu('Plot') _plot_sofq = _plot_menu.addAction("S(Q) ...") _plot_sofq_diff_first_run_row = _plot_menu.addAction( "S(Q) Diff (1st run)...") _plot_sofq_diff_average_row = _plot_menu.addAction( "S(Q) Diff (Avg.)...") _temp_menu = _plot_menu.addMenu("Temperature") _plot_cryostat = _temp_menu.addAction("Cyrostat...") _plot_furnace = _temp_menu.addAction("Furnace...") menu.addSeparator() _refresh_table = menu.addAction("Refresh/Reset Table") _clear_table = menu.addAction("Clear Table") action = menu.exec_(QCursor.pos()) self.current_row = self.current_row() if action == _undo: self.parent.action_undo_clicked() elif action == _redo: self.parent.action_redo_clicked() elif action == _copy: self._copy() elif action == _paste: self._paste() elif action == _cut: self._cut() elif action == _duplicate_row: self._duplicate_row() elif action == _plot_sofq: self._plot_sofq() elif action == _plot_sofq_diff_first_run_row: self._plot_sofq_diff_first_run_row() elif action == _plot_sofq_diff_average_row: self._plot_sofq_diff_average_row() elif action == _plot_cryostat: self._plot_temperature(samp_env_choice='cryostat') elif action == _plot_furnace: self._plot_temperature(samp_env_choice='furnace') elif action == _invert_selection: self._inverse_selection() elif action == _new_row: self._new_row() elif action == _remove_row: self._remove_selected_rows() elif action == _refresh_table: self._refresh_table() elif action == _clear_table: self._clear_table() elif action == _check_all: self.check_all() elif action == _uncheck_all: self.uncheck_all()
def __init__(self, parent=None): """Conda environments tab.""" super(EnvironmentsTab, self).__init__(parent) # Variables self.api = AnacondaAPI() self.current_prefix = None self.style_sheet = None # Widgets self.frame_header_left = FrameTabHeader() self.frame_list = FrameEnvironmentsList(self) self.frame_widget = FrameEnvironmentsPackages(self) self.text_search = LineEditSearch() self.list = ListWidgetEnv() self.menu_list = QMenu() self.button_create = ButtonToolNormal(text="Create") self.button_clone = ButtonToolNormal(text="Clone") self.button_import = ButtonToolNormal(text="Import") self.button_remove = ButtonToolNormal(text="Remove") self.button_toggle_collapse = ButtonToggleCollapse() self.widget = CondaPackagesWidget(parent=self) # Widgets setup self.frame_list.is_expanded = True self.text_search.setPlaceholderText("Search Environments") self.list.setContextMenuPolicy(Qt.CustomContextMenu) self.button_create.setObjectName("create") # Needed for QSS selectors self.button_clone.setObjectName("clone") self.button_import.setObjectName("import") self.button_remove.setObjectName("remove") self.widget.textbox_search.set_icon_visibility(False) # Layouts layout_header_left = QVBoxLayout() layout_header_left.addWidget(self.text_search) self.frame_header_left.setLayout(layout_header_left) layout_buttons = QHBoxLayout() layout_buttons.addWidget(self.button_create) layout_buttons.addWidget(self.button_clone) layout_buttons.addWidget(self.button_import) layout_buttons.addWidget(self.button_remove) layout_list_buttons = QVBoxLayout() layout_list_buttons.addWidget(self.frame_header_left) layout_list_buttons.addWidget(self.list) layout_list_buttons.addLayout(layout_buttons) self.frame_list.setLayout(layout_list_buttons) layout_widget = QHBoxLayout() layout_widget.addWidget(self.widget) self.frame_widget.setLayout(layout_widget) layout_main = QHBoxLayout() layout_main.addWidget(self.frame_list, 10) layout_main.addWidget(self.button_toggle_collapse, 1) layout_main.addWidget(self.frame_widget, 30) self.setLayout(layout_main) # Signals for buttons and boxes self.button_toggle_collapse.clicked.connect(self.expand_collapse) self.button_create.clicked.connect(self.sig_create_requested) self.button_clone.clicked.connect(self.sig_clone_requested) self.button_import.clicked.connect(self.sig_import_requested) self.button_remove.clicked.connect(self.sig_remove_requested) self.text_search.textChanged.connect(self.filter_list) # Signals for list self.list.sig_item_selected.connect(self._item_selected) # Signals for packages widget self.widget.sig_ready.connect(self.sig_ready) self.widget.sig_channels_requested.connect(self.sig_channels_requested) self.widget.sig_update_index_requested.connect( self.sig_update_index_requested) self.widget.sig_cancel_requested.connect(self.sig_cancel_requested) self.widget.sig_packages_action_requested.connect( self.sig_packages_action_requested)
class ShellBaseWidget(ConsoleBaseWidget, SaveHistoryMixin): """ Shell base widget """ redirect_stdio = Signal(bool) sig_keyboard_interrupt = Signal() execute = Signal(str) append_to_history = Signal(str, str) def __init__(self, parent, history_filename, profile=False): """ parent : specifies the parent widget """ ConsoleBaseWidget.__init__(self, parent) SaveHistoryMixin.__init__(self) # Prompt position: tuple (line, index) self.current_prompt_pos = None self.new_input_line = True # History self.histidx = None self.hist_wholeline = False assert is_text_string(history_filename) self.history_filename = history_filename self.history = self.load_history() # Session self.historylog_filename = CONF.get('main', 'historylog_filename', get_conf_path('history.log')) # Context menu self.menu = None self.setup_context_menu() # Simple profiling test self.profile = profile # Buffer to increase performance of write/flush operations self.__buffer = [] self.__timestamp = 0.0 self.__flushtimer = QTimer(self) self.__flushtimer.setSingleShot(True) self.__flushtimer.timeout.connect(self.flush) # Give focus to widget self.setFocus() # Cursor width self.setCursorWidth( CONF.get('main', 'cursor/width') ) def toggle_wrap_mode(self, enable): """Enable/disable wrap mode""" self.set_wrap_mode('character' if enable else None) def set_font(self, font): """Set shell styles font""" self.setFont(font) self.set_pythonshell_font(font) cursor = self.textCursor() cursor.select(QTextCursor.Document) charformat = QTextCharFormat() charformat.setFontFamily(font.family()) charformat.setFontPointSize(font.pointSize()) cursor.mergeCharFormat(charformat) #------ Context menu def setup_context_menu(self): """Setup shell context menu""" self.menu = QMenu(self) self.cut_action = create_action(self, _("Cut"), shortcut=keybinding('Cut'), icon=ima.icon('editcut'), triggered=self.cut) self.copy_action = create_action(self, _("Copy"), shortcut=keybinding('Copy'), icon=ima.icon('editcopy'), triggered=self.copy) paste_action = create_action(self, _("Paste"), shortcut=keybinding('Paste'), icon=ima.icon('editpaste'), triggered=self.paste) save_action = create_action(self, _("Save history log..."), icon=ima.icon('filesave'), tip=_("Save current history log (i.e. all " "inputs and outputs) in a text file"), triggered=self.save_historylog) self.delete_action = create_action(self, _("Delete"), shortcut=keybinding('Delete'), icon=ima.icon('editdelete'), triggered=self.delete) selectall_action = create_action(self, _("Select All"), shortcut=keybinding('SelectAll'), icon=ima.icon('selectall'), triggered=self.selectAll) add_actions(self.menu, (self.cut_action, self.copy_action, paste_action, self.delete_action, None, selectall_action, None, save_action) ) def contextMenuEvent(self, event): """Reimplement Qt method""" state = self.has_selected_text() self.copy_action.setEnabled(state) self.cut_action.setEnabled(state) self.delete_action.setEnabled(state) self.menu.popup(event.globalPos()) event.accept() #------ Input buffer def get_current_line_from_cursor(self): return self.get_text('cursor', 'eof') def _select_input(self): """Select current line (without selecting console prompt)""" line, index = self.get_position('eof') if self.current_prompt_pos is None: pline, pindex = line, index else: pline, pindex = self.current_prompt_pos self.setSelection(pline, pindex, line, index) @Slot() def clear_line(self): """Clear current line (without clearing console prompt)""" if self.current_prompt_pos is not None: self.remove_text(self.current_prompt_pos, 'eof') @Slot() def clear_terminal(self): """ Clear terminal window Child classes reimplement this method to write prompt """ self.clear() # The buffer being edited def _set_input_buffer(self, text): """Set input buffer""" if self.current_prompt_pos is not None: self.replace_text(self.current_prompt_pos, 'eol', text) else: self.insert(text) self.set_cursor_position('eof') def _get_input_buffer(self): """Return input buffer""" input_buffer = '' if self.current_prompt_pos is not None: input_buffer = self.get_text(self.current_prompt_pos, 'eol') input_buffer = input_buffer.replace(os.linesep, '\n') return input_buffer input_buffer = Property("QString", _get_input_buffer, _set_input_buffer) #------ Prompt def new_prompt(self, prompt): """ Print a new prompt and save its (line, index) position """ if self.get_cursor_line_column()[1] != 0: self.write('\n') self.write(prompt, prompt=True) # now we update our cursor giving end of prompt self.current_prompt_pos = self.get_position('cursor') self.ensureCursorVisible() self.new_input_line = False def check_selection(self): """ Check if selected text is r/w, otherwise remove read-only parts of selection """ if self.current_prompt_pos is None: self.set_cursor_position('eof') else: self.truncate_selection(self.current_prompt_pos) #------ Copy / Keyboard interrupt @Slot() def copy(self): """Copy text to clipboard... or keyboard interrupt""" if self.has_selected_text(): ConsoleBaseWidget.copy(self) elif not sys.platform == 'darwin': self.interrupt() def interrupt(self): """Keyboard interrupt""" self.sig_keyboard_interrupt.emit() @Slot() def cut(self): """Cut text""" self.check_selection() if self.has_selected_text(): ConsoleBaseWidget.cut(self) @Slot() def delete(self): """Remove selected text""" self.check_selection() if self.has_selected_text(): ConsoleBaseWidget.remove_selected_text(self) @Slot() def save_historylog(self): """Save current history log (all text in console)""" title = _("Save history log") self.redirect_stdio.emit(False) filename, _selfilter = getsavefilename(self, title, self.historylog_filename, "%s (*.log)" % _("History logs")) self.redirect_stdio.emit(True) if filename: filename = osp.normpath(filename) try: encoding.write(to_text_string(self.get_text_with_eol()), filename) self.historylog_filename = filename CONF.set('main', 'historylog_filename', filename) except EnvironmentError as error: QMessageBox.critical(self, title, _("<b>Unable to save file '%s'</b>" "<br><br>Error message:<br>%s" ) % (osp.basename(filename), to_text_string(error))) #------ Basic keypress event handler def on_enter(self, command): """on_enter""" self.execute_command(command) def execute_command(self, command): self.execute.emit(command) self.add_to_history(command) self.new_input_line = True def on_new_line(self): """On new input line""" self.set_cursor_position('eof') self.current_prompt_pos = self.get_position('cursor') self.new_input_line = False @Slot() def paste(self): """Reimplemented slot to handle multiline paste action""" if self.new_input_line: self.on_new_line() ConsoleBaseWidget.paste(self) def keyPressEvent(self, event): """ Reimplement Qt Method Basic keypress event handler (reimplemented in InternalShell to add more sophisticated features) """ if self.preprocess_keyevent(event): # Event was accepted in self.preprocess_keyevent return self.postprocess_keyevent(event) def preprocess_keyevent(self, event): """Pre-process keypress event: return True if event is accepted, false otherwise""" # Copy must be done first to be able to copy read-only text parts # (otherwise, right below, we would remove selection # if not on current line) ctrl = event.modifiers() & Qt.ControlModifier meta = event.modifiers() & Qt.MetaModifier # meta=ctrl in OSX if event.key() == Qt.Key_C and \ ((Qt.MetaModifier | Qt.ControlModifier) & event.modifiers()): if meta and sys.platform == 'darwin': self.interrupt() elif ctrl: self.copy() event.accept() return True if self.new_input_line and ( len(event.text()) or event.key() in \ (Qt.Key_Up, Qt.Key_Down, Qt.Key_Left, Qt.Key_Right) ): self.on_new_line() return False def postprocess_keyevent(self, event): """Post-process keypress event: in InternalShell, this is method is called when shell is ready""" event, text, key, ctrl, shift = restore_keyevent(event) # Is cursor on the last line? and after prompt? if len(text): #XXX: Shouldn't it be: `if len(unicode(text).strip(os.linesep))` ? if self.has_selected_text(): self.check_selection() self.restrict_cursor_position(self.current_prompt_pos, 'eof') cursor_position = self.get_position('cursor') if key in (Qt.Key_Return, Qt.Key_Enter): if self.is_cursor_on_last_line(): self._key_enter() # add and run selection else: self.insert_text(self.get_selected_text(), at_end=True) elif key == Qt.Key_Insert and not shift and not ctrl: self.setOverwriteMode(not self.overwriteMode()) elif key == Qt.Key_Delete: if self.has_selected_text(): self.check_selection() self.remove_selected_text() elif self.is_cursor_on_last_line(): self.stdkey_clear() elif key == Qt.Key_Backspace: self._key_backspace(cursor_position) elif key == Qt.Key_Tab: self._key_tab() elif key == Qt.Key_Space and ctrl: self._key_ctrl_space() elif key == Qt.Key_Left: if self.current_prompt_pos == cursor_position: # Avoid moving cursor on prompt return method = self.extend_selection_to_next if shift \ else self.move_cursor_to_next method('word' if ctrl else 'character', direction='left') elif key == Qt.Key_Right: if self.is_cursor_at_end(): return method = self.extend_selection_to_next if shift \ else self.move_cursor_to_next method('word' if ctrl else 'character', direction='right') elif (key == Qt.Key_Home) or ((key == Qt.Key_Up) and ctrl): self._key_home(shift, ctrl) elif (key == Qt.Key_End) or ((key == Qt.Key_Down) and ctrl): self._key_end(shift, ctrl) elif key == Qt.Key_Up: if not self.is_cursor_on_last_line(): self.set_cursor_position('eof') y_cursor = self.get_coordinates(cursor_position)[1] y_prompt = self.get_coordinates(self.current_prompt_pos)[1] if y_cursor > y_prompt: self.stdkey_up(shift) else: self.browse_history(backward=True) elif key == Qt.Key_Down: if not self.is_cursor_on_last_line(): self.set_cursor_position('eof') y_cursor = self.get_coordinates(cursor_position)[1] y_end = self.get_coordinates('eol')[1] if y_cursor < y_end: self.stdkey_down(shift) else: self.browse_history(backward=False) elif key in (Qt.Key_PageUp, Qt.Key_PageDown): #XXX: Find a way to do this programmatically instead of calling # widget keyhandler (this won't work if the *event* is coming from # the event queue - i.e. if the busy buffer is ever implemented) ConsoleBaseWidget.keyPressEvent(self, event) elif key == Qt.Key_Escape and shift: self.clear_line() elif key == Qt.Key_Escape: self._key_escape() elif key == Qt.Key_L and ctrl: self.clear_terminal() elif key == Qt.Key_V and ctrl: self.paste() elif key == Qt.Key_X and ctrl: self.cut() elif key == Qt.Key_Z and ctrl: self.undo() elif key == Qt.Key_Y and ctrl: self.redo() elif key == Qt.Key_A and ctrl: self.selectAll() elif key == Qt.Key_Question and not self.has_selected_text(): self._key_question(text) elif key == Qt.Key_ParenLeft and not self.has_selected_text(): self._key_parenleft(text) elif key == Qt.Key_Period and not self.has_selected_text(): self._key_period(text) elif len(text) and not self.isReadOnly(): self.hist_wholeline = False self.insert_text(text) self._key_other(text) else: # Let the parent widget handle the key press event ConsoleBaseWidget.keyPressEvent(self, event) #------ Key handlers def _key_enter(self): command = self.input_buffer self.insert_text('\n', at_end=True) self.on_enter(command) self.flush() def _key_other(self, text): raise NotImplementedError def _key_backspace(self, cursor_position): raise NotImplementedError def _key_tab(self): raise NotImplementedError def _key_ctrl_space(self): raise NotImplementedError def _key_home(self, shift, ctrl): if self.is_cursor_on_last_line(): self.stdkey_home(shift, ctrl, self.current_prompt_pos) def _key_end(self, shift, ctrl): if self.is_cursor_on_last_line(): self.stdkey_end(shift, ctrl) def _key_pageup(self): raise NotImplementedError def _key_pagedown(self): raise NotImplementedError def _key_escape(self): raise NotImplementedError def _key_question(self, text): raise NotImplementedError def _key_parenleft(self, text): raise NotImplementedError def _key_period(self, text): raise NotImplementedError #------ History Management def load_history(self): """Load history from a .py file in user home directory""" if osp.isfile(self.history_filename): rawhistory, _ = encoding.readlines(self.history_filename) rawhistory = [line.replace('\n', '') for line in rawhistory] if rawhistory[1] != self.INITHISTORY[1]: rawhistory[1] = self.INITHISTORY[1] else: rawhistory = self.INITHISTORY history = [line for line in rawhistory \ if line and not line.startswith('#')] # Truncating history to X entries: while len(history) >= CONF.get('historylog', 'max_entries'): del history[0] while rawhistory[0].startswith('#'): del rawhistory[0] del rawhistory[0] # Saving truncated history: encoding.writelines(rawhistory, self.history_filename) return history def browse_history(self, backward): """Browse history""" if self.is_cursor_before('eol') and self.hist_wholeline: self.hist_wholeline = False tocursor = self.get_current_line_to_cursor() text, self.histidx = self.__find_in_history(tocursor, self.histidx, backward) if text is not None: if self.hist_wholeline: self.clear_line() self.insert_text(text) else: cursor_position = self.get_position('cursor') # Removing text from cursor to the end of the line self.remove_text('cursor', 'eol') # Inserting history text self.insert_text(text) self.set_cursor_position(cursor_position) def __find_in_history(self, tocursor, start_idx, backward): """Find text 'tocursor' in history, from index 'start_idx'""" if start_idx is None: start_idx = len(self.history) # Finding text in history step = -1 if backward else 1 idx = start_idx if len(tocursor) == 0 or self.hist_wholeline: idx += step if idx >= len(self.history) or len(self.history) == 0: return "", len(self.history) elif idx < 0: idx = 0 self.hist_wholeline = True return self.history[idx], idx else: for index in range(len(self.history)): idx = (start_idx+step*(index+1)) % len(self.history) entry = self.history[idx] if entry.startswith(tocursor): return entry[len(tocursor):], idx else: return None, start_idx #------ Simulation standards input/output def write_error(self, text): """Simulate stderr""" self.flush() self.write(text, flush=True, error=True) if DEBUG: STDERR.write(text) def write(self, text, flush=False, error=False, prompt=False): """Simulate stdout and stderr""" if prompt: self.flush() if not is_string(text): # This test is useful to discriminate QStrings from decoded str text = to_text_string(text) self.__buffer.append(text) ts = time.time() if flush or prompt: self.flush(error=error, prompt=prompt) elif ts - self.__timestamp > 0.05: self.flush(error=error) self.__timestamp = ts # Timer to flush strings cached by last write() operation in series self.__flushtimer.start(50) def flush(self, error=False, prompt=False): """Flush buffer, write text to console""" # Fix for Issue 2452 if PY3: try: text = "".join(self.__buffer) except TypeError: text = b"".join(self.__buffer) try: text = text.decode( locale.getdefaultlocale()[1] ) except: pass else: text = "".join(self.__buffer) self.__buffer = [] self.insert_text(text, at_end=True, error=error, prompt=prompt) QCoreApplication.processEvents() self.repaint() # Clear input buffer: self.new_input_line = True #------ Text Insertion def insert_text(self, text, at_end=False, error=False, prompt=False): """ Insert text at the current cursor position or at the end of the command line """ if at_end: # Insert text at the end of the command line self.append_text_to_shell(text, error, prompt) else: # Insert text at current cursor position ConsoleBaseWidget.insert_text(self, text) #------ Re-implemented Qt Methods def focusNextPrevChild(self, next): """ Reimplemented to stop Tab moving to the next window """ if next: return False return ConsoleBaseWidget.focusNextPrevChild(self, next) #------ Drag and drop def dragEnterEvent(self, event): """Drag and Drop - Enter event""" event.setAccepted(event.mimeData().hasFormat("text/plain")) def dragMoveEvent(self, event): """Drag and Drop - Move event""" if (event.mimeData().hasFormat("text/plain")): event.setDropAction(Qt.MoveAction) event.accept() else: event.ignore() def dropEvent(self, event): """Drag and Drop - Drop event""" if (event.mimeData().hasFormat("text/plain")): text = to_text_string(event.mimeData().text()) if self.new_input_line: self.on_new_line() self.insert_text(text, at_end=True) self.setFocus() event.setDropAction(Qt.MoveAction) event.accept() else: event.ignore() def drop_pathlist(self, pathlist): """Drop path list""" raise NotImplementedError
def setup(self, check_all=None, exclude_private=None, exclude_uppercase=None, exclude_capitalized=None, exclude_unsupported=None, excluded_names=None, truncate=None, minmax=None, remote_editing=None, autorefresh=None): """Setup the namespace browser""" assert self.shellwidget is not None self.check_all = check_all self.exclude_private = exclude_private self.exclude_uppercase = exclude_uppercase self.exclude_capitalized = exclude_capitalized self.exclude_unsupported = exclude_unsupported self.excluded_names = excluded_names self.truncate = truncate self.minmax = minmax self.remote_editing = remote_editing self.autorefresh = autorefresh if self.editor is not None: self.editor.setup_menu(truncate, minmax) self.exclude_private_action.setChecked(exclude_private) self.exclude_uppercase_action.setChecked(exclude_uppercase) self.exclude_capitalized_action.setChecked(exclude_capitalized) self.exclude_unsupported_action.setChecked(exclude_unsupported) if self.auto_refresh_button is not None: self.auto_refresh_button.setChecked(autorefresh) self.refresh_table() return self.editor = RemoteCollectionsEditorTableView( self, None, truncate=truncate, minmax=minmax, remote_editing=remote_editing, get_value_func=self.get_value, set_value_func=self.set_value, new_value_func=self.set_value, remove_values_func=self.remove_values, copy_value_func=self.copy_value, is_list_func=self.is_list, get_len_func=self.get_len, is_array_func=self.is_array, is_image_func=self.is_image, is_dict_func=self.is_dict, is_data_frame_func=self.is_data_frame, is_series_func=self.is_series, get_array_shape_func=self.get_array_shape, get_array_ndim_func=self.get_array_ndim, oedit_func=self.oedit, plot_func=self.plot, imshow_func=self.imshow, show_image_func=self.show_image) self.editor.sig_option_changed.connect(self.sig_option_changed.emit) self.editor.sig_files_dropped.connect(self.import_data) # Setup layout layout = QVBoxLayout() blayout = QHBoxLayout() toolbar = self.setup_toolbar(exclude_private, exclude_uppercase, exclude_capitalized, exclude_unsupported, autorefresh) for widget in toolbar: blayout.addWidget(widget) # Options menu options_button = create_toolbutton(self, text=_('Options'), icon=ima.icon('tooloptions')) options_button.setPopupMode(QToolButton.InstantPopup) menu = QMenu(self) editor = self.editor actions = [ self.exclude_private_action, self.exclude_uppercase_action, self.exclude_capitalized_action, self.exclude_unsupported_action, None, editor.truncate_action ] if is_module_installed('numpy'): actions.append(editor.minmax_action) add_actions(menu, actions) options_button.setMenu(menu) blayout.addStretch() blayout.addWidget(options_button) layout.addLayout(blayout) layout.addWidget(self.editor) self.setLayout(layout) layout.setContentsMargins(0, 0, 0, 0) self.sig_option_changed.connect(self.option_changed)
def more_options_device_view(self, button): if 'Disconnect' in button.text(): menu = QMenu("Menu", self) menu.addAction("Pair / Ping", self.ping_paired_device) menu.addAction("Attempt TCPIP on device", self.tcpip_paired_device) menu.addAction("Forget device", self.forget_paired_device) else: menu = QMenu("Menu", self) menu.addAction("Attempt TCPIP on device", self.tcpip_paired_device) menu.addAction("Attempt reconnection", self.ping_paired_device) menu.addAction("Refresh", self.refresh_devices) _, identifier = self.current_device_identifier() if platform.System.system() == "Linux" and identifier.count('.') >= 3: menu.addAction("Add Desktop Shortcut to this device", self.create_desktop_shortcut_linux_os) menu.exec_( self.devices_view.mapToGlobal( QPoint( self.devices_view.visualItemRect(button).x() + 22, self.devices_view.visualItemRect(button).y() + 22)))
class BaseTabs(QTabWidget): """TabWidget with context menu and corner widgets""" sig_close_tab = Signal(int) def __init__(self, parent, actions=None, menu=None, corner_widgets=None, menu_use_tooltips=False): QTabWidget.__init__(self, parent) self.setUsesScrollButtons(True) # To style tabs on Mac if sys.platform == 'darwin': self.setObjectName('plugin-tab') self.corner_widgets = {} self.menu_use_tooltips = menu_use_tooltips if menu is None: self.menu = QMenu(self) if actions: add_actions(self.menu, actions) else: self.menu = menu # Corner widgets if corner_widgets is None: corner_widgets = {} corner_widgets.setdefault(Qt.TopLeftCorner, []) corner_widgets.setdefault(Qt.TopRightCorner, []) self.browse_button = create_toolbutton(self, icon=ima.icon('browse_tab'), tip=_("Browse tabs")) self.browse_tabs_menu = QMenu(self) self.browse_button.setMenu(self.browse_tabs_menu) self.browse_button.setPopupMode(self.browse_button.InstantPopup) self.browse_tabs_menu.aboutToShow.connect(self.update_browse_tabs_menu) corner_widgets[Qt.TopLeftCorner] += [self.browse_button] self.set_corner_widgets(corner_widgets) def update_browse_tabs_menu(self): """Update browse tabs menu""" self.browse_tabs_menu.clear() names = [] dirnames = [] for index in range(self.count()): if self.menu_use_tooltips: text = to_text_string(self.tabToolTip(index)) else: text = to_text_string(self.tabText(index)) names.append(text) if osp.isfile(text): # Testing if tab names are filenames dirnames.append(osp.dirname(text)) offset = None # If tab names are all filenames, removing common path: if len(names) == len(dirnames): common = get_common_path(dirnames) if common is None: offset = None else: offset = len(common) + 1 if offset <= 3: # Common path is not a path but a drive letter... offset = None for index, text in enumerate(names): tab_action = create_action( self, text[offset:], icon=self.tabIcon(index), toggled=lambda state, index=index: self.setCurrentIndex(index), tip=self.tabToolTip(index)) tab_action.setChecked(index == self.currentIndex()) self.browse_tabs_menu.addAction(tab_action) def set_corner_widgets(self, corner_widgets): """ Set tabs corner widgets corner_widgets: dictionary of (corner, widgets) corner: Qt.TopLeftCorner or Qt.TopRightCorner widgets: list of widgets (may contains integers to add spacings) """ assert isinstance(corner_widgets, dict) assert all(key in (Qt.TopLeftCorner, Qt.TopRightCorner) for key in corner_widgets) self.corner_widgets.update(corner_widgets) for corner, widgets in list(self.corner_widgets.items()): cwidget = QWidget() cwidget.hide() prev_widget = self.cornerWidget(corner) if prev_widget: prev_widget.close() self.setCornerWidget(cwidget, corner) clayout = QHBoxLayout() clayout.setContentsMargins(0, 0, 0, 0) for widget in widgets: if isinstance(widget, int): clayout.addSpacing(widget) else: clayout.addWidget(widget) cwidget.setLayout(clayout) cwidget.show() def add_corner_widgets(self, widgets, corner=Qt.TopRightCorner): self.set_corner_widgets( {corner: self.corner_widgets.get(corner, []) + widgets}) def contextMenuEvent(self, event): """Override Qt method""" self.setCurrentIndex(self.tabBar().tabAt(event.pos())) if self.menu: self.menu.popup(event.globalPos()) def mousePressEvent(self, event): """Override Qt method""" if event.button() == Qt.MidButton: index = self.tabBar().tabAt(event.pos()) if index >= 0: self.sig_close_tab.emit(index) event.accept() return QTabWidget.mousePressEvent(self, event) def keyPressEvent(self, event): """Override Qt method""" ctrl = event.modifiers() & Qt.ControlModifier key = event.key() handled = False if ctrl and self.count() > 0: index = self.currentIndex() if key == Qt.Key_PageUp: if index > 0: self.setCurrentIndex(index - 1) else: self.setCurrentIndex(self.count() - 1) handled = True elif key == Qt.Key_PageDown: if index < self.count() - 1: self.setCurrentIndex(index + 1) else: self.setCurrentIndex(0) handled = True if not handled: QTabWidget.keyPressEvent(self, event) def set_close_function(self, func): """Setting Tabs close function None -> tabs are not closable""" state = func is not None if state: self.sig_close_tab.connect(func) try: # Assuming Qt >= 4.5 QTabWidget.setTabsClosable(self, state) self.tabCloseRequested.connect(func) except AttributeError: # Workaround for Qt < 4.5 close_button = create_toolbutton(self, triggered=func, icon=ima.icon('fileclose'), tip=_("Close current tab")) self.setCornerWidget(close_button if state else None)