Ejemplo n.º 1
0
class WorkflowWidget(QWidget):
    sigAddFunction = Signal(object)

    def __init__(self, workflowview: QAbstractItemView):
        super(WorkflowWidget, self).__init__()

        self.view = workflowview

        self.toolbar = QToolBar()
        self.addfunctionmenu = QToolButton()
        self.addfunctionmenu.setIcon(QIcon(path("icons/addfunction.png")))
        self.addfunctionmenu.setText("Add Function")
        # Defer menu population to once the plugins have been loaded; otherwise, the menu may not contain anything
        # if this widget is init'd before all plugins have been loaded.
        self.functionmenu = QMenu()
        self.functionmenu.aboutToShow.connect(self.populateFunctionMenu)
        self.addfunctionmenu.setMenu(self.functionmenu)
        self.addfunctionmenu.setPopupMode(QToolButton.InstantPopup)
        self.toolbar.addWidget(self.addfunctionmenu)
        # self.toolbar.addAction(QIcon(path('icons/up.png')), 'Move Up')
        # self.toolbar.addAction(QIcon(path('icons/down.png')), 'Move Down')
        self.toolbar.addAction(QIcon(path("icons/folder.png")),
                               "Load Workflow")
        self.toolbar.addAction(QIcon(path("icons/trash.png")),
                               "Delete Operation", self.deleteOperation)

        v = QVBoxLayout()
        v.addWidget(self.view)
        v.addWidget(self.toolbar)
        v.setContentsMargins(0, 0, 0, 0)
        self.setLayout(v)

    def populateFunctionMenu(self):
        self.functionmenu.clear()
        sortingDict = {}
        for plugin in pluginmanager.get_plugins_of_type("OperationPlugin"):
            typeOfOperationPlugin = plugin.getCategory()
            if not typeOfOperationPlugin in sortingDict.keys():
                sortingDict[typeOfOperationPlugin] = []
            sortingDict[typeOfOperationPlugin].append(plugin)
        for key in sortingDict.keys():
            self.functionmenu.addSeparator()
            self.functionmenu.addAction(key)
            self.functionmenu.addSeparator()
            for plugin in sortingDict[key]:
                self.functionmenu.addAction(
                    plugin.name,
                    partial(self.addOperation, plugin, autoconnectall=True))

    def addOperation(self, operation: OperationPlugin, autoconnectall=True):
        self.view.model().workflow.addOperation(operation(), autoconnectall)
        print("selected new row:", self.view.model().rowCount() - 1)
        self.view.setCurrentIndex(self.view.model().index(
            self.view.model().rowCount() - 1, 0))

    def deleteOperation(self):
        for index in self.view.selectedIndexes():
            operation = self.view.model().workflow.operations[index.row()]
            self.view.model().workflow.remove_operation(operation)
Ejemplo n.º 2
0
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 selection'),
                                     icon=ima.icon('collapse_selection'),
                                     triggered=self.collapse_selection)
        self.expand_selection_action = create_action(self,
                                     text=_('Expand selection'),
                                     icon=ima.icon('expand_selection'),
                                     triggered=self.expand_selection)
        return [self.collapse_all_action, self.expand_all_action,
                self.restore_action, None,
                self.collapse_selection_action, self.expand_selection_action]
                     
    def update_menu(self):
        self.menu.clear()
        items = self.selectedItems()
        actions = self.get_actions_from_items(items)
        if actions:
            actions.append(None)
        actions += self.common_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())
Ejemplo n.º 3
0
class Projects(SpyderPluginWidget):
    """Projects plugin."""

    CONF_SECTION = 'project_explorer'
    CONF_FILE = False
    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.completions_available = 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.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.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))

        # 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(
            functools.partial(lspmgr.project_path_update,
                              update_kind='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_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(
            functools.partial(lspmgr.project_path_update,
                              update_kind='deletion'))
        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)

    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,
            ]
        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)

    @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)

    def open_project(self,
                     path=None,
                     restart_consoles=True,
                     save_previous_files=True):
        """Open the project located in `path`"""
        self.notify_project_open(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())

        project = EmptyProject(path)
        self.current_active_project = project
        self.latest_project = project
        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)

        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')
        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)
            self.recent_projects = self.recent_projects[:10]

    def register_lsp_server_settings(self, settings):
        """Enable LSP workspace functions."""
        self.completions_available = 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.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
Ejemplo n.º 4
0
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
        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(
            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(
            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(), '~')

                    def slot():
                        self.switch_to_plugin()
                        self.open_project(path=project)

                    action = create_action(self,
                        name,
                        icon = ima.icon('project'),
                        triggered=slot)
                    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()
            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()

            if self.main.editor is not None:
                self.set_project_filenames(
                    self.main.editor.get_open_filenames())

    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]
Ejemplo n.º 5
0
class PluginWidget(BasePluginWidget):
    """
    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."""
        BasePluginWidget.__init__(self, main)
        assert self.CONF_SECTION is not 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.dockwidget = None
        self.mainwindow = 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)
        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()

        # Undock action
        self.create_undock_action()
        self.plugin_actions = self.get_plugin_actions() + [
            MENU_SEPARATOR, self.undock_action
        ]

        # Options button and menu
        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 create_mainwindow(self):
        """
        Create a QMainWindow instance containing this plugin.

        Note: this method is currently not used in Spyder core plugins
        """
        self.mainwindow = mainwindow = PluginMainWindow(self)
        mainwindow.setAttribute(Qt.WA_DeleteOnClose)
        icon = self.get_plugin_icon()
        if is_text_string(icon):
            icon = self.get_icon(icon)
        mainwindow.setWindowIcon(icon)
        mainwindow.setWindowTitle(self.get_plugin_title())
        mainwindow.setCentralWidget(self)
        self.refresh_plugin()
        return mainwindow

    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 enable:
            self.dockwidget.raise_()
            widget = self.get_focus_widget()
            if widget 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.

        Use a SIGNAL to call it, e.g.:
        plugin.sig_option_changed.emit('show_all', checked)
        """
        CONF.set(self.CONF_SECTION, str(option), value)

    def get_option(self, option, default=NoDefault):
        """Get a plugin 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):
        """Clear the menu of the plugin and add the actions."""
        self.options_menu.clear()
        self.plugin_actions = self.get_plugin_actions() + [
            MENU_SEPARATOR, self.undock_action
        ]
        add_actions(self.options_menu, self.plugin_actions)
Ejemplo n.º 6
0
class Q7Tree(Q7Window, Ui_Q7TreeWindow):
    def __init__(self, control, path, fgprintindex):
        Q7Window.__init__(self, Q7Window.VIEW_TREE, control, path, fgprintindex)
        self._depthExpanded = 0
        self._lastEntered = None
        self.lastdiag = None
        self._linkwindow = None
        self._querywindow = None
        self._vtkwindow = None
        self._selectwindow = None
        self._column = {NMT.COLUMN_SIDS: OCTXT.ShowSIDSColumn,
                        NMT.COLUMN_FLAG_LINK: OCTXT.ShowLinkColumn,
                        NMT.COLUMN_FLAG_SELECT: OCTXT.ShowSelectColumn,
                        NMT.COLUMN_FLAG_CHECK: OCTXT.ShowCheckColumn,
                        NMT.COLUMN_FLAG_USER: OCTXT.ShowUserColumn,
                        NMT.COLUMN_SHAPE: OCTXT.ShowShapeColumn,
                        NMT.COLUMN_DATATYPE: OCTXT.ShowDataTypeColumn}
        self.selectForLinkSrc = None  # one link source per tree view allowed

        #self.treeview.expanded[QModelIndex].connect(self.expandNode)
        self.treeview.collapsed.connect(self.collapseNode)
        self.treeview.pressed[QModelIndex].connect(self.clickedPressedNode)
        self.treeview.customContextMenuRequested.connect(self.clickedNode)

        # QObject.connect(self.treeview,
        #                SIGNAL("expanded(QModelIndex)"),
        #                self.expandNode)
        # QObject.connect(self.treeview,
        #                SIGNAL("collapsed()"),
        #                self.collapseNode)
        # QObject.connect(self.treeview,
        #                SIGNAL("pressed(QModelIndex)"),
        #                self.clickedPressedNode)
        # QObject.connect(self.treeview,
        #                SIGNAL("customContextMenuRequested(QPoint)"),
        #                self.clickedNode)

        self.bSave.clicked.connect(self.savetree)
        self.lockable(self.bSave)
        self.bQueryView.clicked.connect(self.queryview)
        self.lockable(self.bQueryView)
        self.bSaveAs.clicked.connect(self.savetreeas)
        self.lockable(self.bSaveAs)
        self.bInfo.clicked.connect(self.infoTreeView)
        self.bZoomIn.clicked.connect(self.expandLevel)
        self.bZoomOut.clicked.connect(self.collapseLevel)
        self.bZoomAll.clicked.connect(self.expandMinMax)
        self.bFormView.clicked.connect(self.formview)
        self.bMarkAll.clicked.connect(self.markall)
        self.bUnmarkAll_1.clicked.connect(self.unmarkall)
        self.bUnmarkAll_2.clicked.connect(self.unmarkall)
        self.bPreviousMark.clicked.connect(self.previousmark)
        self.bNextMark.clicked.connect(self.nextmark)
        self.bSwapMarks.clicked.connect(self.swapmarks)
        self.bMarksAsList.clicked.connect(self.selectionlist)
        self.bVTKView.clicked.connect(self.vtkview)
        self.lockable(self.bVTKView)
        self.bScreenShot.clicked.connect(self.screenshot)
        self.bCheck.clicked.connect(self.check)
        self.bCheckList.clicked.connect(self.checklist)
        self.bClearChecks.clicked.connect(self.clearchecks)
        self.bLinkView.clicked.connect(self.linklist)
        self.bPatternView.clicked.connect(self.patternlist)
        self.bToolsView.clicked.connect(self.tools)
        self.setContextMenuPolicy(Qt.CustomContextMenu)
        self.popupmenu = QMenu()
        self.diagview = None
        lmodel = self.FG.model
        self.treeview.setModel(lmodel)
        self.treeview.setItemDelegate(Q7TreeItemDelegate(self.treeview, lmodel))
        self.treeview.setControlWindow(self, self.FG.index)
        if (self._control.transientRecurse or OCTXT.RecursiveTreeDisplay):
            self.expandMinMax()
        if (self._control.transientVTK): self.vtkview()
        self._control.transientRecurse = False
        self._control.transientVTK = False
        self.clearchecks()
        #
        self.bCheckList.setDisabled(True)
        if (not OCTXT._HasProPackage):
            self.bToolsView.setDisabled(True)
        self.bCheckView.setDisabled(True)
        self.bPatternDB.setDisabled(True)
        self.bAddLink.clicked.connect(self.linkadd)
        self.bSelectLinkSrc.clicked.connect(self.linkselectsrc)
        self.bSelectLinkDst.clicked.connect(self.linkselectdst)
        self.bAddLink.setDisabled(True)
        self.lineEdit.returnPressed.connect(self.jumpToNode)
        # QObject.connect(self.lineEdit,
        #                SIGNAL("returnPressed()"),
        #                self.jumpToNode)
        tvh = self.treeview.header()
        tvh.setContextMenuPolicy(Qt.CustomContextMenu)
        tvh.customContextMenuRequested.connect(self.headerMenu)
        self._hmenu = QMenu()
        self._hmenu._idx = {}
        self._tlist = (('SIDS type', NMT.COLUMN_SIDS),
                       ('Link flag', NMT.COLUMN_FLAG_LINK),
                       ('Mark flag', NMT.COLUMN_FLAG_SELECT),
                       ('Check flag', NMT.COLUMN_FLAG_CHECK),
                       ('User flag', NMT.COLUMN_FLAG_USER),
                       ('Shape', NMT.COLUMN_SHAPE),
                       ('Data type', NMT.COLUMN_DATATYPE))
        for (tag, idx) in self._tlist:
            a = QAction(tag, self._hmenu, checkable=True)
            self._hmenu._idx[idx] = a
            if (self._column[idx]):
                a.setChecked(True)
            else:
                a.setChecked(False)
            self._hmenu.addAction(a)
            self.treeview.setColumnHidden(idx, not self._column[idx])
        self._recursiveAddNewNode = False
        self.updateTreeStatus()

    def headerMenu(self, pos):
        for (tag, idx) in self._tlist:
            self._hmenu._idx[idx].setChecked(self._column[idx])
        self._hmenu.exec_(self.treeview.mapToGlobal(pos))
        for (tag, idx) in self._tlist:
            if (self._hmenu._idx[idx].isChecked()):
                self._column[idx] = True
            else:
                self._column[idx] = False
            self.treeview.setColumnHidden(idx, not self._column[idx])

    def model(self):
        return self.FG.model

    def modelIndex(self, idx):
        if not idx.isValid():
            return -1
        midx = idx
        if idx.model() != self.treeview.M():
            midx = self.treeview.model().mapToSource(idx)
        return midx

    def modelData(self, idx):
        if not idx.isValid():
            return None
        return self.modelIndex(idx).internalPointer()

    def savetree(self):
        if (not (self.FG.isSaveable() and self.FG.isModified())):
            return
        self._control.savedirect(self.FG)
        self.updateTreeStatus()

    def tools(self):
        from CGNS.NAV.wtools import Q7ToolsView
        if (self._control._toolswindow is None):
            self._control._toolswindow = Q7ToolsView(self._control,
                                                     self.FG,
                                                     self)
            self._control._toolswindow.show()
        else:
            self._control._toolswindow.raise_()

    def savetreeas(self):
        self._control.save(self.FG)
        self.updateTreeStatus()

    def infoTreeView(self):
        self._control.helpWindow('Tree')

    def screenshot(self):
        self.treeview.model().sort(0)
        sshot = QScreen.grabWindow(self.treeview.winId())
        sshot.save('/tmp/foo.png', 'png')

    def expandMinMax(self):
        if (self._depthExpanded == self.FG.depth - 2):
            self._depthExpanded = -1
            self.treeview.collapseAll()
        else:
            self._depthExpanded = self.FG.depth - 2
            self.treeview.expandAll()
        self.resizeAll()

    def resetOptions(self):
        if (OCTXT.AutoExpand):
            self.treeview.setAutoExpandDelay(1000)
        else:
            self.treeview.setAutoExpandDelay(-1)

    def expandLevel(self):
        if (self._depthExpanded < self.FG.depth - 2):
            self._depthExpanded += 1
        self.treeview.expandToDepth(self._depthExpanded)
        self.resizeAll()

    def collapseLevel(self):
        if (self._depthExpanded != -1): self._depthExpanded -= 1
        if (self._depthExpanded == -1):
            self.treeview.collapseAll()
        else:
            self.treeview.expandToDepth(self._depthExpanded)
        self.resizeAll()

    def updateStatus(self, node):
        if (not self.lineEditLock.isChecked()):
            self.lineEdit.clear()
            self.lineEdit.insert(node.sidsPath())

    def jumpToNode(self):
        path = self.lineEdit.text()
        self.treeview.selectByPath(path)

    def popform(self):
        self.formview()

    def openLkTree(self):
        self.busyCursor()
        filename = self.getLastEntered().sidsLinkFilename()
        if (filename is not None):
            self._control.loadfile(filename)
        self.readyCursor()

    def openSubTree(self):
        self.busyCursor()
        node = self.getLastEntered().sidsPath()
        child = Q7Tree(self._control, node, self.FG)
        self.readyCursor()
        child.show()

    def pop0(self):
        pass

    def newnodebrother(self):
        if (self.getLastEntered() is not None):
            self.model().newNodeBrother(self.getLastEntered())

    def newnodechild(self):
        if (self.getLastEntered() is not None):
            self.model().newNodeChild(self.getLastEntered())

    def marknode(self):
        if (self.getLastEntered() is not None):
            self.treeview.markNode(self.getLastEntered())

    def mcopy(self):
        if (self.getLastEntered() is not None):
            self.model().copyNode(self.getLastEntered())
            self.clearOtherSelections()

    def mcutselected(self):
        self.model().cutAllSelectedNodes()
        self.clearLastEntered()
        self.clearOtherSelections()

    def mcut(self):
        if (self.getLastEntered() is not None):
            self.model().cutNode(self.getLastEntered())
            self.clearLastEntered()
            self.clearOtherSelections()

    def mpasteasbrotherselected(self):
        self.model().pasteAsBrotherAllSelectedNodes()

    def mpasteasbrother(self):
        if (self.getLastEntered() is not None):
            self.model().pasteAsBrother(self.getLastEntered())

    def mpasteaschildselected(self):
        self.model().pasteAsChildAllSelectedNodes()

    def mpasteaschild(self):
        if (self.getLastEntered() is not None):
            self.model().pasteAsChild(self.getLastEntered())

    def updateMenu(self, nodeidxs):
        nodeidx = self.modelIndex(nodeidxs)
        if (not nodeidx.isValid): return False
        if (nodeidx.internalPointer() is None): return False
        if (nodeidx.internalPointer().sidsPath() == '/CGNSTree'): return False
        self.setLastEntered(nodeidxs)
        if (nodeidx != -1):
            node = nodeidx.internalPointer()
            lknode = not node.sidsIsLink()
            lznode = node.hasLazyLoad()
            actlist = (
                ("%s goodies" % node.sidsType(),),
                None,
                ("Expand sub-tree from this node", self.expand_sb, 'Ctrl++', False),
                ("Collapses sub-tree from this node", self.collapse_sb, 'Ctrl+-', False),                
                None,
                ['Mark nodes...',[
                ("Mark/unmark node", self.marknode, 'Space', False),
                 None,
                ("Mark all nodes same SIDS type", self.marknode_t, 'Ctrl+1', False),
                ("Mark all nodes same name", self.marknode_n, 'Ctrl+2', False),
                ("Mark all nodes same value", self.marknode_v, 'Ctrl+3', False),
                 None,
                ("Mark parent path", self.marknode_p, 'Ctrl+4', False)]],
                ("Add new child node", self.newnodechild, 'Ctrl+A', False),
                ("Add new brother node", self.newnodebrother, 'Ctrl+Z', False),
                #            None,
                #            ("Open form",self.popform,'Ctrl+F',False),
                #            ("Open view",self.openSubTree,'Ctrl+W',False),
                #            ("Open view on linked-to file",self.openLkTree,'Ctrl+O',lknode),
                None,
                ("Load node data in memory", self.dataLoad, 'Ctrl+L', not lznode),
                ("Release memory node data", self.dataRelease, 'Ctrl+R', lznode),
                None,
                ("Copy current", self.mcopy, 'Ctrl+C', False),
                ("Cut current", self.mcut, 'Ctrl+X', False),
                ("Paste as brother", self.mpasteasbrother, 'Ctrl+V', False),
                ("Paste as child", self.mpasteaschild, 'Ctrl+Y', False),
                None,
                ['On selected nodes...',[
                ("Expand sub-tree from all selected nodes", self.sexpand_sb, 'Ctrl+Shift++', False),
                ("Collapses sub-tree from all selected nodes", self.scollapse_sb, 'Ctrl+Shift+-', False),
                None,
                ("Cut all selected", self.mcutselected, 'Ctrl+Shift+X', False),
                ("Paste as brother for each selected",
                 self.mpasteasbrotherselected, 'Ctrl+Shift+V', False),
                ("Paste as child for each selected",
                 self.mpasteaschildselected, 'Ctrl+Shift+Y', False),
                 None,
                ("Load nodes data in memory for each selected",
                 self.dataLoadSelected, 'Ctrl+Shift+L', False),
                ("Release memory node data for each selected",
                 self.dataReleaseSelected, 'Ctrl+Shift+R', False)]],
            )
            self.popupmenu.clear()
            self.popupmenu.setTitle('Node menu')
            for aparam in actlist:
                if (aparam is None):
                    self.popupmenu.addSeparator()
                elif (len(aparam) == 1):
                    stp = node.sidsType()
                    tag = '_GM_{}'.format(stp)
                    subm = self.popupmenu.addMenu('{}...'.format(stp))
                    a = QAction("About %s" % node.sidsType(), self,
                                triggered=self.aboutSIDS)
                    subm.addAction(a)
                    patmenu = subm.addMenu('Insert pattern')
                    self.patternMenu(patmenu, node.sidsNode())
                    subm.addSeparator()
                    if (hasattr(self, tag)):
                        getattr(self, tag)(subm, node)
                else:
                    if isinstance(aparam,list):
                        subm = self.popupmenu.addMenu(aparam[0])
                        for aaparam in aparam[1]:
                            if (aaparam is None):
                                 subm.addSeparator()
                            else:
                                a = QAction(aaparam[0], self, triggered=aaparam[1])
                                if (aaparam[2] is not None): a.setShortcut(aaparam[2])
                                subm.addAction(a)
                                a.setDisabled(aaparam[3])
                    else:
                        a = QAction(aparam[0], self, triggered=aparam[1])
                        if (aparam[2] is not None): a.setShortcut(aparam[2])
                        self.popupmenu.addAction(a)
                        a.setDisabled(aparam[3])
            return True

    def _runAndSelect(self, qname, value):
        q = Q7Query.getQuery(qname)
        sl = q.run(self.FG.tree, self.FG.links, list(self.FG.lazy), False, value)
        self.model().markExtendToList(sl)
        self.model().updateSelected()
        self.treeview.refreshView()

    def patternMenu(self, menu, node):
        a = QAction("Recursive sub-pattern add", self, checkable=True)
        menu.addAction(a)
        a.setChecked(self._recursiveAddNewNode)
        menu.addSeparator()
        for t in [n[0] for n in CGU.getAuthChildren(node)]:
            def genCopyPattern(arg):
                def copyPattern():
                    self.model().copyNodeRaw(CGS.profile[arg][0])
                    if (self.getLastEntered() is not None):
                        self.model().pasteAsChild(self.getLastEntered())
                return copyPattern
            a = QAction("{}".format(t), self, triggered=genCopyPattern(t))
            menu.addAction(a)
        
    def _gm_family_1(self, node):
        self._runAndSelect('013. FamilyName reference', "'%s'" % node.sidsName())

    def _gm_family_2(self, node):
        self._runAndSelect('003. Node type', "'Family_t'")

    def _GM_Family_t(self, m, node):
        a = QAction('Select references to myself', self)
        a.triggered.connect(functools.partial(self._gm_family_1, node))
        m.addAction(a)
        a = QAction('Select all families', self)
        a.triggered.connect(functools.partial(self._gm_family_2, node))
        m.addAction(a)
        m.addSeparator()
        return True

    def _GM_IndexRange_t(self, m, node):
        if (node.sidsName() != CGK.ElementRange_s):
            v = 0
            a = QAction('Range size: %d' % (v), self)
            m.addAction(a)
        else:
            v = node.sidsValue()[1] - node.sidsValue()[0]
            etp = CGU.getEnumAsString(node.sidsParent())
            a = QAction('Number of elements of type [%s]: %d' % (etp, v), self)
            m.addAction(a)
        return True

    def _GM_Elements_t(self, m, node):
        etp = CGU.getEnumAsString(node.sidsNode())
        npe = CGK.ElementTypeNPE[etp]
        a = QAction('Element type [%s] npe [%d]' % (etp, npe), self)
        m.addAction(a)
        return True

    def marknode_t(self):
        node = self.getLastEntered()
        self._runAndSelect('003. Node type', "'%s'" % node.sidsType())

    def marknode_n(self):
        node = self.getLastEntered()
        self._runAndSelect('001. Node name', "'%s'" % node.sidsName())

    def marknode_v(self):
        node = self.getLastEntered()
        value = node.sidsValue()
        self._runAndSelect('005. Node value', value)

    def marknode_p(self):
        node = self.getLastEntered()
        node.switchMarked()
        path = node.sidsPath()
        while path is not None:
            path = CGU.getPathAncestor(path)
            if (path not in ['/', None]):
                node = self.model().nodeFromPath('/CGNSTree' + path)
                node.switchMarked()
        self.model().updateSelected()

    def setLastEntered(self, nix=None):
        if ((nix is None) or (not nix.isValid())):
            nix = self.treeview.modelCurrentIndex()
        self._lastEntered = None
        if (nix.isValid()):
            self.treeview.exclusiveSelectRow(nix, False)
            self._lastEntered = self.modelData(nix)

    def getLastEntered(self):
        return self._lastEntered

    def clearLastEntered(self):
        self._lastEntered = None
        self.treeview.selectionModel().clearSelection()
        return None

    def clearOtherSelections(self):
        if (self._control._patternwindow is not None):
            self._control._patternwindow.clearSelection()

    def clickedPressedNode(self, index):
        self.clickedNode(index)

    def clickedNode(self, index):
        self.treeview.exclusiveSelectRow(index)
        if (self.treeview.lastButton == Qt.RightButton):
            if (self.updateMenu(index)):
                self.popupmenu.popup(self.treeview.lastPos)

    def expandNode(self, *args):
        self.resizeAll()

    def collapseNode(self, *args):
        pass

    def expand_sb(self):
        self.treeview.expand_sb()
    
    def collapse_sb(self):
        self.treeview.collapse_sb()
    
    def sexpand_sb(self):
        self.treeview.sexpand_sb()
    
    def scollapse_sb(self):
        self.treeview.scollapse_sb()
    
    def resizeAll(self):
        for n in range(NMT.COLUMN_LAST + 1):
            self.treeview.resizeColumnToContents(n)

    def show(self):
        super(Q7Tree, self).show()

    def linkselectsrc(self):
        if (self.bSelectLinkSrc.isChecked()):
            if (self.getLastEntered() is None): return
            self.bAddLink.setDisabled(False)
            node = self.getLastEntered()
            self.selectForLinkSrc = (node, node.sidsPath())
        else:
            self.bAddLink.setDisabled(True)
            self.selectForLinkSrc = None

    def linkselectdst(self):
        if (self.getLastEntered() is None): return
        node = self.getLastEntered()
        if (node is None):  return
        if (node.sidsIsLink()): return
        if (node.sidsType() == CGK.CGNSTree_ts): return
        if (self._control.selectForLinkDst is not None):
            bt = self._control.selectForLinkDst[-1].bSelectLinkDst
            bt.setChecked(Qt.Unchecked)
            if (self._control.selectForLinkDst[-1] == self):
                self._control.selectForLinkDst = None
                return
        self._control.selectForLinkDst = (node, node.sidsPath(),
                                          self.FG.filedir,
                                          self.FG.filename,
                                          self)
        self.bSelectLinkDst.setChecked(Qt.Checked)
        if (self._linkwindow is not None):
            n = node.sidsPath()
            d = self.FG.filedir
            f = self.FG.filename
            self._linkwindow.updateSelected(d, f, n)

    def linkadd(self):
        if (self._control.selectForLinkDst is None): return
        dst = self._control.selectForLinkDst
        str_dst = "%s:%s" % (dst[3], dst[1])
        tpath = 'relative'
        newname = CGU.getPathLeaf(dst[1])
        if (CGU.checkDuplicatedName(self.selectForLinkSrc[0].sidsNode(),
                                    newname, dienow=False)):
            str_cnm = "New child node name is <b>%s</b>" % newname
        else:
            count = 0
            while (not CGU.checkDuplicatedName(self.selectForLinkSrc[0].sidsNode(),
                                               newname, dienow=False)):
                count += 1
                newname = '{%s#%.3d}' % (dst[0].sidsType(), count)
            str_cnm = """As a child with this name already exists, the name <b>%s</b> is used (generated name)""" % \
                      newname
        str_src = "%s:%s/%s" % (self.FG.filename,
                                self.selectForLinkSrc[1], newname)
        str_msg = "you want to create a link from <b>%s</b> to <b>%s</b><br>%s<br>Your current user options do force " \
                  "the link to use <b>%s</b> destination file path.""" % (
        str_src, str_dst, str_cnm, tpath)
        reply = MSG.wQuestion(self, 231, 'Create link as a new node', str_msg)

    def linklist(self):
        if (self._linkwindow is None):
            self._linkwindow = Q7LinkList(self._control, self.FG.index, self)
            self._linkwindow.show()
        else:
            self._linkwindow.raise_()

    def patternlist(self):
        if (self._control._patternwindow is None):
            self._control._patternwindow = Q7PatternList(self._control, self.FG)
            self._control._patternwindow.show()
        self._control._patternwindow.raise_()

    def check(self):
        self.busyCursor()
        if (self.diagview is not None):
            self.diagview.close()
            self.diagview = None
        self.lastdiag = self.model().checkSelected()
        self.readyCursor()
        self.treeview.refreshView()
        self.bCheckList.setDisabled(False)

    def checklist(self):
        if (self.lastdiag is None): return
        self.diagview = Q7CheckList(self, self.lastdiag, self.FG.index)
        self.diagview.show()

    def clearchecks(self):
        self.model().checkClear()
        self.treeview.refreshView()
        self.lastdiag = None
        self.bCheckList.setDisabled(True)

    def selectionlist(self):
        if (self._selectwindow is not None):
            self._selectwindow.close()
            self._selectwindow = None
        self._selectwindow = Q7SelectionList(self, self.model(), self.FG.index)
        self._selectwindow.show()
        self._selectwindow.raise_()

    def previousmark(self):
        self.treeview.changeSelectedMark(-1)

    def nextmark(self):
        self.treeview.changeSelectedMark(+1)

    def markall(self):
        self.model().markAll()
        self.model().updateSelected()
        self.treeview.refreshView()

    def unmarkall(self):
        self.model().unmarkAll()
        self.model().updateSelected()
        self.treeview.refreshView()

    def swapmarks(self):
        self.model().swapMarks()
        self.model().updateSelected()
        self.treeview.refreshView()

    def formview(self):
        ix = self.treeview.modelCurrentIndex()
        node = self.modelData(ix)
        if (node is None):
            MSG.wInfo(self, 254, "Form view:",
                      """You have to select a node to open its form view""",
                      again=False)
            return
        if (node.sidsType() == CGK.CGNSTree_ts): return
        form = Q7Form(self._control, node, self.FG.index)
        form.show()

    def vtkview(self):
        if (not HAS_VTK): return
        from CGNS.NAV.wvtk import Q7VTK
        if (self._vtkwindow is None):
            self.busyCursor()
            ix = self.treeview.modelCurrentIndex()
            zlist = self.model().getSelectedZones()
            node = self.modelData(ix)
            self._vtkwindow = Q7VTK(self._control, self, node, self.FG.index,
                                    self.model(), zlist)
            if (self._vtkwindow._vtkstatus):
                self._vtkwindow.show()
            else:
                self._vtkwindow.close()
                self._vtkwindow = None
            self.readyCursor()
        else:
            self._vtkwindow.raise_()

    def plotview(self):
        return

    def queryview(self):
        if (self._querywindow is None):
            self._querywindow = Q7Query(self._control, self.FG.index, self)
            self._querywindow.show()
        else:
            self._querywindow.raise_()

    def aboutSIDS(self):
        path = self.getLastEntered().sidsPath()

    def dataLoadSelected(self):
        self.model().dataLoadSelected()

    def dataReleaseSelected(self):
        self.model().dataReleaseSelected()

    def dataLoad(self):
        node = self.getLastEntered()
        self.model().dataLoadSelected(single=node.sidsPath())

    def dataRelease(self):
        node = self.getLastEntered()
        self.model().dataReleaseSelected(single=node.sidsPath())

    def forceapply(self):
        pass

    def updateTreeStatus(self):
        if ((Q7FingerPrint.STATUS_MODIFIED in self.FG._status)
            and (Q7FingerPrint.STATUS_SAVEABLE in self.FG._status)):
            self.bSave.setEnabled(True)
        else:
            self.bSave.setEnabled(False)

    def doRelease(self):
        # break cyclic refs to allow garbage
        self.treeview.itemDelegate().doRelease()
        self.treeview.setItemDelegate(None)
        self.treeview.doRelease()
        self.treeview = None
Ejemplo n.º 7
0
class Q7Main(QW, Ui_Q7ControlWindow):
    verbose = False

    def __init__(self, parent=None):
        self.lastView = None
        self.w = None
        self.fdialog = None
        import platform
        QW.control_log = MSG.Q7Log()
        QW.__init__(self, QW.VIEW_CONTROL, self, None, None)
        self.versions = {
            'pycgnsversion': 'pyCGNS v%s' % config.version,
            #                       'chloneversion':'CHLone %s'%config.CHLONE_VERSION,
            'vtkversion': 'VTK v%s' % config.VTK_VERSION,
            'cythonversion': 'Cython v%s' % config.CYTHON_VERSION,
            'hdf5version': 'HDF5 v%s' % config.HDF5_VERSION,
            'numpyversion': 'numpy v%s' % config.NUMPY_VERSION,
            'pythonversion': 'python v%s' % platform.python_version(),
            'pyqtversion': 'PyQt v%s' % config.PYQT_VERSION,
            'qtversion': 'Qt v%s' % config.QT_VERSION,
        }
        self.getHistory()
        self.bAbout.clicked.connect(self.about)
        self.bOptionView.clicked.connect(self.option)
        self.bTreeLoadLast.clicked.connect(self.loadlast)
        self.lockable(self.bTreeLoadLast)
        self.bTreeLoad.clicked.connect(self.load)
        self.bLog.clicked.connect(self.logView)
        self.lockable(self.bTreeLoad)
        self.bEditTree.clicked.connect(self.edit)
        self.lockable(self.bEditTree)
        self.bInfo.clicked.connect(self.infoControl)
        self.bPatternView.setDisabled(True)
        # self.bResetScrollBars.clicked.connect(self.resetScrolls)
        self.bClose.clicked.connect(self.close)
        # QObject.connect(self.controlTable,
        #                SIGNAL("cellClicked(int,int)"),
        #                self.clickedLine)
        self.controlTable.cellClicked.connect(self.clickedLine)
        self.initControlTable()
        self.controlTable.setItemDelegate(Q7ControlItemDelegate(self))
        self.signals = Q7SignalPool()
        self.signals.loadFile.connect(self.loadStart)
        self.signals.saveFile.connect(self.saving)
        self.signals.cancel.connect(self.cancelUnlock)
        self.signals.loadCompleted.connect(self.loadCompleted)
        self.setContextMenuPolicy(Qt.CustomContextMenu)
        self.popupmenu = QMenu()
        self.transientRecurse = False
        self.transientVTK = False
        self.copyPasteBuffer = None
        self.wOption = None
        self.selectForLinkDst = None
        self.newtreecount = 1
        self.help = None
        self._patternwindow = None
        self._toolswindow = None
        self.query = None
        Q7Query.loadUserQueries()
        Q7Query.fillQueries()
        Q7Query.loadUserFunctions()

    def clickedLine(self, *args):
        if self.controlTable.lastButton == Qt.LeftButton:
            # Q7FingerPrint.raiseView(self.getIdxFromLine(args[0]))
            pass
        if self.controlTable.lastButton == Qt.RightButton:
            self.updateMenu(self.controlTable.currentIndex())
            self.popupmenu.popup(self.controlTable.lastPos)

    def closeView(self):
        self.updateLastView()
        if self.lastView is not None:
            fg = Q7FingerPrint.getFingerPrint(self.lastView)
            fg.closeView(self.lastView)
            self.lastView = None

    def raiseView(self):
        self.updateLastView()
        if self.lastView is not None:
            Q7FingerPrint.raiseView(self.lastView)

    def logView(self):
        self.control_log.show()

    def infoControl(self):
        self.helpWindow('Control')

    def helpWindowDoc(self, doc):
        if self.help is not None:
            self.help.close()
        self.help = Q7Help(self, doc=doc)
        self.help.show()

    def helpWindow(self, key):
        if self.help is not None:
            self.help.close()
        self.help = Q7Help(self, key)
        self.help.show()

    def info(self):
        self.updateLastView()
        if self.lastView is not None:
            (f, v, d) = Q7FingerPrint.infoView(self.lastView)
            if not f.isFile():
                return
            self.w = Q7Info(self, d, f)
            self.w.show()

    def closeTree(self):
        self.updateLastView()
        if self.lastView is None:
            return
        (f, v, d) = Q7FingerPrint.infoView(self.lastView)
        reply = MSG.wQuestion(
            self, 101, 'Double check...',
            """Do you want to close the tree and all its views,<br>
                              and <b>forget unsaved</b> modifications?""")
        if reply:
            f.closeAllViews()

    def closeAllTrees(self):
        reply = MSG.wQuestion(
            self, 101, 'Double check...',
            """Do you want to close all the views,<br>
                              and <b>forget unsaved</b> modifications?""")
        if reply:
            Q7FingerPrint.closeAllTrees()

    def updateLastView(self):
        it = self.controlTable.currentItem()
        if it is None:
            self.lastView = None
        else:
            r = it.row()
            self.lastView = self.getIdxFromLine(r)
        return self.lastView

    def updateMenu(self, idx):
        lv = self.getIdxFromLine(idx.row())
        if lv is not None:
            self.lastView = lv
            actlist = (("View information (Enter)", self.info),
                       ("Raise selected view (Space)", self.raiseView), None,
                       ("Close all trees", self.closeAllTrees),
                       ("Close selected tree", self.closeTree),
                       ("Close selected view (Del)", self.closeView))
            self.popupmenu.clear()
            self.popupmenu.setTitle('Control view menu')
            for aparam in actlist:
                if aparam is None:
                    self.popupmenu.addSeparator()
                else:
                    a = QAction(aparam[0], self)
                    a.triggered.connect(aparam[1])
                    self.popupmenu.addAction(a)

    def loadOptions(self):
        if self.wOption is None:
            self.wOption = Q7Option(self)
        self.wOption.reset()

    def option(self):
        if self.wOption is None:
            self.wOption = Q7Option(self)
        self.wOption.show()

    def about(self):
        MSG.wInfo(self,
                  100,
                  "pyCGNS v%s" % OCTXT._ToolVersion,
                  OCTXT._CopyrightNotice % self.versions,
                  again=False)

    def closeApplication(self):
        reply = MSG.wQuestion(
            self, 101, 'Double check...',
            """Do you want to quit %s,<b>close all views</b>
                              and forget unsaved modifications?""" %
            OCTXT._ToolName)
        if reply == MSG.OK:
            Q7FingerPrint.closeAllTrees()
            if self.help is not None:
                self.help.close()
            if self._patternwindow is not None:
                self._patternwindow.close()
            if self.control_log is not None:
                self.control_log.close()
            if self._toolswindow is not None:
                self._toolswindow.close()
            return True
        else:
            return False

    def closeEvent(self, event):
        if self.closeApplication():
            event.accept()
        # return True
        else:
            event.ignore()
            #            return False

    def resetScrolls(self):
        self.controlTable.verticalScrollBar().setSliderPosition(0)
        self.controlTable.horizontalScrollBar().setSliderPosition(0)

    def initControlTable(self):
        ctw = self.controlTable
        ctw.control = self
        cth = ctw.horizontalHeader()
        ctw.verticalHeader().hide()
        h = ['S', 'T', 'View', 'Dir', 'File', 'Node']
        for i in range(len(h)):
            hi = QTableWidgetItem(h[i])
            hi.setFont(OCTXT._Label_Font)
            ctw.setHorizontalHeaderItem(i, hi)
            cth.setSectionResizeMode(i, QHeaderView.ResizeToContents)
        cth.setSectionResizeMode(len(h) - 1, QHeaderView.Stretch)

    def updateViews(self):
        for i in self.getAllIdx():
            f = Q7FingerPrint.getFingerPrint(i)
            v = Q7FingerPrint.getView(i)
            l = self.getLineFromIdx(i)
            self.modifiedLine(l, f._status, f)
            try:
                v.updateTreeStatus()
            except AttributeError:
                pass

    def modifiedLine(self, n, stat, fg):
        if ((Q7FingerPrint.STATUS_MODIFIED in stat)
                and (Q7FingerPrint.STATUS_SAVEABLE in stat)):
            stitem = QTableWidgetItem(self.IC(QW.I_MOD_SAV), '')
            stitem.setToolTip('Tree modified and saveable')
        if ((Q7FingerPrint.STATUS_MODIFIED in stat)
                and (Q7FingerPrint.STATUS_SAVEABLE not in stat)):
            stitem = QTableWidgetItem(self.IC(QW.I_MOD_USAV), '')
            stitem.setToolTip('Tree modified but NOT saveable')
        if ((Q7FingerPrint.STATUS_MODIFIED not in stat)
                and (Q7FingerPrint.STATUS_SAVEABLE not in stat)):
            stitem = QTableWidgetItem(self.IC(QW.I_UMOD_USAV), '')
            stitem.setToolTip('Tree unmodified and NOT saveable')
        if ((Q7FingerPrint.STATUS_MODIFIED not in stat)
                and (Q7FingerPrint.STATUS_SAVEABLE in stat)):
            stitem = QTableWidgetItem(self.IC(QW.I_UMOD_SAV), '')
            stitem.setToolTip('Tree unmodified and saveable')
        stitem.setTextAlignment(Qt.AlignCenter)
        self.controlTable.setItem(n, 0, stitem)
        self.controlTable.item(n, 3).setText(str(fg.filedir))
        self.controlTable.item(n, 4).setText(str(fg.filename))

    def addLine(self, l, fg):
        ctw = self.controlTable
        ctw.setRowCount(ctw.rowCount() + 1)
        r = ctw.rowCount() - 1
        if l[1] == QW.VIEW_TREE:
            tpitem = QTableWidgetItem(self.IC(QW.I_TREE), '')
        if l[1] == QW.VIEW_FORM:
            tpitem = QTableWidgetItem(self.IC(QW.I_FORM), '')
        if l[1] == QW.VIEW_VTK:
            tpitem = QTableWidgetItem(self.IC(QW.I_VTK), '')
        if l[1] == QW.VIEW_QUERY:
            tpitem = QTableWidgetItem(self.IC(QW.I_QUERY), '')
        if l[1] == QW.VIEW_SELECT:
            tpitem = QTableWidgetItem(self.IC(QW.I_SELECT), '')
        if l[1] == QW.VIEW_DIAG:
            tpitem = QTableWidgetItem(self.IC(QW.I_DIAG), '')
        if l[1] == QW.VIEW_TOOLS:
            tpitem = QTableWidgetItem(self.IC(QW.I_TOOLS), '')
            l = l[0:2] + [None, None, None]
        if l[1] == QW.VIEW_LINK:
            tpitem = QTableWidgetItem(self.IC(QW.I_LINK), '')
        if l[1] == QW.VIEW_DIFF:
            tpitem = QTableWidgetItem(self.IC(QW.I_DIFF), '')
        tpitem.setTextAlignment(Qt.AlignCenter)
        ctw.setItem(r, 1, tpitem)
        for i in range(len(l) - 2):
            it = QTableWidgetItem('%s ' % (l[i + 2]))
            if i in [0]:
                it.setTextAlignment(Qt.AlignCenter)
            else:
                it.setTextAlignment(Qt.AlignLeft)
            it.setFont(OCTXT._Table_Font)
            ctw.setItem(r, i + 2, it)
        self.modifiedLine(r, l[0], fg)
        ctw.setColumnWidth(0, 25)
        ctw.setColumnWidth(1, 25)
        ctw.setColumnWidth(2, 50)
        for i in range(self.controlTable.rowCount()):
            ctw.resizeRowToContents(i)

    def selectLine(self, idx):
        i = int(self.getLineFromIdx(idx))
        if i != -1:
            self.controlTable.setCurrentCell(i, 2)

    def delLine(self, idx):
        i = int(self.getLineFromIdx(idx))
        if i != -1:
            self.controlTable.removeRow(i)

    def getIdxFromLine(self, l):
        self.controlTable.setCurrentCell(l, 2)
        it = self.controlTable.currentItem()
        return it.text()

    def getLineFromIdx(self, idx):
        found = -1
        for n in range(self.controlTable.rowCount()):
            if int(idx) == int(self.controlTable.item(n, 2).text()):
                found = n
        return found

    def getAllIdx(self):
        all = []
        for n in range(self.controlTable.rowCount()):
            all.append(self.controlTable.item(n, 2).text())
        return all

    def clearOtherSelections(self):
        if self._patternwindow is not None:
            self._patternwindow.clearSelection()

    def cancelUnlock(self, *args):
        self.lockView(False)

    def loadStart(self, *args):
        self._T('loading: [%s]' % self.signals.buffer)
        self.busyCursor()
        Q7FingerPrint.treeLoad(self, self.signals.buffer)
        Q7FingerPrint.refreshScreen()

    def setDefaults(self):
        self.loadOptions()
        self._application.setStyleSheet(self.wOption._options['UserCSS'])

    def loadCompleted(self, *args, **kwargs):
        self.lockView(False)
        if 'dataset_name' in kwargs:
            filedir = kwargs['dataset_base']
            filename = kwargs['dataset_name']
            tree = kwargs['dataset_tree']
            links = kwargs['dataset_references']
            paths = kwargs['dataset_paths']
            fgprint = Q7FingerPrint(self, filedir, filename, tree, links,
                                    paths, **kwargs)
        else:
            fgprint = self.signals.fgprint
        if len(fgprint) > 1:
            code = fgprint[1][0]
            msg0 = fgprint[1][1]
            msg1 = fgprint[1][2]
            MSG.wError(self, code, msg0, msg1)
        elif fgprint.tree is None:
            MSG.wError(self, 201, 'Load error',
                       'Fatal error while loading file, empty tree')
        else:
            child = self.loadQ7Tree(fgprint)
            child.show()
            self.setHistory(fgprint.filedir, fgprint.filename)
            self.updateViews()
            fgprint.getInfo(force=True)
        self.signals.fgprint = None
        Q7FingerPrint.killProxy()
        self.readyCursor()

    def saving(self, *args):
        self._T('saving as: [%s]' % self.signals.buffer)
        self.busyCursor()
        Q7FingerPrint.treeSave(self, self.signals.fgprint, self.signals.buffer,
                               self.signals.saveAs)
        self.setHistory(self.signals.fgprint.filedir,
                        self.signals.fgprint.filename)
        self.updateViews()
        self.signals.fgprint.getInfo(force=True)
        self.readyCursor()
        self.lockView(False)

    def load(self):
        self.fdialog = Q7File(self)
        self.lockView(True)
        self.fdialog.show()

    def loadlast(self):
        if self.getLastFile() is None:
            return
        self.signals.buffer = self.getLastFile()[0] + '/' + self.getLastFile(
        )[1]
        if self.signals.buffer is None:
            self.load()
        else:
            self.signals.loadFile.emit()

    def loadfile(self, name):
        self.signals.buffer = name
        self.signals.loadFile.emit()

    def save(self, fgprint):
        self.signals.fgprint = fgprint
        self.signals.saveAs = True
        self.fdialog = Q7File(self, 1)
        self.lockView(True)
        self.fdialog.show()

    def savedirect(self, fgprint):
        self.signals.fgprint = fgprint
        self.signals.saveAs = False
        self.signals.buffer = fgprint.filedir + '/' + fgprint.filename
        self.signals.saveFile.emit()

    def edit(self):
        self._T('edit new')
        tree = CGL.newCGNSTree()
        tc = self.newtreecount
        self.busyCursor()
        fgprint = Q7FingerPrint(self, '.', 'new#%.3d.hdf' % tc, tree, [], [])
        child = self.loadQ7Tree(fgprint)
        fgprint._status = [Q7FingerPrint.STATUS_MODIFIED]
        self.readyCursor()
        self.newtreecount += 1
        child.show()

    def userFunctionFromPath(self, path, types):
        return Q7Query._userFunction

    def loadQ7Tree(self, fgprint):
        from CGNS.NAV.wtree import Q7Tree
        from CGNS.NAV.mtree import Q7TreeModel
        Q7TreeModel(fgprint.index)
        return Q7Tree(self, '/', fgprint.index)
class WorkflowWidget(QWidget):
    sigAddFunction = Signal(object)
    sigRunWorkflow = Signal()

    # TODO -- emit Workflow from sigRunWorkflow

    def __init__(self,
                 workflowview: QAbstractItemView,
                 operation_filter: Callable[[OperationPlugin], bool] = None):
        super(WorkflowWidget, self).__init__()

        self.operation_filter = operation_filter
        self.view = workflowview

        self.autorun_checkbox = QCheckBox("Run Automatically")
        self.autorun_checkbox.setCheckState(Qt.Unchecked)
        self.autorun_checkbox.stateChanged.connect(self._autorun_state_changed)
        self.run_button = QPushButton("Run Workflow")
        self.run_button.clicked.connect(self.sigRunWorkflow.emit)
        self.view.model().workflow.attach(self._autorun)
        # TODO -- actually hook up the auto run OR dependent class needs to connect (see SAXSGUIPlugin)

        self.toolbar = QToolBar()
        self.addfunctionmenu = QToolButton()
        self.addfunctionmenu.setIcon(QIcon(path("icons/addfunction.png")))
        self.addfunctionmenu.setText("Add Function")
        # Defer menu population to once the plugins have been loaded; otherwise, the menu may not contain anything
        # if this widget is init'd before all plugins have been loaded.
        self.functionmenu = QMenu()
        self.functionmenu.aboutToShow.connect(self.populateFunctionMenu)
        self.addfunctionmenu.setMenu(self.functionmenu)
        self.addfunctionmenu.setPopupMode(QToolButton.InstantPopup)
        self.toolbar.addWidget(self.addfunctionmenu)
        # self.toolbar.addAction(QIcon(path('icons/up.png')), 'Move Up')
        # self.toolbar.addAction(QIcon(path('icons/down.png')), 'Move Down')
        self.toolbar.addAction(QIcon(path("icons/folder.png")),
                               "Load Workflow")
        self.toolbar.addAction(QIcon(path("icons/trash.png")),
                               "Delete Operation", self.deleteOperation)

        v = QVBoxLayout()
        v.addWidget(self.view)
        h = QHBoxLayout()
        h.addWidget(self.autorun_checkbox)
        h.addWidget(self.run_button)
        v.addLayout(h)
        v.addWidget(self.toolbar)
        v.setContentsMargins(0, 0, 0, 0)
        self.setLayout(v)

    def _autorun_state_changed(self, state):
        if state == Qt.Checked:
            self.run_button.setDisabled(True)
        else:
            self.run_button.setDisabled(False)

    def _autorun(self):
        if self.autorun_checkbox.isChecked():
            self.sigRunWorkflow.emit()

    def populateFunctionMenu(self):
        self.functionmenu.clear()
        sortingDict = MenuDict()
        operations = pluginmanager.get_plugins_of_type("OperationPlugin")
        if self.operation_filter is not None:
            operations = filter(self.operation_filter, operations)
        for operation in operations:

            categories = operation.categories
            if not categories:
                categories = [("Uncategorized", )
                              ]  # put found operations into a default category

            for categories_tuple in categories:
                if isinstance(categories_tuple, str):
                    categories_tuple = (categories_tuple, )
                submenu = sortingDict
                categories_list = list(categories_tuple)
                while categories_list:
                    category = categories_list.pop(0)
                    submenu = submenu[category]

                submenu['___'].append(operation)

        self._mkMenu(sortingDict)

    def _mkMenu(self, sorting_dict, menu=None):
        if menu is None:
            menu = self.functionmenu
            menu.clear()

        for key in sorting_dict:
            if key == '___':
                menu.addSeparator()
                for operation in sorting_dict['___']:
                    menu.addAction(
                        operation.name,
                        partial(self.addOperation,
                                operation,
                                autoconnectall=True))
            else:
                submenu = QMenu(title=key, parent=menu)
                menu.addMenu(submenu)
                self._mkMenu(sorting_dict[key], submenu)

    def addOperation(self, operation: OperationPlugin, autoconnectall=True):
        self.view.model().workflow.add_operation(operation())
        if autoconnectall:
            self.view.model().workflow.auto_connect_all()
        print("selected new row:", self.view.model().rowCount() - 1)
        self.view.setCurrentIndex(self.view.model().index(
            self.view.model().rowCount() - 1, 0))

    def deleteOperation(self):
        index = self.view.currentIndex()
        operation = self.view.model().workflow.operations[index.row()]
        self.view.model().workflow.remove_operation(operation)
        self.view.setCurrentIndex(QModelIndex())
Ejemplo n.º 9
0
class InputsWidget(QWidget, Ui_Form):
    """There has following functions:

    + Function of mechanism variables settings.
    + Path recording.
    """
    about_to_resolve = Signal()

    def __init__(self, parent: MainWindowBase) -> None:
        super(InputsWidget, self).__init__(parent)
        self.setupUi(self)

        # parent's function pointer
        self.free_move_button = parent.free_move_button
        self.entities_point = parent.entities_point
        self.entities_link = parent.entities_link
        self.vpoints = parent.vpoint_list
        self.vlinks = parent.vlink_list
        self.main_canvas = parent.main_canvas
        self.solve = parent.solve
        self.reload_canvas = parent.reload_canvas
        self.output_to = parent.output_to
        self.conflict = parent.conflict
        self.dof = parent.dof
        self.right_input = parent.right_input
        self.command_stack = parent.command_stack
        self.set_coords_as_current = parent.set_coords_as_current
        self.get_back_position = parent.get_back_position

        # Angle panel
        self.dial = QRotatableView(self)
        self.dial.setStatusTip("Input widget of rotatable joint.")
        self.dial.setEnabled(False)
        self.dial.value_changed.connect(self.__update_var)
        self.dial_spinbox.valueChanged.connect(self.__set_var)
        self.inputs_dial_layout.addWidget(self.dial)

        # Play button
        self.variable_stop.clicked.connect(self.variable_value_reset)

        # Timer for play button
        self.inputs_play_shaft = QTimer()
        self.inputs_play_shaft.setInterval(10)
        self.inputs_play_shaft.timeout.connect(self.__change_index)

        # Change the point coordinates with current position
        self.update_pos.clicked.connect(self.set_coords_as_current)

        # Inputs record context menu
        self.pop_menu_record_list = QMenu(self)
        self.record_list.customContextMenuRequested.connect(
            self.__record_list_context_menu)
        self.__path_data: Dict[str, _Paths] = {}

    def clear(self) -> None:
        """Clear function to reset widget status."""
        self.__path_data.clear()
        for _ in range(self.record_list.count() - 1):
            self.record_list.takeItem(1)
        self.variable_list.clear()

    def __set_angle_mode(self) -> None:
        """Change to angle input."""
        self.dial.set_minimum(0)
        self.dial.set_maximum(360)
        self.dial_spinbox.setMinimum(0)
        self.dial_spinbox.setMaximum(360)

    def __set_unit_mode(self) -> None:
        """Change to unit input."""
        self.dial.set_minimum(-500)
        self.dial.set_maximum(500)
        self.dial_spinbox.setMinimum(-500)
        self.dial_spinbox.setMaximum(500)

    def path_data(self) -> Dict[str, _Paths]:
        """Return current path data."""
        return self.__path_data

    @Slot(tuple)
    def set_selection(self, selections: Sequence[int]) -> None:
        """Set one selection from canvas."""
        self.joint_list.setCurrentRow(selections[0])

    @Slot()
    def clear_selection(self) -> None:
        """Clear the points selection."""
        self.driver_list.clear()
        self.joint_list.setCurrentRow(-1)

    @Slot(int, name='on_joint_list_currentRowChanged')
    def __update_relate_points(self, _=None) -> None:
        """Change the point row from input widget."""
        self.driver_list.clear()

        item: Optional[QListWidgetItem] = self.joint_list.currentItem()
        if item is None:
            return
        p0 = _variable_int(item.text())
        base_point = self.vpoints[p0]
        type_int = base_point.type
        if type_int == VJoint.R:
            for i, vpoint in enumerate(self.vpoints):
                if i == p0:
                    continue
                if base_point.same_link(vpoint):
                    if base_point.grounded() and vpoint.grounded():
                        continue
                    self.driver_list.addItem(f"[{vpoint.type_str}] Point{i}")
        elif type_int in {VJoint.P, VJoint.RP}:
            self.driver_list.addItem(f"[{base_point.type_str}] Point{p0}")

    @Slot(int, name='on_driver_list_currentRowChanged')
    def __set_add_var_enabled(self, _=None) -> None:
        """Set enable of 'add variable' button."""
        driver = self.driver_list.currentIndex()
        self.variable_add.setEnabled(driver != -1)

    @Slot(name='on_variable_add_clicked')
    def __add_inputs_variable(self,
                              p0: Optional[int] = None,
                              p1: Optional[int] = None) -> None:
        """Add variable with '->' sign."""
        if p0 is None:
            item: Optional[QListWidgetItem] = self.joint_list.currentItem()
            if item is None:
                return
            p0 = _variable_int(item.text())
        if p1 is None:
            item = self.driver_list.currentItem()
            if item is None:
                return
            p1 = _variable_int(item.text())

        # Check DOF
        if self.dof() <= self.input_count():
            QMessageBox.warning(
                self, "Wrong DOF",
                "The number of variable must no more than degrees of freedom.")
            return

        # Check same link
        if not self.vpoints[p0].same_link(self.vpoints[p1]):
            QMessageBox.warning(
                self, "Wrong pair",
                "The base point and driver point should at the same link.")
            return

        # Check repeated pairs
        for p0_, p1_, a in self.input_pairs():
            if {p0, p1} == {p0_, p1_} and self.vpoints[p0].type == VJoint.R:
                QMessageBox.warning(self, "Wrong pair",
                                    "There already have a same pair.")
                return

        if p0 == p1:
            # One joint by offset
            value = self.vpoints[p0].true_offset()
        else:
            # Two joints by angle
            value = self.vpoints[p0].slope_angle(self.vpoints[p1])
        self.command_stack.push(
            AddInput(
                '->'.join((
                    f'Point{p0}',
                    f"Point{p1}",
                    f"{value:.02f}",
                )), self.variable_list))

    def add_inputs_variables(self, variables: Sequence[Tuple[int,
                                                             int]]) -> None:
        """Add from database."""
        for p0, p1 in variables:
            self.__add_inputs_variable(p0, p1)

    @Slot(QListWidgetItem, name='on_variable_list_itemClicked')
    def __dial_ok(self, _=None) -> None:
        """Set the angle of base link and drive link."""
        if self.inputs_play_shaft.isActive():
            return
        row = self.variable_list.currentRow()
        enabled = row > -1
        rotatable = (enabled and not self.free_move_button.isChecked()
                     and self.right_input())
        self.dial.setEnabled(rotatable)
        self.dial_spinbox.setEnabled(rotatable)
        self.oldVar = self.dial.value()
        self.variable_play.setEnabled(rotatable)
        self.variable_speed.setEnabled(rotatable)
        item: Optional[QListWidgetItem] = self.variable_list.currentItem()
        if item is None:
            return
        expr = item.text().split('->')
        p0 = int(expr[0].replace('Point', ''))
        p1 = int(expr[1].replace('Point', ''))
        value = float(expr[2])
        if p0 == p1:
            self.__set_unit_mode()
        else:
            self.__set_angle_mode()
        self.dial.set_value(value if enabled else 0)

    def variable_excluding(self, row: Optional[int] = None) -> None:
        """Remove variable if the point was been deleted. Default: all."""
        one_row: bool = row is not None
        for i, (b, d, a) in enumerate(self.input_pairs()):
            # If this is not origin point any more
            if one_row and row != b:
                continue
            self.command_stack.push(DeleteInput(i, self.variable_list))

    @Slot(name='on_variable_remove_clicked')
    def remove_var(self, row: int = -1) -> None:
        """Remove and reset angle."""
        if row == -1:
            row = self.variable_list.currentRow()
        if not row > -1:
            return
        self.variable_stop.click()
        self.command_stack.push(DeleteInput(row, self.variable_list))
        self.get_back_position()
        self.solve()

    def interval(self) -> float:
        """Return interval value."""
        return self.record_interval.value()

    def input_count(self) -> int:
        """Use to show input variable count."""
        return self.variable_list.count()

    def input_pairs(self) -> Iterator[Tuple[int, int, float]]:
        """Back as point number code."""
        for row in range(self.variable_list.count()):
            var = self.variable_list.item(row).text().split('->')
            p0 = int(var[0].replace('Point', ''))
            p1 = int(var[1].replace('Point', ''))
            angle = float(var[2])
            yield p0, p1, angle

    def variable_reload(self) -> None:
        """Auto check the points and type."""
        self.joint_list.clear()
        for i in range(self.entities_point.rowCount()):
            type_text = self.entities_point.item(i, 2).text()
            self.joint_list.addItem(f"[{type_text}] Point{i}")
        self.variable_value_reset()

    @Slot(float)
    def __set_var(self, value: float) -> None:
        self.dial.set_value(value)

    @Slot(float)
    def __update_var(self, value: float) -> None:
        """Update the value when rotating QDial."""
        item = self.variable_list.currentItem()
        self.dial_spinbox.blockSignals(True)
        self.dial_spinbox.setValue(value)
        self.dial_spinbox.blockSignals(False)
        if item:
            item_text = item.text().split('->')
            item_text[-1] = f"{value:.02f}"
            item.setText('->'.join(item_text))
            self.about_to_resolve.emit()
        if (self.record_start.isChecked()
                and abs(self.oldVar - value) > self.record_interval.value()):
            self.main_canvas.record_path()
            self.oldVar = value

    def variable_value_reset(self) -> None:
        """Reset the value of QDial."""
        if self.inputs_play_shaft.isActive():
            self.variable_play.setChecked(False)
            self.inputs_play_shaft.stop()
        self.get_back_position()
        for i, (p0, p1, a) in enumerate(self.input_pairs()):
            self.variable_list.item(i).setText('->'.join([
                f'Point{p0}',
                f'Point{p1}',
                f"{self.vpoints[p0].slope_angle(self.vpoints[p1]):.02f}",
            ]))
        self.__dial_ok()
        self.solve()

    @Slot(bool, name='on_variable_play_toggled')
    def __play(self, toggled: bool) -> None:
        """Triggered when play button was changed."""
        self.dial.setEnabled(not toggled)
        self.dial_spinbox.setEnabled(not toggled)
        if toggled:
            self.inputs_play_shaft.start()
        else:
            self.inputs_play_shaft.stop()
            if self.update_pos_option.isChecked():
                self.set_coords_as_current()

    @Slot()
    def __change_index(self) -> None:
        """QTimer change index."""
        index = self.dial.value()
        speed = self.variable_speed.value()
        extreme_rebound = (self.conflict.isVisible()
                           and self.extremeRebound.isChecked())
        if extreme_rebound:
            speed = -speed
            self.variable_speed.setValue(speed)
        index += speed * 0.06 * (3 if extreme_rebound else 1)
        self.dial.set_value(index)

    @Slot(bool, name='on_record_start_toggled')
    def __start_record(self, toggled: bool) -> None:
        """Save to file path data."""
        if toggled:
            self.main_canvas.record_start(
                int(self.dial_spinbox.maximum() /
                    self.record_interval.value()))
            return
        path = self.main_canvas.get_record_path()
        name, ok = QInputDialog.getText(self, "Recording completed!",
                                        "Please input name tag:")
        i = 0
        name = name or f"Record_{i}"
        while name in self.__path_data:
            name = f"Record_{i}"
            i += 1
        QMessageBox.information(self, "Record",
                                "The name tag is being used or empty.")
        self.add_path(name, path)

    def add_path(self, name: str, path: _Paths) -> None:
        """Add path function."""
        self.command_stack.push(
            AddPath(self.record_list, name, self.__path_data, path))
        self.record_list.setCurrentRow(self.record_list.count() - 1)

    def load_paths(self, paths: Dict[str, _Paths]) -> None:
        """Add multiple path."""
        for name, path in paths.items():
            self.add_path(name, path)

    @Slot(name='on_record_remove_clicked')
    def __remove_path(self) -> None:
        """Remove path data."""
        row = self.record_list.currentRow()
        if not row > 0:
            return
        self.command_stack.push(
            DeletePath(row, self.record_list, self.__path_data))
        self.record_list.setCurrentRow(self.record_list.count() - 1)
        self.reload_canvas()

    @Slot(QListWidgetItem, name='on_record_list_itemDoubleClicked')
    def __path_dlg(self, item: QListWidgetItem) -> None:
        """View path data."""
        name = item.text().split(":")[0]
        try:
            data = self.__path_data[name]
        except KeyError:
            return

        points_text = ", ".join(f"Point{i}" for i in range(len(data)))
        if QMessageBox.question(self, "Path data",
                                f"This path data including {points_text}.",
                                (QMessageBox.Save | QMessageBox.Close),
                                QMessageBox.Close) != QMessageBox.Save:
            return
        file_name = self.output_to(
            "path data",
            ["Comma-Separated Values (*.csv)", "Text file (*.txt)"])
        if not file_name:
            return
        with open(file_name, 'w', encoding='utf-8', newline='') as stream:
            writer = csv.writer(stream)
            for point in data:
                for coordinate in point:
                    writer.writerow(coordinate)
                writer.writerow(())
        logger.info(f"Output path data: {file_name}")

    @Slot(QPoint)
    def __record_list_context_menu(self, p: QPoint) -> None:
        """Show the context menu.

        Show path [0], [1], ...
        Or copy path coordinates.
        """
        row = self.record_list.currentRow()
        if not row > -1:
            return
        showall_action = self.pop_menu_record_list.addAction("Show all")
        showall_action.index = -1
        copy_action = self.pop_menu_record_list.addAction("Copy as new")
        name = self.record_list.item(row).text().split(':')[0]
        if name in self.__path_data:
            data = self.__path_data[name]
        else:
            # Auto preview path
            data = self.main_canvas.path_preview
        targets = 0
        for text in ("Show", "Copy data from"):
            self.pop_menu_record_list.addSeparator()
            for i, path in enumerate(data):
                if len(set(path)) > 1:
                    action = self.pop_menu_record_list.addAction(
                        f"{text} Point{i}")
                    action.index = i
                    targets += 1
        copy_action.setEnabled(targets > 0)
        action = self.pop_menu_record_list.exec_(
            self.record_list.mapToGlobal(p))
        if action is None:
            self.pop_menu_record_list.clear()
            return
        text = action.text()
        if action == copy_action:
            # Copy path data
            num = 0
            name_copy = f"{name}_{num}"
            while name_copy in self.__path_data:
                name_copy = f"{name}_{num}"
                num += 1
            self.add_path(name_copy, copy(data))
        elif text.startswith("Copy data from"):
            # Copy data to clipboard (csv)
            QApplication.clipboard().setText('\n'.join(
                f"[{x}, {y}]," for x, y in data[action.index]))
        elif text.startswith("Show"):
            # Switch points enabled status
            if action.index == -1:
                self.record_show.setChecked(True)
            self.main_canvas.set_path_show(action.index)
        self.pop_menu_record_list.clear()

    @Slot(bool, name='on_record_show_toggled')
    def __set_path_show(self, toggled: bool) -> None:
        """Show all paths or hide."""
        self.main_canvas.set_path_show(-1 if toggled else -2)

    @Slot(int, name='on_record_list_currentRowChanged')
    def __set_path(self, _=None) -> None:
        """Reload the canvas when switch the path."""
        if not self.record_show.isChecked():
            self.record_show.setChecked(True)
        self.reload_canvas()

    def current_path(self) -> _Paths:
        """Return current path data to main canvas.

        + No path.
        + Show path data.
        + Auto preview.
        """
        row = self.record_list.currentRow()
        if row in {0, -1}:
            return ()
        path_name = self.record_list.item(row).text().split(':')[0]
        return self.__path_data.get(path_name, ())

    @Slot(name='on_variable_up_clicked')
    @Slot(name='on_variable_down_clicked')
    def __set_variable_priority(self) -> None:
        row = self.variable_list.currentRow()
        if not row > -1:
            return
        item = self.variable_list.currentItem()
        self.variable_list.insertItem(
            row + (-1 if self.sender() == self.variable_up else 1),
            self.variable_list.takeItem(row))
        self.variable_list.setCurrentItem(item)
Ejemplo n.º 10
0
class BaseMainWindow(QMainWindow):
    """
    Base for main windows of subprograms

    :ivar settings: store state of application. initial value is obtained from :py:attr:`.settings_class`
    :ivar files_num: maximal number of files accepted by drag and rop event
    :param config_folder: path to directory in which application save state. If `settings` parameter is note
        then settings object is created with passing this path to :py:attr:`.settings_class`.
        If this parameter and `settings`
        are None then constructor fail with :py:exc:`ValueError`.
    :param title: Window default title
    :param settings: object to store application state
    :param signal_fun: function which need to be called when window shown.
    """

    show_signal = Signal()
    """Signal emitted when window has shown. Used to hide Launcher."""
    @classmethod
    def get_setting_class(cls) -> Type[BaseSettings]:
        """Get constructor for :py:attr:`settings`"""
        return BaseSettings

    def __init__(
        self,
        config_folder: Union[str, Path, None] = None,
        title="PartSeg",
        settings: Optional[BaseSettings] = None,
        load_dict: Optional[Register] = None,
        signal_fun=None,
    ):
        if settings is None:
            if config_folder is None:
                raise ValueError("wrong config folder")
            if not os.path.exists(config_folder):
                import_config()
            settings: BaseSettings = self.get_setting_class()(config_folder)
            errors = settings.load()
            if errors:
                errors_message = QMessageBox()
                errors_message.setText("There are errors during start")
                errors_message.setInformativeText(
                    "During load saved state some of data could not be load properly\n"
                    "The files has prepared backup copies in "
                    " state directory (Help > State directory)")
                errors_message.setStandardButtons(QMessageBox.Ok)
                text = "\n".join("File: " + x[0] + "\n" + str(x[1])
                                 for x in errors)
                errors_message.setDetailedText(text)
                errors_message.exec_()

        super().__init__()
        if signal_fun is not None:
            self.show_signal.connect(signal_fun)
        self.settings = settings
        self._load_dict = load_dict
        self.viewer_list: List[Viewer] = []
        self.files_num = 1
        self.setAcceptDrops(True)
        self.setWindowTitle(title)
        self.title_base = title
        app = QApplication.instance()
        if app is not None:
            app.setStyleSheet(settings.style_sheet)
        self.settings.theme_changed.connect(self.change_theme)
        self.channel_info = ""
        self.multiple_files = None
        self.settings.request_load_files.connect(self.read_drop)
        self.recent_file_menu = QMenu("Open recent")
        self._refresh_recent()
        self.settings.connect_(FILE_HISTORY, self._refresh_recent)
        self.settings.napari_settings.appearance.events.theme.connect(
            self.change_theme)
        self.settings.set_parent(self)
        self.console = None
        self.console_dock = QDockWidget("console", self)
        self.console_dock.setAllowedAreas(Qt.LeftDockWidgetArea
                                          | Qt.BottomDockWidgetArea)
        # self.console_dock.setWidget(self.console)
        self.console_dock.hide()
        self.addDockWidget(Qt.BottomDockWidgetArea, self.console_dock)

    def _toggle_console(self):
        if self.console is None:
            self.console = QtConsole(self)
            self.console_dock.setWidget(self.console)
        self.console_dock.setVisible(not self.console_dock.isVisible())

    def _refresh_recent(self):

        self.recent_file_menu.clear()
        for name_list, method in self.settings.get_last_files():
            action = self.recent_file_menu.addAction(
                f"{name_list[0]}, {method}")
            action.setData((name_list, method))
            action.triggered.connect(self._load_recent)

    def _load_recent(self):
        sender: QAction = self.sender()
        data = sender.data()
        try:
            method: LoadBase = self._load_dict[data[1]]
            dial = ExecuteFunctionDialog(
                method.load, [data[0]],
                exception_hook=load_data_exception_hook)
            if dial.exec_():
                result = dial.get_result()
                self.main_menu.set_data(result)
                self.settings.add_last_files(data[0], method.get_name())
                self.settings.set(OPEN_DIRECTORY, os.path.dirname(data[0][0]))
                self.settings.set(OPEN_FILE, data[0][0])
                self.settings.set(OPEN_FILE_FILTER, data[1])
        except KeyError:
            self.read_drop(data[0])

    def toggle_multiple_files(self):
        self.settings.set("multiple_files_widget",
                          not self.settings.get("multiple_files_widget"))

    def get_colormaps(self) -> List[Optional[colormap.Colormap]]:
        channel_num = self.settings.image.channels
        if not self.channel_info:
            return [None for _ in range(channel_num)]
        colormaps_name = [
            self.settings.get_channel_colormap_name(self.channel_info, i)
            for i in range(channel_num)
        ]
        return [
            self.settings.colormap_dict[name][0] for name in colormaps_name
        ]

    def napari_viewer_show(self):
        viewer = Viewer(title="Additional output",
                        settings=self.settings,
                        partseg_viewer_name=self.channel_info)
        viewer.theme = self.settings.theme_name
        viewer.create_initial_layers(image=True,
                                     roi=True,
                                     additional_layers=False,
                                     points=True)
        self.viewer_list.append(viewer)
        viewer.window.qt_viewer.destroyed.connect(
            lambda x: self.close_viewer(viewer))

    def additional_layers_show(self, with_channels=False):
        if not self.settings.additional_layers:
            QMessageBox().information(
                self, "No data",
                "Last executed algoritm does not provide additional data")
            return
        viewer = Viewer(title="Additional output",
                        settings=self.settings,
                        partseg_viewer_name=self.channel_info)
        viewer.theme = self.settings.theme_name
        viewer.create_initial_layers(image=with_channels,
                                     roi=False,
                                     additional_layers=True,
                                     points=False)
        self.viewer_list.append(viewer)
        viewer.window.qt_viewer.destroyed.connect(
            lambda x: self.close_viewer(viewer))

    def close_viewer(self, obj):
        for i, el in enumerate(self.viewer_list):
            if el == obj:
                self.viewer_list.pop(i)
                break

    # @ensure_main_thread
    def change_theme(self, event):
        style_sheet = self.settings.style_sheet
        app = QApplication.instance()
        if app is not None:
            app.setStyleSheet(style_sheet)
        self.setStyleSheet(style_sheet)

    def showEvent(self, a0: QShowEvent):
        self.show_signal.emit()

    def dragEnterEvent(self, event: QDragEnterEvent):  # pylint: disable=R0201
        if event.mimeData().hasUrls():
            event.acceptProposedAction()

    def read_drop(self, paths: List[str]):
        """Function to process loading files by drag and drop."""
        self._read_drop(paths, self._load_dict)

    def _read_drop(self, paths, load_dict):
        ext_set = {os.path.splitext(x)[1].lower() for x in paths}

        def exception_hook(exception):
            if isinstance(exception, OSError):
                QMessageBox().warning(
                    self, "IO Error",
                    "Disc operation error: " + ", ".join(exception.args),
                    QMessageBox.Ok)

        for load_class in load_dict.values():
            if load_class.partial(
            ) or load_class.number_of_files() != len(paths):
                continue
            if ext_set.issubset(load_class.get_extensions()):
                dial = ExecuteFunctionDialog(load_class.load, [paths],
                                             exception_hook=exception_hook)
                if dial.exec_():
                    result = dial.get_result()
                    self.main_menu.set_data(result)
                    self.settings.add_last_files(paths, load_class.get_name())
                return
        QMessageBox.information(
            self, "No method", "No methods for load files: " + ",".join(paths))

    def dropEvent(self, event: QDropEvent):
        """
        Support for load files by drag and drop.
        At beginning it check number of files and if it greater than :py:attr:`.files_num` it refuse loading. Otherwise
        it call :py:meth:`.read_drop` method and this method should be overwritten in sub classes
        """
        if not all(x.isLocalFile() for x in event.mimeData().urls()):
            QMessageBox().warning(
                self, "Load error",
                "Not all files are locally. Cannot load data.", QMessageBox.Ok)
        paths = [x.toLocalFile() for x in event.mimeData().urls()]
        if self.files_num != -1 and len(paths) > self.files_num:
            QMessageBox.information(
                self, "To many files",
                "currently support only drag and drop one file")
            return
        self.read_drop(paths)

    def show_settings_directory(self):
        DirectoryDialog(
            self.settings.json_folder_path,
            "Path to place where PartSeg store the data between runs").exec_()

    @staticmethod
    def show_about_dialog():
        """Show about dialog."""
        AboutDialog().exec_()

    @staticmethod
    def get_project_info(file_path, image):
        raise NotADirectoryError()

    def image_adjust_exec(self):
        dial = ImageAdjustmentDialog(self.settings.image)
        if dial.exec_():
            algorithm = dial.result_val.algorithm
            dial2 = ExecuteFunctionDialog(algorithm.transform, [], {
                "image": self.settings.image,
                "arguments": dial.result_val.values
            })
            if dial2.exec_():
                result: Image = dial2.get_result()
                self.settings.set_project_info(
                    self.get_project_info(result.file_path, result))

    def closeEvent(self, event: QCloseEvent):
        for el in self.viewer_list:
            el.close()
            del el
        self.settings.napari_settings.appearance.events.theme.disconnect(
            self.change_theme)
        self.settings.dump()
        super().closeEvent(event)

    def screenshot(self, viewer: ImageView):
        def _screenshot():
            data = viewer.viewer_widget.screenshot()
            dial = PSaveDialog(
                SaveScreenshot,
                settings=self.settings,
                system_widget=False,
                path="io.save_screenshot",
                file_mode=PSaveDialog.AnyFile,
            )

            if not dial.exec_():
                return
            res = dial.get_result()
            res.save_class.save(res.save_destination, data, res.parameters)

        return _screenshot

    def image_read(self):
        folder_name, file_name = os.path.split(self.settings.image_path)
        self.setWindowTitle(
            f"{self.title_base}: {os.path.join(os.path.basename(folder_name), file_name)}"
        )
        self.statusBar().showMessage(self.settings.image_path)

    def deleteLater(self) -> None:
        self.settings.napari_settings.appearance.events.theme.disconnect(
            self.change_theme)
        super().deleteLater()
Ejemplo n.º 11
0
class WorkflowWidget(QWidget):
    sigAddFunction = Signal(object)
    sigRunWorkflow = Signal(object)

    # TODO -- emit Workflow from sigRunWorkflow

    def __init__(self, workflowview: QAbstractItemView):
        super(WorkflowWidget, self).__init__()

        self.view = workflowview

        self.autorun_checkbox = QCheckBox("Run Automatically")
        self.autorun_checkbox.setCheckState(Qt.Unchecked)
        self.autorun_checkbox.stateChanged.connect(self._autorun_state_changed)
        self.run_button = QPushButton("Run Workflow")
        self.run_button.clicked.connect(self.sigRunWorkflow.emit)
        # TODO -- actually hook up the auto run OR dependent class needs to connect (see SAXSGUIPlugin)

        self.toolbar = QToolBar()
        self.addfunctionmenu = QToolButton()
        self.addfunctionmenu.setIcon(QIcon(path("icons/addfunction.png")))
        self.addfunctionmenu.setText("Add Function")
        # Defer menu population to once the plugins have been loaded; otherwise, the menu may not contain anything
        # if this widget is init'd before all plugins have been loaded.
        self.functionmenu = QMenu()
        self.functionmenu.aboutToShow.connect(self.populateFunctionMenu)
        self.addfunctionmenu.setMenu(self.functionmenu)
        self.addfunctionmenu.setPopupMode(QToolButton.InstantPopup)
        self.toolbar.addWidget(self.addfunctionmenu)
        # self.toolbar.addAction(QIcon(path('icons/up.png')), 'Move Up')
        # self.toolbar.addAction(QIcon(path('icons/down.png')), 'Move Down')
        self.toolbar.addAction(QIcon(path("icons/folder.png")),
                               "Load Workflow")
        self.toolbar.addAction(QIcon(path("icons/trash.png")),
                               "Delete Operation", self.deleteOperation)

        v = QVBoxLayout()
        v.addWidget(self.view)
        h = QHBoxLayout()
        h.addWidget(self.autorun_checkbox)
        h.addWidget(self.run_button)
        v.addLayout(h)
        v.addWidget(self.toolbar)
        v.setContentsMargins(0, 0, 0, 0)
        self.setLayout(v)

    def _autorun_state_changed(self, state):
        if state == Qt.Checked:
            self.run_button.setDisabled(True)
        else:
            self.run_button.setDisabled(False)

    def _run_workflow(self, _):
        self._workflow

    def populateFunctionMenu(self):
        self.functionmenu.clear()
        sortingDict = {}
        for plugin in pluginmanager.get_plugins_of_type("OperationPlugin"):
            typeOfOperationPlugin = plugin.categories
            # TODO : should OperationPlugin be responsible for initializing categories
            # to some placeholder value (instead of [])?
            if typeOfOperationPlugin == []:
                typeOfOperationPlugin = "uncategorized"  # put found operations into a default category
            if not typeOfOperationPlugin in sortingDict.keys():
                sortingDict[typeOfOperationPlugin] = []
            sortingDict[typeOfOperationPlugin].append(plugin)
        for key in sortingDict.keys():
            self.functionmenu.addSeparator()
            self.functionmenu.addAction(key)
            self.functionmenu.addSeparator()
            for plugin in sortingDict[key]:
                self.functionmenu.addAction(
                    plugin.name,
                    partial(self.addOperation, plugin, autoconnectall=True))

    def addOperation(self, operation: OperationPlugin, autoconnectall=True):
        self.view.model().workflow.add_operation(operation())
        if autoconnectall:
            self.view.model().workflow.auto_connect_all()
        print("selected new row:", self.view.model().rowCount() - 1)
        self.view.setCurrentIndex(self.view.model().index(
            self.view.model().rowCount() - 1, 0))

    def deleteOperation(self):
        for index in self.view.selectedIndexes():
            operation = self.view.model().workflow.operations[index.row()]
            self.view.model().workflow.remove_operation(operation)
Ejemplo n.º 12
0
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)
Ejemplo n.º 13
0
class MenuBar(QMenuBar):
    def __init__(self, parent, ide):

        super(MenuBar, self).__init__(parent)
        self._ide = ide
        self._cfg_actions = {}
        # File menu
        self._action_new_file = self._action(_(u'&New'), u'document-new',
                                             u'Ctrl+N', ide.new_file)
        self._action_open_file = self._action(_(u'&Open…'), u'document-open',
                                              u'Ctrl+O',
                                              ide.quick_select_files)
        self._action_open_folder = self._action(_(u'Open &folder…'), u'folder',
                                                u'Ctrl+Shift+O',
                                                ide.quick_select_folders)
        self._action_close_all_folders = self._action(_(u'&Close all folders'),
                                                      u'folder', None,
                                                      ide.close_all_folders)
        self._action_save_file = self._action(_(u'&Save'), u'document-save',
                                              u'Ctrl+S', ide.save_file)
        self._action_save_file_as = self._action(_(u'Save &as…'),
                                                 u'document-save-as',
                                                 u'Ctrl+Shift+S',
                                                 ide.save_file_as)
        self._action_quit = self._action(_(u'&Quit'), u'window-close',
                                         u'Alt+F4', ide.main_window.close)
        self._menu_file = self.addMenu(_(u'&File'))
        self._menu_file.addAction(self._action_new_file)
        self._menu_file.addAction(self._action_open_file)
        self._menu_file.addAction(self._action_save_file)
        self._menu_file.addAction(self._action_save_file_as)
        self._menu_file.addAction(self._action_open_folder)
        self._menu_file.addSeparator()
        self._menu_file.addAction(self._action_close_all_folders)
        self._menu_file.addSeparator()
        self._menu_file.addAction(self._action_quit)
        # Edit menu
        self._menu_edit = self.addMenu(_(u'&Edit'))
        self._menu_edit.aboutToShow.connect(self._show_edit_menu)
        self._menu_edit.aboutToHide.connect(self._menu_edit.clear)
        # Tools menu
        self._action_preferences = self._action(_(u'&Preferences'),
                                                u'preferences-system', None,
                                                ide.tabwidget.open_preferences)
        self._action_plugins = self._action(_(u'P&lugins'),
                                            u'preferences-system', None,
                                            ide.open_plugin_manager)
        self._menu_tools = self.addMenu(_(u'&Tools'))
        self._menu_tools.addAction(self._action_preferences)
        self._menu_tools.addAction(self._action_plugins)
        self._menu_tools.addSeparator()
        self._action_jupyter_notebook = self._add_extension_action(
            'JupyterNotebook', menu=self._menu_tools)
        self._action_git_gui = self._add_extension_action(
            'GitGUI', menu=self._menu_tools)
        self._action_symbol_selector = self._add_extension_action(
            'SymbolSelector', menu=self._menu_tools, separate=True)
        self._action_find_in_files = self._add_extension_action(
            'FindInFiles', menu=self._menu_tools, separate=False)
        self._action_command_palette = self._add_extension_action(
            'CommandPalette', menu=self._menu_tools, separate=False)
        self._action_spellcheck = self._add_extension_action(
            'SpellCheck', menu=self._menu_tools, separate=False)
        self._action_word_count = self._add_extension_action(
            'WordCount', menu=self._menu_tools, separate=False)
        # Editor menu (submenu of view)
        self._menu_editor = QMenu(_('&Editor'), self)
        self._menu_editor.setIcon(
            self._ide.theme.qicon(u'accessories-text-editor'))
        self._action_toggle_line_wrap = self._cfg_action(
            self._menu_editor, _(u'Wrap lines'), u'pyqode_line_wrap')
        self._action_toggle_whitespaces = self._cfg_action(
            self._menu_editor, _(u'Show whitespace'),
            u'pyqode_show_whitespaces')
        self._action_toggle_line_numbers = self._cfg_action(
            self._menu_editor, _(u'Show line numbers'),
            u'pyqode_show_line_numbers')
        self._action_toggle_tab_bar = self._action(
            _(u'Show editor tabs'),
            None,
            None,
            self._toggle_show_tab_bar,
            checkable=True,
            checked=cfg.opensesame_ide_show_tab_bar)
        self._action_select_indentation_mode = self._action(
            _(u'Select indentation mode'), u'accessories-text-editor', None,
            self._select_indentation_mode)
        self._action_toggle_code_folding = self._cfg_action(
            self._menu_editor, _(u'Code folding'), u'pyqode_code_folding')
        self._action_toggle_right_margin = self._cfg_action(
            self._menu_editor, _(u'Show right margin'), u'pyqode_right_margin')
        self._action_toggle_fixed_width = self._cfg_action(
            self._menu_editor, _(u'Fixed editor width'), u'pyqode_fixed_width')
        self._action_toggle_code_completion = self._cfg_action(
            self._menu_editor, _(u'Code completion'),
            u'pyqode_code_completion')
        self._menu_editor.addAction(self._action_toggle_tab_bar)
        self._menu_editor.addAction(self._action_select_indentation_mode)
        # Tabs menu (submenu of view)
        self._menu_tabs = QMenu(_('&Tabs'), self)
        self._menu_tabs.setIcon(
            self._ide.theme.qicon(u'accessories-text-editor'))
        self._action_close_tab = self._action(
            _(u'&Close tab'), u'window-close',
            cfg.opensesame_ide_shortcut_close_tab, ide.close_tab)
        self._action_close_other_tabs = self._action(
            _(u'Close &other tabs'), u'window-close',
            cfg.opensesame_ide_shortcut_close_other_tabs, ide.close_other_tabs)
        self._action_close_all_tabs = self._action(
            _(u'Close &all tabs'), u'window-close',
            cfg.opensesame_ide_shortcut_close_all_tabs, ide.close_all_tabs)
        self._action_split_vertical = self._action(
            _(u'Split &vertical'), u'go-down',
            cfg.opensesame_ide_shortcut_split_vertical, ide.split_vertical)
        self._action_split_horizontal = self._action(
            _(u'Split &horizontal'), u'go-next',
            cfg.opensesame_ide_shortcut_split_horizontal, ide.split_horizontal)
        self._action_switch_splitter_previous = self._action(
            _(u'Switch to previous panel'), u'go-previous',
            cfg.opensesame_ide_shortcut_switch_previous_panel,
            ide.switch_splitter_previous)
        self._action_switch_splitter_next = self._action(
            _(u'Switch to next panel'), u'go-next',
            cfg.opensesame_ide_shortcut_switch_next_panel,
            ide.switch_splitter_next)
        self._menu_tabs.addAction(self._action_close_tab)
        self._menu_tabs.addAction(self._action_close_other_tabs)
        self._menu_tabs.addAction(self._action_close_all_tabs)
        self._menu_tabs.addSeparator()
        self._menu_tabs.addAction(self._action_split_vertical)
        self._menu_tabs.addAction(self._action_split_horizontal)
        self._menu_tabs.addAction(self._action_switch_splitter_previous)
        self._menu_tabs.addAction(self._action_switch_splitter_next)
        # View menu
        self._action_toggle_fullscreen = self._action(
            _(u'Toggle fullscreen'),
            u'view-fullscreen',
            cfg.opensesame_ide_shortcut_toggle_fullscreen,
            ide._toggle_fullscreen,
            checkable=True)
        self._action_toggle_folder_browsers = self._action(
            _(u'Toggle &folder browsers'),
            u'os-overview',
            cfg.opensesame_ide_shortcut_toggle_folder_browsers,
            ide.toggle_folder_browsers,
            checkable=True)
        self._action_locate_file_in_folder = self._action(
            _(u'&Locate active file'), u'folder',
            cfg.opensesame_ide_shortcut_locate_active_file,
            ide.locate_file_in_folder)
        self._menu_view = self.addMenu(_('&View'))
        self._menu_view.addMenu(self._menu_editor)
        self._menu_view.addMenu(self._menu_tabs)
        self._menu_view.addSeparator()
        self._menu_view.addAction(self._action_toggle_fullscreen)
        self._menu_view.addSeparator()
        self._menu_view.addAction(self._action_toggle_folder_browsers)
        self._menu_view.addAction(self._action_locate_file_in_folder)
        self._action_toggle_console = self._add_extension_action(
            'JupyterConsole', menu=self._menu_view, separate=True)
        self._action_toggle_workspace = self._add_extension_action(
            'WorkspaceExplorer',
            menu=self._menu_view,
        )
        # Run menu
        self._menu_run = self.addMenu(_('&Run'))
        self._action_run_current_file = self._action(
            _(u'&Run project or file'),
            u'os-run',
            cfg.opensesame_ide_shortcut_run_file,
            ide.run_current_file,
        )
        self._action_run_current_selection = self._action(
            _(u'Run &selection, cell, or current line'),
            u'os-run-quick',
            cfg.opensesame_ide_shortcut_run_selection,
            ide.run_current_selection,
        )
        self._action_run_from_current_position = self._action(
            _(u'Run &from current position'),
            u'os-run-quick',
            cfg.opensesame_ide_shortcut_run_from_current_position,
            ide.run_from_current_position,
        )
        self._action_run_up_to_current_position = self._action(
            _(u'Run &up to current position'),
            u'os-run-quick',
            cfg.opensesame_ide_shortcut_run_up_to_current_position,
            ide.run_up_to_current_position,
        )
        self._action_run_debug = self._action(
            _(u'Run file in &debugger'),
            u'os-run',
            cfg.opensesame_ide_shortcut_run_debug,
            ide.run_debug,
        )
        self._action_toggle_breakpoint = self._action(
            _(u'&Toggle breakpoint'),
            u'list-add',
            cfg.opensesame_ide_shortcut_toggle_breakpoint,
            ide.toggle_breakpoint,
        )
        self._action_clear_breakpoints = self._action(
            _(u'Clear &breakpoints'),
            u'list-remove',
            cfg.opensesame_ide_shortcut_clear_breakpoints,
            ide.clear_breakpoints,
        )
        self._action_run_interrupt = self._action(
            _(u'&Interrupt kernel'),
            u'os-kill',
            cfg.opensesame_ide_shortcut_run_interrupt,
            ide.run_interrupt,
        )
        self._action_run_restart = self._action(
            _(u'Restart &kernel'),
            u'view-refresh',
            None,
            ide.run_restart,
        )
        self._action_change_working_directory = self._action(
            _(u'Change &working directory to active file'),
            u'folder-open',
            cfg.opensesame_ide_shortcut_change_working_directory,
            ide.change_working_directory,
        )
        # Output menu (submenu of run)
        self._menu_output = QMenu(_('&Capture output'), self)
        self._menu_output.setIcon(self._ide.theme.qicon(u'os-debug'))
        self._action_no_capture = self._action(
            _(u'Don\'t capture output'),
            None,
            None,
            self._image_annotations_no_capture,
            checkable=True,
            checked=not cfg.image_annotations_enabled)
        self._action_capture_images = self._action(
            _(u'Capture images'),
            None,
            None,
            self._image_annotations_capture_images,
            checkable=True,
            checked=cfg.image_annotations_enabled and \
                not cfg.image_annotations_capture_output
        )
        self._action_capture_images_and_text = self._action(
            _(u'Capture images and text'),
            None,
            None,
            self._image_annotations_capture_images_and_text,
            checkable=True,
            checked=cfg.image_annotations_capture_output)
        self._action_clear_output = self._action(
            _(u'Clear output'), u'edit-clear', None, lambda: self._ide.
            extension_manager.fire('image_annotations_clear_output'))
        self._menu_output.addAction(self._action_no_capture)
        self._menu_output.addAction(self._action_capture_images)
        self._menu_output.addAction(self._action_capture_images_and_text)
        self._menu_output.addSeparator()
        self._menu_output.addAction(self._action_clear_output)
        # Logging menu (submeny of run)
        self._menu_logging = QMenu(_(u'&Logging level'), self)
        self._menu_logging.setIcon(self._ide.theme.qicon('text-x-script'))
        self._menu_logging.aboutToShow.connect(self._show_logging_menu)
        # Run menu
        self._menu_run.addAction(self._action_run_current_file)
        self._menu_run.addAction(self._action_run_current_selection)
        self._menu_run.addAction(self._action_run_from_current_position)
        self._menu_run.addAction(self._action_run_up_to_current_position)
        self._menu_run.addSeparator()
        self._menu_run.addMenu(self._menu_output)
        self._menu_run.addMenu(self._menu_logging)
        self._menu_run.addSeparator()
        self._menu_run.addAction(self._action_run_interrupt)
        self._menu_run.addAction(self._action_run_restart)
        self._menu_run.addSeparator()
        self._menu_run.addAction(self._action_change_working_directory)
        self._menu_run.addSeparator()
        self._menu_run.addAction(self._action_run_debug)
        self._menu_run.addAction(self._action_toggle_breakpoint)
        self._menu_run.addAction(self._action_clear_breakpoints)

    def _show_edit_menu(self):

        editor = self._ide._current_editor()
        self._menu_edit.clear()
        if editor is None:
            action = self._menu_edit.addAction(_(u'No active editor'))
            action.setEnabled(False)
            return
        for action in editor.get_context_menu().actions():
            self._menu_edit.addAction(action)

    def _show_logging_menu(self):

        logging_commands = self._ide.extension_manager.provide(
            'workspace_logging_commands')
        self._menu_logging.clear()
        if logging_commands is None:
            action = self._menu_logging.addAction(_(u'Kernel not supported'))
            action.setEnabled(False)
            return
        for level, command in logging_commands.items():

            def set_logging_level(command):
                def inner():
                    self._ide.extension_manager.fire('jupyter_run_code',
                                                     code=command)

                return inner

            action = self._menu_logging.addAction(
                self._ide.theme.qicon('text-x-script'), level,
                set_logging_level(command))

    def _add_extension_action(self, ext, menu, separate=False):

        if ext not in self._ide.extension_manager:
            return None
        if separate:
            menu.addSeparator()
        menu.addAction(self._ide.extension_manager[ext].action)
        return self._ide.extension_manager[ext].action

    def build_tool_bar(self):

        tool_bar = ToolBar(self.parent(), self._ide)
        tool_bar.addAction(self._action_new_file)
        tool_bar.addAction(self._action_open_file)
        tool_bar.addAction(self._action_save_file)
        tool_bar.addAction(self._action_open_folder)
        tool_bar.addSeparator()
        tool_bar.addAction(self._action_run_current_file)
        tool_bar.addAction(self._action_run_current_selection)
        tool_bar.addAction(self._action_run_interrupt)
        tool_bar.addAction(self._action_run_restart)
        tool_bar.addSeparator()
        tool_bar.addAction(self._action_toggle_folder_browsers)
        if self._action_toggle_console is not None:
            tool_bar.addAction(self._action_toggle_console)
        if self._action_toggle_workspace is not None:
            tool_bar.addAction(self._action_toggle_workspace)
        if self._action_find_in_files is not None:
            tool_bar.addSeparator()
            tool_bar.addAction(self._action_find_in_files)
        tool_bar.setWindowTitle(u'IDE toolbar')
        tool_bar.setObjectName(u'OpenSesameIDE_Toolbar')
        return tool_bar

    def setting_changed(self, setting, value):

        if setting in self._cfg_actions:
            self._cfg_actions[setting].setChecked(value)

    def _cfg_action(self,
                    menu,
                    title,
                    setting,
                    icon=None,
                    shortcut=None,
                    negate=False):

        action = QAction(title, self)
        if icon:
            action.setIcon(self._ide.theme.qicon(icon))
        if shortcut:
            action.setShortcut(shortcut)
            action.setToolTip(u'{} ({})'.format(title.replace(u'&', u''),
                                                shortcut))

        def change_setting(value):
            self._ide.extension_manager.fire('setting_changed',
                                             setting=setting,
                                             value=value)

        def change_negated_setting(value):
            change_setting(not value)

        action.triggered.connect(
            change_negated_setting if negate else change_setting)
        action.setCheckable(True)
        action.setChecked(cfg[setting])
        action.setPriority(QAction.HighPriority)
        menu.addAction(action)
        self._cfg_actions[setting] = action
        return action

    def _action(self,
                title,
                icon,
                shortcut,
                target,
                checkable=False,
                checked=False):

        action = QAction(title, self)
        if icon:
            action.setIcon(self._ide.theme.qicon(icon))
        if shortcut:
            action.setShortcut(shortcut)
            action.setToolTip(u'{} ({})'.format(title.replace(u'&', u''),
                                                shortcut))
        action.triggered.connect(target)
        if checkable:
            action.setCheckable(True)
            action.setChecked(checked)
        action.setPriority(QAction.HighPriority)
        return action

    def _toggle_show_tab_bar(self, show_tab_bar):

        self._ide.extension_manager.fire('ide_show_tab_bar',
                                         show_tab_bar=show_tab_bar)

    def _select_indentation_mode(self):

        self._ide.extension_manager.fire('pyqode_select_indentation_mode')

    def _image_annotations_no_capture(self):

        self._action_capture_images.setChecked(False)
        self._action_capture_images_and_text.setChecked(False)
        self._ide.extension_manager.fire('setting_changed',
                                         setting='image_annotations_enabled',
                                         value=False)

    def _image_annotations_capture_images(self):
        self._action_no_capture.setChecked(False)
        self._action_capture_images_and_text.setChecked(False)
        self._ide.extension_manager.fire('setting_changed',
                                         setting='image_annotations_enabled',
                                         value=True)

    def _image_annotations_capture_images_and_text(self):
        self._action_no_capture.setChecked(False)
        self._action_capture_images.setChecked(False)
        self._ide.extension_manager.fire(
            'setting_changed',
            setting='image_annotations_capture_output',
            value=True)
Ejemplo n.º 14
0
class Q7Merge(Q7Window, Ui_Q7MergeWindow):
    def __init__(self, control, fgprint, diag):
        Q7Window.__init__(self, Q7Window.VIEW_DIFF, control, None, fgprint)
        self._depthExpanded = 0
        self._lastEntered = None
        self._fgprint = fgprint
        (ldiag, lmerge) = self.diagAnalysis(diag)
        self.treeview.expanded[QModelIndex].connect(self.expandNode)
        self.treeview.collapsed.connect(self.collapseNode)
        self.treeview.clicked[QModelIndex].connect(self.clickedNode)
        self.treeview.customContextMenuRequested[QPoint].connect(
            self.clickedNode)
        # QObject.connect(self.treeview,
        #                 SIGNAL("expanded(QModelIndex)"),
        #                 self.expandNode)
        # QObject.connect(self.treeview,
        #                 SIGNAL("collapsed()"),
        #                 self.collapseNode)
        # QObject.connect(self.treeview,
        #                 SIGNAL("clicked(QModelIndex)"),
        #                 self.clickedNode)
        # QObject.connect(self.treeview,
        #                 SIGNAL("customContextMenuRequested(QPoint)"),
        #                 self.clickedNode)
        self.bClose.clicked.connect(self.leave)
        self.bInfo.clicked.connect(self.infoTreeView)
        self.bZoomIn.clicked.connect(self.expandLevel)
        self.bZoomOut.clicked.connect(self.collapseLevel)
        self.bZoomAll.clicked.connect(self.expandMinMax)
        self.bSaveDiff.clicked.connect(self.savediff)
        self.bSelectA.clicked.connect(self.showSelected)
        self.bSelectB.clicked.connect(self.showSelected)
        self.bSelectOrderSwap.clicked.connect(self.swapSelected)
        self.setContextMenuPolicy(Qt.CustomContextMenu)
        self.popupmenu = QMenu()
        self.proxyA = self._fgprint.model
        self.treeview.setModel(self.proxyA)
        self.treeview.setItemDelegate(
            Q7MergeItemDelegate(self.treeview, self._fgprint.model, ldiag,
                                lmerge))
        self.treeview.setControlWindow(self, self._fgprint.model)
        self.treeview.hideColumn(NMT.COLUMN_FLAG_LINK)
        self.treeview.hideColumn(NMT.COLUMN_FLAG_CHECK)
        self.treeview.hideColumn(NMT.COLUMN_FLAG_SELECT)
        self._fgprint.model.addA = True
        self._fgprint.model.addB = True
        self._A = QIcon(QPixmap(":/images/icons/user-A.png"))
        self._B = QIcon(QPixmap(":/images/icons/user-B.png"))
        self._order = 0  # A first

    def swapSelected(self):
        if not self._order:
            self.bSelectA.setIcon(self._B)
            self.bSelectB.setIcon(self._A)
            self._order = 1
        else:
            self.bSelectA.setIcon(self._A)
            self.bSelectB.setIcon(self._B)
            self._order = 0

    def showSelected(self):
        self._fgprint.model.addA = False
        self._fgprint.model.addB = False
        if self.bSelectA.isChecked():
            self._fgprint.model.addA = True
        if self.bSelectB.isChecked():
            self._fgprint.model.addB = True

    def diagAnalysis(self, diag):
        ldiag = {}
        lmerge = {}
        for k in diag:
            ldiag[k] = DIFF_NX
            for d in diag[k]:
                if d[0] == 'NA':
                    ldiag[d[1]] = DIFF_NA
                    lmerge[d[1]] = MERGE_NB
                if d[0] == 'ND':
                    ldiag[d[1]] = DIFF_ND
                    lmerge[d[1]] = MERGE_NA
                if d[0] in ['CT']:
                    ldiag[k] = DIFF_CT
                    lmerge[k] = MERGE_NA
                if d[0] in ['C3', 'C1', 'C2']:
                    ldiag[k] = DIFF_CQ
                    lmerge[k] = MERGE_NA
                if d[0] in ['C6', 'C7']:
                    ldiag[k] = DIFF_CV
                    lmerge[k] = MERGE_NA
                if d[0] in ['C4', 'C5']:
                    ldiag[k] = DIFF_CS
                    lmerge[k] = MERGE_NA
        return (ldiag, lmerge)

    def model(self):
        return self._fgprint.model

    def modelIndex(self, idx):
        if not idx.isValid():
            return -1
        midx = idx
        # if (idx.model() != self.treeview.M()):
        #    midx=self.treeview.model().mapToSource(idx)
        return midx

    def modelData(self, idx):
        if not idx.isValid():
            return None
        return self.modelIndex(idx).internalPointer()

    def infoTreeView(self):
        self._control.helpWindow('Tree')

    def savediff(self):
        pass

    def expandMinMax(self):
        if self._depthExpanded == self._fgprint.depth - 2:
            self._depthExpanded = -1
            self.treeview.collapseAll()
        else:
            self._depthExpanded = self._fgprint.depth - 2
            self.treeview.expandAll()
        self.resizeAll()

    def expandLevel(self):
        if self._depthExpanded < self._fgprint.depth - 2:
            self._depthExpanded += 1
        self.treeview.expandToDepth(self._depthExpanded)
        self.resizeAll()

    def collapseLevel(self):
        if self._depthExpanded != -1:
            self._depthExpanded -= 1
        if self._depthExpanded == -1:
            self.treeview.collapseAll()
        else:
            self.treeview.expandToDepth(self._depthExpanded)
        self.resizeAll()

    def updateStatus(self, node):
        return
        self.lineEdit.clear()
        self.lineEdit.insert(node.sidsPath())

    def updateMenu(self, nodeidxs):
        return
        nodeidx = self.modelIndex(nodeidxs)
        if not nodeidx.isValid:
            return False
        if nodeidx.internalPointer() is None:
            return False
        if nodeidx.internalPointer().sidsPath() == '/CGNSTree':
            return False
        self.setLastEntered(nodeidxs)
        if nodeidx != -1:
            node = nodeidx.internalPointer()
            lknode = not node.sidsIsLink()
            lznode = node.hasLazyLoad()
            actlist = (
                ("About %s" % node.sidsType(), self.aboutSIDS, None, False),
                None,
                ("Mark/unmark node", self.marknode, 'Space', False),
                ("Add new child node", self.newnodechild, 'Ctrl+A', False),
                ("Add new brother node", self.newnodebrother, 'Ctrl+Z', False),
                None,
                ("Open form", self.popform, 'Ctrl+F', False),
                ("Open view", self.openSubTree, 'Ctrl+W', False),
                ("Open view on linked-to file", self.openLkTree, 'Ctrl+O',
                 lknode),
                None,
                ("Load node data in memory", self.dataLoad, 'Ctrl+L',
                 not lznode),
                ("Release memory node data", self.dataRelease, 'Ctrl+R',
                 lznode),
                None,
                ("Copy", self.mcopy, 'Ctrl+C', False),
                ("Cut", self.mcut, 'Ctrl+X', False),
                ("Paste as brother", self.mpasteasbrother, 'Ctrl+V', False),
                ("Paste as child", self.mpasteaschild, 'Ctrl+Y', False),
                None,
                ("Cut all selected", self.mcutselected, 'Ctrl+Shift+X', False),
                ("Paste as brother for each selected",
                 self.mpasteasbrotherselected, 'Ctrl+Shift+V', False),
                ("Paste as child for each selected",
                 self.mpasteaschildselected, 'Ctrl+Shift+Y', False),
                ("Load nodes data in memory for each selected",
                 self.dataLoadSelected, 'Ctrl+Shift+L', False),
                ("Release memory node data for each selected",
                 self.dataReleaseSelected, 'Ctrl+Shift+R', False),
            )
            self.popupmenu.clear()
            self.popupmenu.setTitle('Node menu')
            for aparam in actlist:
                if aparam is None:
                    self.popupmenu.addSeparator()
                else:
                    a = QAction(aparam[0], self)
                    a.triggered.connect(aparam[1])
                    if aparam[2] is not None:
                        a.setShortcut(aparam[2])
                    self.popupmenu.addAction(a)
                    a.setDisabled(aparam[3])
            return True

    def setLastEntered(self, nix=None):
        self._lastEntered = None

    def getLastEntered(self):
        return self._lastEntered

    def clearLastEntered(self):
        self._lastEntered = None
        self.treeview.selectionModel().clearSelection()
        return None

    def clickedNode(self, index):
        pass

    def expandNode(self, *args):
        self.resizeAll()

    def collapseNode(self, *args):
        pass

    def resizeAll(self):
        for n in range(NMT.COLUMN_LAST + 1):
            self.treeview.resizeColumnToContents(n)

    def show(self):
        super(Q7Merge, self).show()

    def closeAlone(self):
        pass

    def leave(self):
        self.close()
Ejemplo n.º 15
0
class LineEditEnvironment(LineEditBase):
    """
    Custom line edit to handle regex for naming an environment.
    """
    if WIN:
        VALID_RE = QRegExp('^[A-Za-z][A-Za-z0-9 _-]{0,30}$')
    else:
        VALID_RE = QRegExp('^[A-Za-z][A-Za-z0-9_ -]{0,30}$')

    sig_return_pressed = Signal()
    sig_escape_pressed = Signal()
    sig_copied = Signal()

    def __init__(self, *args, **kwargs):
        """Custom line edit for naming an environment."""
        super(LineEditEnvironment, 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(LineEditEnvironment, 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(LineEditEnvironment, 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 environment text to clipboard."""
        clipboard = QApplication.clipboard()
        clipboard.setText(self.text())
        self.sig_copied.emit()
Ejemplo n.º 16
0
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

        self.setStyleSheet("QTabWidget::tab-bar {" "alignment: left;}")

        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_button.setStyleSheet(STYLE_BUTTON_CSS)

        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 get_offset_pos(self, event):
        """
        Add offset to position event to capture the mouse cursor
        inside a tab.
        """
        # This is necessary because self.tabBar().tabAt(event.pos()) is not
        # returning the expected index. For further information see
        # spyder-ide/spyder#12617
        point = event.pos()
        if sys.platform == 'darwin':
            # The close button on tab is on the left
            point.setX(point.x() + 3)
        else:
            # The close buttton on tab is on the right
            point.setX(point.x() - 30)
        return self.tabBar().tabAt(point)

    def contextMenuEvent(self, event):
        """Override Qt method"""
        index = self.get_offset_pos(event)
        self.setCurrentIndex(index)
        if self.menu:
            self.menu.popup(event.globalPos())

    def mousePressEvent(self, event):
        """Override Qt method"""
        if event.button() == Qt.MidButton:
            index = self.get_offset_pos(event)
            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 tab_navigate(self, delta=1):
        """Ctrl+Tab"""
        if delta > 0 and self.currentIndex() == self.count() - 1:
            index = delta - 1
        elif delta < 0 and self.currentIndex() == 0:
            index = self.count() + delta
        else:
            index = self.currentIndex() + delta
        self.setCurrentIndex(index)

    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)
Ejemplo n.º 17
0
class Projects(SpyderPluginWidget):
    """Projects plugin."""

    CONF_SECTION = 'project_explorer'
    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

        self.editor = None
        self.workingdirectory = 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.explorer.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)

        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"""
        self.editor = self.main.editor
        self.workingdirectory = self.main.workingdirectory
        ipyconsole = self.main.ipyconsole
        treewidget = self.explorer.treewidget

        self.main.add_dockwidget(self)
        self.explorer.sig_open_file.connect(self.main.open_file)

        treewidget.sig_edit.connect(self.editor.load)
        treewidget.sig_removed.connect(self.editor.removed)
        treewidget.sig_removed_tree.connect(self.editor.removed_tree)
        treewidget.sig_renamed.connect(self.editor.renamed)
        treewidget.sig_create_module.connect(self.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.workingdirectory.chdir(v))
        self.sig_project_loaded.connect(
            lambda v: self.main.update_window_title())
        self.sig_project_loaded.connect(
            lambda v: self.editor.setup_open_files())
        self.sig_project_loaded.connect(self.update_explorer)
        self.sig_project_closed[object].connect(
            lambda v: self.workingdirectory.chdir(self.get_last_working_dir()))
        self.sig_project_closed.connect(
            lambda v: self.main.update_window_title())
        self.sig_project_closed.connect(
            lambda v: self.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.pythonpath_changed.connect(self.main.pythonpath_changed)
        self.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

    #------ 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 v, path=project:
                                           self.open_project(path=path))
                    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.widgets.projects.configdialog 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"""
        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_():
            pass
            if active_project is None:
                self.show_explorer()
            self.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`"""
        if path is None:
            basedir = get_home_dir()
            path = getexistingdirectory(parent=self,
                                        caption=_("Open project"),
                                        basedir=basedir)
            if not self.is_valid_project(path):
                if path:
                    QMessageBox.critical(
                        self, _('Error'),
                        _("<b>%s</b> is not a Spyder project!") % path)
                return
            else:
                self.add_to_recent(path)

        # A project was not open before
        if self.current_active_project is None:
            if save_previous_files:
                self.editor.save_open_files()
            self.editor.set_option('last_working_dir', getcwd_or_home())
            self.show_explorer()
        else:  # we are switching projects
            self.set_project_filenames(self.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.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:
            path = self.current_active_project.root_path
            self.set_project_filenames(self.editor.get_open_filenames())
            self.current_active_project = None
            self.set_option('current_project_path', None)
            self.setup_menu_actions()
            self.sig_project_closed.emit(path)
            self.pythonpath_changed.emit()
            self.dockwidget.close()
            self.explorer.clear()
            self.restart_consoles()

    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:
            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.editor.get_option('last_working_dir',
                                      default=getcwd_or_home())

    def save_config(self):
        """Save configuration: opened projects & tree widget state"""
        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())

    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.isHidden():
            self.dockwidget.show()
        self.dockwidget.raise_()
        self.dockwidget.update()

    def restart_consoles(self):
        """Restart consoles when closing, opening and switching projects"""
        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]
Ejemplo n.º 18
0
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()

    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)
Ejemplo n.º 19
0
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)
Ejemplo n.º 20
0
class EnvironmentsTab(WidgetBase):
    """
    This tab holds the list of named and application environments in the local
    machine.

    Available options include, `create`, `clone` and `remove` and package
    management.
    """
    BLACKLIST = ['anaconda-navigator']  # Do not show in package manager.

    sig_status_updated = Signal(object, object, object, object)

    def __init__(self, parent=None):
        super(EnvironmentsTab, self).__init__(parent)

        self.api = AnacondaAPI()
        self.last_env_prefix = None
        self.last_env_name = None
        self.previous_environments = None
        self.tracker = GATracker()
        self.metadata = {}

        active_channels = CONF.get('main',  'conda_active_channels', tuple())
        channels = CONF.get('main',  'conda_channels', tuple())
        conda_url = CONF.get('main',  'conda_url',
                             'https:/conda.anaconda.org')
        conda_api_url = CONF.get('main',  'anaconda_api_url',
                                 'https://api.anaconda.org')

        # Widgets
        self.button_clone = ButtonEnvironmentPrimary("Clone")
        self.button_create = ButtonEnvironmentPrimary("Create")
        self.button_remove = ButtonEnvironmentCancel("Remove")
        self.frame_environments = FrameEnvironments(self)
        self.frame_environments_list = FrameEnvironmentsList(self)
        self.frame_environments_list_buttons = FrameEnvironmentsListButtons(self)
        self.frame_environments_packages = FrameEnvironmentsPackages(self)
        self.list_environments = ListWidgetEnvironment()
        self.packages_widget = CondaPackagesWidget(
            self,
            setup=False,
            active_channels=active_channels,
            channels=channels,
            data_directory=CHANNELS_PATH,
            conda_api_url=conda_api_url,
            conda_url=conda_url)
        self.menu_list = QMenu()
        self.text_search = LineEditSearch()
        self.timer_environments = QTimer()

        # Widgets setup
        self.list_environments.setAttribute(Qt.WA_MacShowFocusRect, False)
        self.list_environments.setContextMenuPolicy(Qt.CustomContextMenu)
        self.packages_widget.textbox_search.setAttribute(
            Qt.WA_MacShowFocusRect, False)
        self.packages_widget.textbox_search.set_icon_visibility(False)
        self.text_search.setPlaceholderText("Search Environments")
        self.text_search.setAttribute(Qt.WA_MacShowFocusRect, False)
        self.timer_environments.setInterval(5000)

        # Layouts
        environments_layout = QVBoxLayout()
        environments_layout.addWidget(self.text_search)

        buttons_layout = QHBoxLayout()
        buttons_layout.addWidget(self.button_create)
        buttons_layout.addWidget(self.button_clone)
        buttons_layout.addWidget(self.button_remove)
        buttons_layout.setContentsMargins(0, 0, 0, 0)

        list_buttons_layout = QVBoxLayout()
        list_buttons_layout.addWidget(self.list_environments)
        list_buttons_layout.addLayout(buttons_layout)
        self.frame_environments_list_buttons.setLayout(list_buttons_layout)
        list_buttons_layout.setContentsMargins(0, 0, 0, 0)
        environments_layout.addWidget(self.frame_environments_list_buttons)

        self.frame_environments_list.setLayout(environments_layout)

        packages_layout = QHBoxLayout()
        packages_layout.addWidget(self.packages_widget)
        packages_layout.setContentsMargins(0, 0, 0, 0)
        self.frame_environments_packages.setLayout(packages_layout)

        main_layout = QHBoxLayout()
        main_layout.addWidget(self.frame_environments_list, 1)
        main_layout.addWidget(self.frame_environments_packages, 3)
        main_layout.setContentsMargins(0, 0, 0, 0)
        self.frame_environments.setLayout(main_layout)

        layout = QHBoxLayout()
        layout.addWidget(self.frame_environments)
        self.setLayout(layout)

        # Signals
        self.button_clone.clicked.connect(self.clone_environment)
        self.button_create.clicked.connect(self.create_environment)
        self.button_remove.clicked.connect(self.remove_environment)
        self.list_environments.sig_item_selected.connect(
            self.load_environment)
        self.packages_widget.sig_packages_ready.connect(self.refresh)
        self.packages_widget.sig_channels_updated.connect(self.update_channels)
#        self.packages_widget.sig_environment_cloned.connect(
#            self._environment_created)
#        self.packages_widget.sig_environment_created.connect(
#            self._environment_created)
#        self.packages_widget.sig_environment_removed.connect(
#            self._environment_removed)
        self.text_search.textChanged.connect(self.filter_environments)
        self.timer_environments.timeout.connect(self.refresh_environments)
        self.packages_widget.sig_process_cancelled.connect(
            lambda: self.update_visibility(True))

    # --- Helpers
    # -------------------------------------------------------------------------
    def update_visibility(self, enabled=True):
        self.button_create.setDisabled(not enabled)
        self.button_remove.setDisabled(not enabled)
        self.button_clone.setDisabled(not enabled)
        self.list_environments.setDisabled(not enabled)
        update_pointer()

    def update_style_sheet(self, style_sheet=None):
        if style_sheet is None:
            style_sheet = load_style_sheet()

        self.setStyleSheet(style_sheet)
        self.menu_list.setStyleSheet(style_sheet)
        self.list_environments.setFrameStyle(QFrame.NoFrame)
        self.list_environments.setFrameShape(QFrame.NoFrame)
        self.packages_widget.table.setFrameStyle(QFrame.NoFrame)
        self.packages_widget.table.setFrameShape(QFrame.NoFrame)
        self.packages_widget.layout().setContentsMargins(0, 0, 0, 0)

        size = QSize(16, 16)

        palette = {
            'icon.action.not_installed': QIcon(images.CONDA_MANAGER_NOT_INSTALLED).pixmap(size),
            'icon.action.installed': QIcon(images.CONDA_MANAGER_INSTALLED).pixmap(size),
            'icon.action.remove': QIcon(images.CONDA_MANAGER_REMOVE).pixmap(size),
            'icon.action.add': QIcon(images.CONDA_MANAGER_ADD).pixmap(size),
            'icon.action.upgrade': QIcon(images.CONDA_MANAGER_UPGRADE).pixmap(size),
            'icon.action.downgrade': QIcon(images.CONDA_MANAGER_DOWNGRADE).pixmap(size),
            'icon.upgrade.arrow': QIcon(images.CONDA_MANAGER_UPGRADE_ARROW).pixmap(size),
            'background.remove': QColor(0, 0, 0, 0),
            'background.install': QColor(0, 0, 0, 0),
            'background.upgrade': QColor(0, 0, 0, 0),
            'background.downgrade': QColor(0, 0, 0, 0),
            'foreground.not.installed': QColor("#666"),
            'foreground.upgrade': QColor("#0071a0"),
            }

        self.packages_widget.update_style_sheet(
            style_sheet=style_sheet,
            extra_dialogs={'cancel_dialog': ClosePackageManagerDialog,
                           'apply_actions_dialog': ActionsDialog,
                           'message_box_error': MessageBoxError,
                           },
            palette=palette,
            )

    def get_environments(self):
        """
        Return an ordered dictionary of all existing named environments as
        keys and the prefix as items.

        The dictionary includes the root environment as the first entry.
        """
        environments = OrderedDict()
        environments_prefix = sorted(self.api.conda_get_envs())
        environments['root'] = self.api.ROOT_PREFIX

        for prefix in environments_prefix:
            name = os.path.basename(prefix)
            environments[name] = prefix

        return environments

    def refresh_environments(self):
        """
        Check every `timer_refresh_envs` amount of miliseconds for newly
        created environments and update the list if new ones are found.
        """
        environments = self.get_environments()
        if self.previous_environments is None:
            self.previous_environments = environments.copy()

        if self.previous_environments != environments:
            self.previous_environments = environments.copy()
            self.setup_tab()

    def open_environment_in(self, which):
        environment_prefix = self.list_environments.currentItem().prefix()
        environment_name = self.list_environments.currentItem().text()
        logger.debug("%s, %s", which, environment_prefix)

        if environment_name == 'root':
            environment_prefix = None

        if which == 'terminal':
            launch.console(environment_prefix)
        else:
            launch.py_in_console(environment_prefix, which)

    def set_last_active_prefix(self):
        current_item = self.list_environments.currentItem()
        if current_item:
            self.last_env_prefix = getattr(current_item, '_prefix')
        else:
            self.last_env_prefix = self.api.ROOT_PREFIX
        CONF.set('main', 'last_active_prefix', self.last_env_prefix)

    def setup_tab(self, metadata={}, load_environment=True):
        if metadata:
            self.metadata = metadata

        # show_apps = CONF.get('main', 'show_application_environments')
        envs = self.get_environments()
        self.timer_environments.start()
        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()))

        def select(value=None, position=None):
            current_item = self.list_environments.currentItem()
            prefix = current_item.prefix()

            if isinstance(position, bool) or position is None:
                width = current_item.button_options.width()
                position = QPoint(width, 0)

#            parent_position = self.list_environments.mapToGlobal(QPoint(0, 0))
            point = QPoint(0, 0)
            parent_position = current_item.button_options.mapToGlobal(point)
            self.menu_list.move(parent_position + position)
            self.menu_list.actions()[2].setEnabled(
                launch.check_prog('ipython', prefix))
            self.menu_list.actions()[3].setEnabled(
                launch.check_prog('notebook', prefix))
            self.menu_list.exec_()

        self.set_last_active_prefix()
        self.list_environments.clear()

#        if show_apps:
#            separator_item = ListItemSeparator('My environments:')
#            self.list_environments.addItem(separator_item)

        for env in envs:
            prefix = envs[env]
            item = ListItemEnvironment(env, prefix=prefix)
            item.button_options.clicked.connect(select)
            self.list_environments.addItem(item)

#        if show_apps:
#            application_envs = self.api.get_application_environments()
#            separator_item = ListItemSeparator('Application environments:')
#            self.list_environments.addItem(separator_item)
#            for app in application_envs:
#                env_prefix = application_envs[app]
#                item = ListItemEnvironment(name=app, prefix=env_prefix)
#                item.button_options.clicked.connect(select)
#                self.list_environments.addItem(item)

        if load_environment:
            self.load_environment()
        else:
            return

        # Adjust Tab Order
        self.setTabOrder(self.text_search,
                         self.list_environments._items[0].widget)
        for i in range(len(self.list_environments._items) - 1):
            self.setTabOrder(self.list_environments._items[i].widget,
                             self.list_environments._items[i+1].widget)
        self.setTabOrder(self.list_environments._items[-1].button_name,
                         self.button_create)
        self.setTabOrder(self.button_create, self.button_clone)
        self.setTabOrder(self.button_clone, self.button_remove)
        self.setTabOrder(self.button_remove,
                         self.packages_widget.combobox_filter)
        self.setTabOrder(self.packages_widget.combobox_filter,
                         self.packages_widget.button_channels)
        self.setTabOrder(self.packages_widget.button_channels,
                         self.packages_widget.button_update)
        self.setTabOrder(self.packages_widget.button_update,
                         self.packages_widget.textbox_search)
        self.setTabOrder(self.packages_widget.textbox_search,
                         self.packages_widget.table_first_row)
        self.setTabOrder(self.packages_widget.table_last_row,
                         self.packages_widget.button_apply)
        self.setTabOrder(self.packages_widget.button_apply,
                         self.packages_widget.button_clear)
        self.setTabOrder(self.packages_widget.button_clear,
                         self.packages_widget.button_cancel)

    def filter_environments(self):
        """
        Filter displayed environments by matching search text.
        """
        text = self.text_search.text().lower()

        for i in range(self.list_environments.count()):
            item = self.list_environments.item(i)
            item.setHidden(text not in item.text().lower())

            if not item.widget.isVisible():
                item.widget.repaint()

    def load_environment(self, item=None):
        self.update_visibility(False)
        if item is None:
            item = self.list_environments.currentItem()

        if item is None or not isinstance(item, ListItemEnvironment):
            prefix = self.api.ROOT_PREFIX
            index = 0
        elif item and isinstance(item, ListItemEnvironment):
            prefix = item.prefix()
        else:
            prefix = self.last_env_prefix if self.last_env_prefix else None

        index = [i for i, it in enumerate(self.list_environments._items)
                 if prefix in it.prefix()]
        index = index[0] if len(index) else 0

        self.list_environments.setCurrentRow(index)
        self.packages_widget.set_environment(prefix=prefix)
        self.packages_widget.setup(check_updates=False,
                                   blacklist=self.BLACKLIST,
                                   metadata=self.metadata)
        self.list_environments.setDisabled(True)
        self.update_visibility(False)
        self.set_last_active_prefix()
#        update_pointer(Qt.BusyCursor)

    def refresh(self):
        self.update_visibility(True)
        self.list_environments.setDisabled(False)
        item = self.list_environments.currentItem()

        try:
            item.set_loading(False)
        except RuntimeError:
            pass
            # C/C++ object not found

        is_root = item.text() == 'root'

        self.button_remove.setDisabled(is_root)
        self.button_clone.setDisabled(is_root)

    def update_channels(self, channels, active_channels):
        """
        Save updated channels to the CONF.
        """
        CONF.set('main', 'conda_active_channels', active_channels)
        CONF.set('main', 'conda_channels', channels)

    # --- Callbacks
    # -------------------------------------------------------------------------
    def _environment_created(self, worker, output, error):
        if error:
            logger.error(str(error))

        self.update_visibility(False)
        for row, environment in enumerate(self.get_environments()):
            if worker.name == environment:
                break

        self.last_env_prefix = self.api.conda_get_prefix_envname(environment)
        self.setup_tab(load_environment=False)
        self.list_environments.setCurrentRow(row)
        self.load_environment()
        self.refresh()
        self.update_visibility(True)
        update_pointer()

    def _environment_removed(self, worker, output, error):
        self.update_visibility(True)
        if error:
            logger.error(str(error))

        self.setup_tab()
        self.list_environments.setCurrentRow(0)

    # --- Public API
    # -------------------------------------------------------------------------
    def update_domains(self, anaconda_api_url, conda_url):
        self.packages_widget.update_domains(
            anaconda_api_url=anaconda_api_url,
            conda_url=conda_url,
            )

    def create_environment(self):
        """
        Create new basic environment with selectable python version.

        Actually makes new env on disc, in directory within the project
        whose name depends on the env name. New project state is saved.
        Should also sync to spec file.
        """
        dlg = CreateEnvironmentDialog(parent=self,
                                      environments=self.get_environments())
        self.tracker.track_page('/environments/create',
                                pagetitle='Create new environment dialog')

        if dlg.exec_():
            name = dlg.text_name.text().strip()
            pyver = dlg.combo_version.currentText()

            if name:
                logger.debug(str('{0}, {1}'.format(name, pyver)))

                self.update_visibility(False)
                update_pointer(Qt.BusyCursor)

                if pyver:
                    pkgs = ['python=' + pyver, 'jupyter']
                else:
                    pkgs = ['jupyter']

                channels = self.packages_widget._active_channels
                logger.debug(str((name, pkgs, channels)))
                self.update_visibility(False)
                worker = self.packages_widget.create_environment(name=name, 
                                                                 packages=pkgs)
#                worker = self.api.conda_create(name=name, pkgs=pkgs,
#                                               channels=channels)
                worker.name = name
                worker.sig_finished.connect(self._environment_created)
        self.tracker.track_page('/environments')

    def remove_environment(self):
        """
        Clone currently selected environment.
        """
        current_item = self.list_environments.currentItem()
        if current_item is not None:
            name = current_item.text()

            if name == 'root':
                return

            dlg = RemoveEnvironmentDialog(environment=name)
            self.tracker.track_page('/environments/remove',
                                    pagetitle='Remove environment dialog')
            if dlg.exec_():
                logger.debug(str(name))
                self.update_visibility(False)
                update_pointer(Qt.BusyCursor)
                worker = self.packages_widget.remove_environment(name=name)
#                worker = self.api.conda_remove(name=name, all_=True)
                worker.sig_finished.connect(self._environment_removed)
#                self.sig_status_updated.emit('Deleting environment '
#                                             '"{0}"'.format(name),
#                                             0, -1, -1)
            self.tracker.track_page('/environments')

    def clone_environment(self):
        """
        Clone currently selected environment.
        """
        current_item = self.list_environments.currentItem()
        if current_item is not None:
            current_name = current_item.text()
            dlg = CloneEnvironmentDialog(parent=self,
                                         environments=self.get_environments())
            self.tracker.track_page('/environments/clone',
                                    pagetitle='Clone environment dialog')

            if dlg.exec_():
                name = dlg.text_name.text().strip()

                if name and current_name:
                    logger.debug(str("{0}, {1}".format(current_name, name)))

                    self.update_visibility(False)
                    update_pointer(Qt.BusyCursor)
                    worker = self.packages_widget.clone_environment(clone=current_name,
                                                                    name=name)
#                    worker = self.api.conda_clone(current_name, name=name)
                    worker.name = name
                    worker.sig_finished.connect(self._environment_created)
            self.tracker.track_page('/environments')

    def import_environment(self):
        """
Ejemplo n.º 21
0
class MainWindow(QMainWindow):
    def __init__(self):
        super(MainWindow, self).__init__()

        # Variables
        self.file_menu = None
        self.file_menu_actions = []
        self.tools_menu = None
        self.tools_menu_actions = []
        self.help_menu = None
        self.help_menu_actions = []
        self.menulist = []

        # Widgets
        self.packages = CondaPackagesWidget(self)

        # Widget setup
        self.setWindowTitle('Conda Package Manager {0}'.format(__version__))
        self.setCentralWidget(self.packages)

        self.setup_window()

    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 update_env_menu(self):
        """ """
        envs_list_actions = []
        envs = self.get_enviroments()
        self.envs_list_menu.clear()
        for env in envs:
            def trigger(value=False, e=env):
                return lambda: self.set_environments(e)
            a = create_action(self, env, triggered=trigger())
            envs_list_actions.append(a)
        add_actions(self.envs_list_menu, envs_list_actions)

    def get_enviroments(self, path=None):
        """ """
        return ['root'] + self.packages.get_environments()

    def set_environments(self, prefix):
        """ """
        self.packages.set_environment(prefix=prefix)

    def add_env(self):
        """ """
        # TODO:

    def clone_env(self):
        """ """
        # TODO:

    def remove_env(self):
        """ """
        # TODO:

    def preferences(self):
        """ """
        # TODO:

    def report_issue(self):
        if PY3:
            from urllib.parse import quote
        else:
            from urllib import quote     # analysis:ignore

        issue_template = """\
## Description

- *What steps will reproduce the problem?*
1.
2.
3.

- *What is the expected output? What do you see instead?*


- *Please provide any additional information below*


## Version and main components

- Conda Package Manager Version:  {version}
- Conda Version:  {conda version}
- Python Version:  {python version}
- Qt Version    :  {Qt version}
- QtPy Version    :  {QtPy version}
"""
        url = QUrl("https://github.com/spyder-ide/conda-manager/issues/new")
        url.addEncodedQueryItem("body", quote(issue_template))
        QDesktopServices.openUrl(url)

    def about(self):
        """About Conda Package Manager."""
        var = {'github': 'https://github.com/spyder-ide/conda-manager'}

        QMessageBox.about(self, _("About"), """
            <p><b>Conda Package Manager</b></p>

            <p>Copyright &copy; 2015 The Spyder Development Team<br>
            Licensed under the terms of the MIT License</p>

            <p>Created by Gonzalo Pe&ntilde;a-Castellanos<br>
            Developed and maintained by the Spyder Development Team.</p>

            <p>For bug reports and feature requests, please go
            to our <a href="{github}">Github website</a>.</p>

            <p>This project is part of a larger effort to promote and
            facilitate the use of Python for scientific and engineering
            software development. The popular Python distributions
            <a href="http://continuum.io/downloads">Anaconda</a>,
            <a href="https://winpython.github.io/">WinPython</a> and
            <a href="http://code.google.com/p/pythonxy/">Python(x,y)</a>
            also contribute to this plan.</p>
            """.format(**var))

    def closeEvent(self, event):
        """ """
        if self.packages.busy:
            answer = QMessageBox.question(
                self,
                'Quit Conda Manager?',
                'Conda is still busy.\n\nDo you want to quit?',
                buttons=QMessageBox.Yes | QMessageBox.No)

            if answer == QMessageBox.Yes:
                QMainWindow.closeEvent(self, event)
                # Do some cleanup?
            else:
                event.ignore()
        else:
            QMainWindow.closeEvent(self, event)
Ejemplo n.º 22
0
class PluginWidget(BasePluginWidget):
    """
    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."""
        BasePluginWidget.__init__(self, main)
        assert self.CONF_SECTION is not 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.dockwidget = None
        self.mainwindow = 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)
        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()

        # Undock action
        self.create_undock_action()
        self.plugin_actions = self.get_plugin_actions() + [MENU_SEPARATOR,
                                                           self.undock_action]

        # Options button and menu
        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 create_mainwindow(self):
        """
        Create a QMainWindow instance containing this plugin.

        Note: this method is currently not used in Spyder core plugins
        """
        self.mainwindow = mainwindow = PluginMainWindow(self)
        mainwindow.setAttribute(Qt.WA_DeleteOnClose)
        icon = self.get_plugin_icon()
        if is_text_string(icon):
            icon = self.get_icon(icon)
        mainwindow.setWindowIcon(icon)
        mainwindow.setWindowTitle(self.get_plugin_title())
        mainwindow.setCentralWidget(self)
        self.refresh_plugin()
        return mainwindow

    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 enable:
            self.dockwidget.raise_()
            widget = self.get_focus_widget()
            if widget 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.

        Use a SIGNAL to call it, e.g.:
        plugin.sig_option_changed.emit('show_all', checked)
        """
        CONF.set(self.CONF_SECTION, str(option), value)

    def get_option(self, option, default=NoDefault):
        """Get a plugin 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):
        """Clear the menu of the plugin and add the actions."""
        self.options_menu.clear()
        self.plugin_actions = self.get_plugin_actions() + [MENU_SEPARATOR,
                                                           self.undock_action]
        add_actions(self.options_menu, self.plugin_actions)
Ejemplo n.º 23
0
Archivo: apps.py Proyecto: bopopescu/QC
class ListItemApplication(ListWidgetItemBase):
    """Item with custom widget for the applications list."""

    ICON_SIZE = 64

    def __init__(
        self,
        name=None,
        display_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.display_name = display_name if display_name else 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.display_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)

        # For VScode app do not display if new updates are available
        # See: https://github.com/ContinuumIO/navigator/issues/1504
        if self.non_conda and self.name == GLOBAL_VSCODE_APP:
            self.button_version.setProperty('pressed', False)
            self.button_version.setToolTip('')

        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,
            )
Ejemplo n.º 24
0
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.server_manager = ServerManager(dark_theme)
        self.tabwidget = NotebookTabWidget(
            self, self.server_manager, 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)
        self.server_manager.shutdown_all_servers()
        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.server_info_action = create_action(
            self, _('Server info...'), icon=ima.icon('log'),
            triggered=self.view_servers)
        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, MENU_SEPARATOR,
            self.server_info_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())

    def view_servers(self):
        """Display server info."""
        dialog = ServerInfoDialog(self.server_manager.servers, parent=self)
        dialog.show()

    # ------ 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()
Ejemplo n.º 25
0
class Q7Diff(Q7Window, Ui_Q7DiffWindow):
    def __init__(self, control, fgprintindexA, fgprintindexB, diag):
        Q7Window.__init__(self, Q7Window.VIEW_DIFF, control, None,
                          fgprintindexA)
        self._depthExpanded = 0
        self._lastEntered = None
        self._fgidxA = fgprintindexA
        self._fgidxB = fgprintindexB
        ldiag = self.diagAnalysis(diag)
        self.treeviewA.expanded[QModelIndex].connect(self.expandNode)
        self.treeviewA.collapsed.connect(self.collapseNode)
        self.treeviewA.clicked[QModelIndex].connect(self.clickedNode)
        self.treeviewA.customContextMenuRequested[QPoint].connect(
            self.clickedNode)
        self.treeviewB.expanded[QModelIndex].connect(self.expandNode)
        self.treeviewB.collapsed.connect(self.collapseNode)
        self.treeviewB.clicked[QModelIndex].connect(self.clickedNode)
        self.treeviewB.customContextMenuRequested[QPoint].connect(
            self.clickedNode)
        # QObject.connect(self.treeviewA,
        #                 SIGNAL("expanded(QModelIndex)"),
        #                 self.expandNode)
        # QObject.connect(self.treeviewA,
        #                 SIGNAL("collapsed()"),
        #                 self.collapseNode)
        # QObject.connect(self.treeviewA,
        #                 SIGNAL("clicked(QModelIndex)"),
        #                 self.clickedNode)
        # QObject.connect(self.treeviewA,
        #                 SIGNAL("customContextMenuRequested(QPoint)"),
        #                 self.clickedNode)
        # QObject.connect(self.treeviewB,
        #                 SIGNAL("expanded(QModelIndex)"),
        #                 self.expandNode)
        # QObject.connect(self.treeviewB,
        #                 SIGNAL("collapsed()"),
        #                 self.collapseNode)
        # QObject.connect(self.treeviewB,
        #                 SIGNAL("clicked(QModelIndex)"),
        #                 self.clickedNode)
        # QObject.connect(self.treeviewB,
        #                 SIGNAL("customContextMenuRequested(QPoint)"),
        #                 self.clickedNode)
        # QObject.connect(self.bLockScroll,
        #                 SIGNAL("clicked()"),
        #                 self.syncScrolls)
        self.bLockScroll.clicked.connect(self.syncScrolls)
        self.bClose.clicked.connect(self.leave)
        self.bInfo.clicked.connect(self.infoTreeView)
        self.bZoomIn.clicked.connect(self.expandLevel)
        self.bZoomOut.clicked.connect(self.collapseLevel)
        self.bZoomAll.clicked.connect(self.expandMinMax)
        self.bSaveDiff.clicked.connect(self.savediff)
        self.setContextMenuPolicy(Qt.CustomContextMenu)
        self.popupmenu = QMenu()
        self.proxyA = Q7DiffTreeModel(self._fgidxA)
        self.proxyB = Q7DiffTreeModel(self._fgidxB)
        self.proxyA.setDiag(ldiag)
        self.proxyB.setDiag(ldiag)
        self.treeviewA.setModel(self.proxyA)
        self.treeviewB.setModel(self.proxyB)
        fga = Q7FingerPrint.getByIndex(self._fgidxA)
        fgb = Q7FingerPrint.getByIndex(self._fgidxB)
        model_a = fga.model
        model_b = fgb.model
        self.treeviewA.setItemDelegate(
            Q7DiffItemDelegate(self.treeviewA, model_a, ldiag))
        self.treeviewB.setItemDelegate(
            Q7DiffItemDelegate(self.treeviewB, model_b, ldiag))
        self.treeviewA.setControlWindow(self, model_a)
        self.treeviewB.setControlWindow(self, model_b)
        self.treeviewA.hideColumn(NMT.COLUMN_FLAG_LINK)
        self.treeviewA.hideColumn(NMT.COLUMN_FLAG_CHECK)
        self.treeviewA.hideColumn(NMT.COLUMN_FLAG_SELECT)
        self.treeviewA.hideColumn(NMT.COLUMN_FLAG_USER)
        self.treeviewB.hideColumn(NMT.COLUMN_FLAG_LINK)
        self.treeviewB.hideColumn(NMT.COLUMN_FLAG_CHECK)
        self.treeviewB.hideColumn(NMT.COLUMN_FLAG_SELECT)
        self.treeviewB.hideColumn(NMT.COLUMN_FLAG_USER)
        self.wvsa = self.treeviewA.verticalScrollBar()
        self.wvsb = self.treeviewB.verticalScrollBar()
        self.uvsa = self.verticalScrollBarA
        self.uvsb = self.verticalScrollBarB
        self.uvsa.setToolTip("%s/%s" % (fga.filedir, fga.filename))
        self.uvsb.setToolTip("%s/%s" % (fgb.filedir, fgb.filename))
        self.syncScrolls(True)

    def diagAnalysis(self, diag):
        ldiag = {}
        for k in diag:
            ldiag[k] = DIFF_NX
            for d in diag[k]:
                if d[0] == 'NA':
                    ldiag[d[1]] = DIFF_NA
                if d[0] == 'ND':
                    ldiag[d[1]] = DIFF_ND
                if d[0] in ['CT']:
                    ldiag[k] = DIFF_CT
                if d[0] in ['C3', 'C1', 'C2']:
                    ldiag[k] = DIFF_CQ
                if d[0] in ['C6', 'C7']:
                    ldiag[k] = DIFF_CV
                if d[0] in ['C4', 'C5']:
                    ldiag[k] = DIFF_CS
        return ldiag

    def syncScrolls(self, force=False):
        self.uvsa.valueChanged[int].connect(self.wvsa.setValue[int])
        self.uvsb.valueChanged[int].connect(self.wvsb.setValue[int])
        self.wvsa.valueChanged[int].connect(self.uvsa.setValue[int])
        self.wvsb.valueChanged[int].connect(self.uvsb.setValue[int])
        #
        # QObject.connect(self.uvsa, SIGNAL("valueChanged(int)"),
        #                 self.wvsa, SLOT("setValue(int)"))
        # QObject.connect(self.uvsb, SIGNAL("valueChanged(int)"),
        #                 self.wvsb, SLOT("setValue(int)"))
        # QObject.connect(self.wvsa, SIGNAL("valueChanged(int)"),
        #                 self.uvsa, SLOT("setValue(int)"))
        # QObject.connect(self.wvsb, SIGNAL("valueChanged(int)"),
        #                 self.uvsb, SLOT("setValue(int)"))
        if force or self.bLockScroll.isChecked():
            self.wvsa.valueChanged[int].connect(self.wvsb.setValue[int])
            self.wvsb.valueChanged[int].connect(self.wvsa.setValue[int])
            # QObject.connect(self.wvsa, SIGNAL("valueChanged(int)"),
            #                 self.wvsb, SLOT("setValue(int)"))
            # QObject.connect(self.wvsb, SIGNAL("valueChanged(int)"),
            #                 self.wvsa, SLOT("setValue(int)"))
        else:
            self.wvsa.valueChanged[int].disconnect(self.wvsb.setValue[int])
            self.wvsb.valueChanged[int].disconnect(self.wvsa.setValue[int])
            # QObject.disconnect(self.wvsa, SIGNAL("valueChanged(int)"),
            #                   self.wvsb, SLOT("setValue(int)"))
            # QObject.disconnect(self.wvsb, SIGNAL("valueChanged(int)"),
            #                   self.wvsa, SLOT("setValue(int)"))

    def model(self):
        return self._fgprint.model

    def modelIndex(self, idx):
        if not idx.isValid():
            return -1
        midx = idx
        # if (idx.model() != self.treeview.M()):
        #    midx=self.treeview.model().mapToSource(idx)
        return midx

    def modelData(self, idx):
        if not idx.isValid():
            return None
        return self.modelIndex(idx).internalPointer()

    def infoTreeView(self):
        self._control.helpWindow('Tree')

    def savediff(self):
        pass

    def expandMinMax(self):
        if self._depthExpanded == Q7FingerPrint.getByIndex(
                self._fgidxA).depth - 2:
            self._depthExpanded = -1
            self.treeviewA.collapseAll()
            self.treeviewB.collapseAll()
        else:
            self._depthExpanded = Q7FingerPrint.getByIndex(
                self._fgidxA).depth - 2
            self.treeviewA.expandAll()
            self.treeviewB.expandAll()
        self.resizeAll()

    def expandLevel(self):
        if self._depthExpanded < Q7FingerPrint.getByIndex(
                self._fgidxA).depth - 2:
            self._depthExpanded += 1
        self.treeviewA.expandToDepth(self._depthExpanded)
        self.treeviewB.expandToDepth(self._depthExpanded)
        self.resizeAll()

    def collapseLevel(self):
        if self._depthExpanded != -1:
            self._depthExpanded -= 1
        if self._depthExpanded == -1:
            self.treeviewA.collapseAll()
            self.treeviewB.collapseAll()
        else:
            self.treeviewA.expandToDepth(self._depthExpanded)
            self.treeviewB.expandToDepth(self._depthExpanded)
        self.resizeAll()

    def updateStatus(self, node):
        return
        self.lineEdit.clear()
        self.lineEdit.insert(node.sidsPath())

    def updateMenu(self, nodeidxs):
        return
        nodeidx = self.modelIndex(nodeidxs)
        if not nodeidx.isValid:
            return False
        if nodeidx.internalPointer() is None:
            return False
        if nodeidx.internalPointer().sidsPath() == '/CGNSTree':
            return False
        self.setLastEntered(nodeidxs)
        if nodeidx != -1:
            node = nodeidx.internalPointer()
            lknode = not node.sidsIsLink()
            lznode = node.hasLazyLoad()
            actlist = (
                ("About %s" % node.sidsType(), self.aboutSIDS, None, False),
                None,
                ("Mark/unmark node", self.marknode, 'Space', False),
                ("Add new child node", self.newnodechild, 'Ctrl+A', False),
                ("Add new brother node", self.newnodebrother, 'Ctrl+Z', False),
                None,
                ("Open form", self.popform, 'Ctrl+F', False),
                ("Open view", self.openSubTree, 'Ctrl+W', False),
                ("Open view on linked-to file", self.openLkTree, 'Ctrl+O',
                 lknode),
                None,
                ("Load node data in memory", self.dataLoad, 'Ctrl+L',
                 not lznode),
                ("Release memory node data", self.dataRelease, 'Ctrl+R',
                 lznode),
                None,
                ("Copy", self.mcopy, 'Ctrl+C', False),
                ("Cut", self.mcut, 'Ctrl+X', False),
                ("Paste as brother", self.mpasteasbrother, 'Ctrl+V', False),
                ("Paste as child", self.mpasteaschild, 'Ctrl+Y', False),
                None,
                ("Cut all selected", self.mcutselected, 'Ctrl+Shift+X', False),
                ("Paste as brother for each selected",
                 self.mpasteasbrotherselected, 'Ctrl+Shift+V', False),
                ("Paste as child for each selected",
                 self.mpasteaschildselected, 'Ctrl+Shift+Y', False),
                ("Load nodes data in memory for each selected",
                 self.dataLoadSelected, 'Ctrl+Shift+L', False),
                ("Release memory node data for each selected",
                 self.dataReleaseSelected, 'Ctrl+Shift+R', False),
            )
            self.popupmenu.clear()
            self.popupmenu.setTitle('Node menu')
            for aparam in actlist:
                if aparam is None:
                    self.popupmenu.addSeparator()
                else:
                    a = QAction(aparam[0], self)
                    a.triggered.connect(aparam[1])
                    if aparam[2] is not None:
                        a.setShortcut(aparam[2])
                    self.popupmenu.addAction(a)
                    a.setDisabled(aparam[3])
            return True

    def setLastEntered(self, nix=None):
        self._lastEntered = None

    def getLastEntered(self):
        return self._lastEntered

    def clearLastEntered(self):
        self._lastEntered = None
        self.treeview.selectionModel().clearSelection()
        return None

    def clickedNode(self, index):
        pass

    def expandNode(self, *args):
        self.resizeAll()

    def collapseNode(self, *args):
        pass

    def resizeAll(self):
        for n in range(NMT.COLUMN_LAST + 1):
            self.treeviewA.resizeColumnToContents(n)
            self.treeviewB.resizeColumnToContents(n)

    def show(self):
        super(Q7Diff, self).show()

    def closeAlone(self):
        pass

    def leave(self):
        self.close()

    def doRelease(self):
        pass
Ejemplo n.º 26
0
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()
Ejemplo n.º 27
0
class PyDMLineEdit(QLineEdit, TextFormatter, PyDMWritableWidget, DisplayFormat):
    Q_ENUMS(DisplayFormat)
    DisplayFormat = DisplayFormat
    """
    A QLineEdit (writable text field) with support for Channels and more
    from PyDM.
    This widget offers an unit conversion menu when users Right Click
    into it.

    Parameters
    ----------
    parent : QWidget
        The parent widget for the Label
    init_channel : str, optional
        The channel to be used by the widget.
    """

    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()

    @Property(DisplayFormat)
    def displayFormat(self):
        return self._display_format_type

    @displayFormat.setter
    def displayFormat(self, new_type):
        if self._display_format_type != new_type:
            self._display_format_type = new_type
            # Trigger the update of display format
            self.value_changed(self.value)

    def value_changed(self, new_val):
        """
        Receive and update the PyDMLineEdit for a new channel value

        The actual value of the input is saved as well as the type received.
        This also resets the PyDMLineEdit display text using
        :meth:`.set_display`

        Parameters
        ----------
        value: str, float or int
            The new value of the channel
        """
        super(PyDMLineEdit, self).value_changed(new_val)
        self.set_display()

    def send_value(self):
        """
        Emit a :attr:`send_value_signal` to update channel value.

        The text is cleaned of all units, user-formatting and scale values
        before being sent back to the channel. This function is attached the
        ReturnPressed signal of the PyDMLineEdit
        """
        send_value = str(self.text())
        # Clean text of unit string
        if self._show_units and self._unit and self._unit in send_value:
            send_value = send_value[:-len(self._unit)].strip()
        try:
            if self.channeltype not in [str, np.ndarray]:
                scale = self._scale
                if scale is None or scale == 0:
                    scale = 1.0

                if self._display_format_type in [DisplayFormat.Default, DisplayFormat.String]:
                    if self.channeltype == float:
                        num_value = locale.atof(send_value)
                    else:
                        num_value = self.channeltype(send_value)
                    scale = self.channeltype(scale)
                elif self._display_format_type == DisplayFormat.Hex:
                    num_value = int(send_value, 16)
                elif self._display_format_type == DisplayFormat.Binary:
                    num_value = int(send_value, 2)
                elif self._display_format_type in [DisplayFormat.Exponential, DisplayFormat.Decimal]:
                    num_value = locale.atof(send_value)

                num_value = num_value / scale
                self.send_value_signal[self.channeltype].emit(num_value)
            elif self.channeltype == np.ndarray:
                # Arrays will be in the [1.2 3.4 22.214] format
                if self._display_format_type == DisplayFormat.String:
                    self.send_value_signal[str].emit(send_value)
                else:
                    arr_value = list(filter(None, send_value.replace("[", "").replace("]", "").split(" ")))
                    arr_value = np.array(arr_value, dtype=self.subtype)
                    self.send_value_signal[np.ndarray].emit(arr_value)
            else:
                # Channel Type is String
                # Lets just send what we have after all
                self.send_value_signal[str].emit(send_value)
        except ValueError:
            logger.exception("Error trying to set data '{0}' with type '{1}' and format '{2}' at widget '{3}'."
                         .format(self.text(), self.channeltype, self._display_format_type, self.objectName()))

        self.clearFocus()
        self.set_display()

    def write_access_changed(self, new_write_access):
        """
        Change the PyDMLineEdit to read only if write access is denied
        """
        super(PyDMLineEdit, self).write_access_changed(new_write_access)
        self.setReadOnly(not new_write_access)

    def unit_changed(self, new_unit):
        """
        Accept a unit to display with a channel's value

        The unit may or may not be displayed based on the :attr:`showUnits`
        attribute. Receiving a new value for the unit causes the display to
        reset.
        """
        super(PyDMLineEdit, self).unit_changed(new_unit)
        self._scale = 1
        self.create_unit_options()

    def create_unit_options(self):
        """
        Create the menu for displaying possible unit values

        The menu is filled with possible unit conversions based on the
        current PyDMLineEdit. If either the unit is not found in the by
        the :func:`utilities.find_unit_options` function, or, the
        :attr:`.showUnits` attribute is set to False, the menu will tell
        the user that there are no available conversions
        """
        self.unitMenu.clear()
        units = utilities.find_unit_options(self._unit)
        if units and self._show_units:
            for choice in units:
                self.unitMenu.addAction(choice,
                                        partial(
                                            self.apply_conversion,
                                            choice
                                            )
                                        )
        else:
            self.unitMenu.addAction('No Unit Conversions found')

    def apply_conversion(self, unit):
        """
        Convert the current unit to a different one

        This function will attempt to find a scalar to convert the current
        unit type to the desired one and reset the display with the new
        conversion.

        Parameters
        ----------
        unit : str
            String name of desired units
        """
        if not self._unit:
            logger.warning("Warning: Attempting to convert PyDMLineEdit unit, but no initial units supplied.")
            return None

        scale = utilities.convert(str(self._unit), unit)
        if scale:
            self._scale = scale * float(self._scale)
            self._unit = unit
            self.update_format_string()
            self.clearFocus()
            self.set_display()
        else:
            logging.warning("Warning: Attempting to convert PyDMLineEdit unit, but '{0}' can not be converted to '{1}'."
                            .format(self._unit, unit))

    def widget_ctx_menu(self):
        """
        Fetch the Widget specific context menu which will be populated with additional tools by `assemble_tools_menu`.

        Returns
        -------
        QMenu or None
            If the return of this method is None a new QMenu will be created by `assemble_tools_menu`.
        """
        menu = self.createStandardContextMenu()
        menu.addSeparator()
        menu.addMenu(self.unitMenu)
        return menu

    def set_display(self):
        """
        Set the text display of the PyDMLineEdit.

        The original value given by the PV is converted to a text entry based
        on the current settings for scale value, precision, a user-defined
        format, and the current units. If the user is currently entering a
        value in the PyDMLineEdit the text will not be changed.
        """
        if self.value is None:
            return

        if self.hasFocus():
            return

        new_value = self.value

        if self._display_format_type in [DisplayFormat.Default,
                                         DisplayFormat.Decimal,
                                         DisplayFormat.Exponential,
                                         DisplayFormat.Hex,
                                         DisplayFormat.Binary]:
            if not isinstance(new_value, (str, np.ndarray)):
                try:
                    new_value *= self.channeltype(self._scale)
                except TypeError:
                    logger.error("Cannot convert the value '{0}', for channel '{1}', to type '{2}'. ".format(
                        self._scale, self._channel, self.channeltype))

        new_value = parse_value_for_display(value=new_value,  precision=self._prec,
                                            display_format_type=self._display_format_type,
                                            string_encoding=self._string_encoding,
                                            widget=self)

        self._display = str(new_value)

        if self._display_format_type == DisplayFormat.Default:
            if isinstance(new_value, (int, float)):
                self._display = str(self.format_string.format(new_value))
                self.setText(self._display)
                return

        if self._show_units:
            self._display += " {}".format(self._unit)

        self.setText(self._display)

    def focusOutEvent(self, event):
        """
        Overwrites the function called when a user leaves a PyDMLineEdit
        without pressing return.  Resets the value of the text field to the
        current channel value.
        """
        if self._display is not None:
            self.setText(self._display)
        super(PyDMLineEdit, self).focusOutEvent(event)
Ejemplo n.º 28
0
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)
        self.fsmodel.modelReset.connect(self.reset_icon_provider)
        self.reset_icon_provider()
        
    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, 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)

    def go_to_parent_directory(self):
        pass
        
    #----- 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)
Ejemplo n.º 29
0
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

        # 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)

    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
        if self.shortcut is not None:
            sc = QShortcut(QKeySequence(self.shortcut), self.main,
                           self.switch_to_plugin)
            self.register_shortcut(sc, "_", "Switch to %s" % 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"""
        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)

    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)
Ejemplo n.º 30
0
class WorkflowWidget(QWidget):
    sigAddFunction = Signal(object)
    sigRunWorkflow = Signal()

    # TODO -- emit Workflow from sigRunWorkflow

    def __init__(self,
                 workflowview: QAbstractItemView,
                 operation_filter: Callable[[OperationPlugin], bool] = None,
                 workflows: Dict[Workflow, str] = None):
        super(WorkflowWidget, self).__init__()

        self.operation_filter = operation_filter
        self.view = workflowview

        self.autorun_checkbox = QCheckBox("Run Automatically")
        self.autorun_checkbox.setCheckState(Qt.Unchecked)
        self.autorun_checkbox.stateChanged.connect(self._autorun_state_changed)
        self.run_button = QPushButton("Run Workflow")
        self.run_button.clicked.connect(self.sigRunWorkflow.emit)
        self.view.model().workflow.attach(self._autorun)
        # TODO -- actually hook up the auto run OR dependent class needs to connect (see SAXSGUIPlugin)

        self.toolbar = QToolBar()
        self.addfunctionmenu = QToolButton()
        self.addfunctionmenu.setIcon(QIcon(path("icons/addfunction.png")))
        self.addfunctionmenu.setText("Add Function")
        self.addfunctionmenu.setToolTip("Add Operation")
        self.addfunctionmenu.setWhatsThis(
            "This button can be used to add a new operation to the end of a workflow. "
            "A menu to select operations will be populated based on the installed "
            "operations' categories.")
        # Defer menu population to once the plugins have been loaded; otherwise, the menu may not contain anything
        # if this widget is init'd before all plugins have been loaded.
        self.functionmenu = QMenu()
        self.functionmenu.aboutToShow.connect(self.populateFunctionMenu)
        self.addfunctionmenu.setMenu(self.functionmenu)
        self.addfunctionmenu.setPopupMode(QToolButton.InstantPopup)

        self.workflows = WorkflowDict(workflows or {})

        self.workflow_menu = QMenu()
        self.workflow_menu.aboutToShow.connect(self.populateWorkflowMenu)
        self.workflow_selector = QToolButton()
        self.workflow_selector.setIcon(QIcon(path("icons/bookshelf.png")))
        self.workflow_selector.setText("Select Workflow")
        self.workflow_selector.setToolTip("Workflow Library")
        self.workflow_selector.setWhatsThis(
            "This button allows switching between any stored workflows. "
            "(Stored workflows are typically defined programmatically "
            "in a GUI Plugin's modules.)")
        self.workflow_selector.setMenu(self.workflow_menu)
        self.workflow_selector.setPopupMode(QToolButton.InstantPopup)
        self.toolbar.addWidget(self.workflow_selector)

        self.toolbar.addWidget(self.addfunctionmenu)
        # self.toolbar.addAction(QIcon(path('icons/up.png')), 'Move Up')
        # self.toolbar.addAction(QIcon(path('icons/down.png')), 'Move Down')
        action = self.toolbar.addAction(QIcon(path("icons/save.png")),
                                        "Export Workflow")
        action.setEnabled(False)  # FIXME: implement export workflow feature
        action = self.toolbar.addAction(QIcon(path("icons/folder.png")),
                                        "Import Workflow")
        action.setEnabled(False)  # FIXME: implement import workflow feature

        action = self.toolbar.addAction(QIcon(path("icons/trash.png")),
                                        "Delete Operation",
                                        self.deleteOperation)
        action.setWhatsThis("This button removes the currently selected operation from the workflow. "\
                            "(The currently selected operation is highlighted. "\
                            "An operation is selected when its text is clicked in the workflow editor.")

        v = QVBoxLayout()
        v.addWidget(self.view)
        h = QHBoxLayout()
        h.addWidget(self.autorun_checkbox)
        h.addWidget(self.run_button)
        v.addLayout(h)
        v.addWidget(self.toolbar)
        v.setContentsMargins(0, 0, 0, 0)
        self.setLayout(v)

    def _autorun_state_changed(self, state):
        if state == Qt.Checked:
            self.run_button.setDisabled(True)
        else:
            self.run_button.setDisabled(False)

    def _autorun(self):
        if self.autorun_checkbox.isChecked():
            self.sigRunWorkflow.emit()

    def populateFunctionMenu(self):
        self.functionmenu.clear()
        sortingDict = MenuDict()
        operations = pluginmanager.get_plugins_of_type("OperationPlugin")
        if self.operation_filter is not None:
            operations = filter(self.operation_filter, operations)
        for operation in operations:

            categories = operation.categories
            if not categories:
                categories = [("Uncategorized", )
                              ]  # put found operations into a default category

            for categories_tuple in categories:
                if isinstance(categories_tuple, str):
                    categories_tuple = (categories_tuple, )
                submenu = sortingDict
                categories_list = list(categories_tuple)
                while categories_list:
                    category = categories_list.pop(0)
                    submenu = submenu[category]

                submenu['___'].append(operation)

        self._mkMenu(sortingDict)

    def populateWorkflowMenu(self):
        self.workflow_menu.clear()
        for workflow, workflow_name in self.workflows.items():
            self.workflow_menu.addAction(workflow_name,
                                         partial(self.setWorkflow, workflow))

    def _mkMenu(self, sorting_dict, menu=None):
        if menu is None:
            menu = self.functionmenu
            menu.clear()

        for key in sorting_dict:
            if key == '___':
                menu.addSeparator()
                for operation in sorting_dict['___']:
                    menu.addAction(
                        operation.name,
                        partial(self.addOperation,
                                operation,
                                autoconnectall=True))
            else:
                submenu = QMenu(title=key, parent=menu)
                menu.addMenu(submenu)
                self._mkMenu(sorting_dict[key], submenu)

    def setWorkflow(self, workflow: Workflow):
        self.view.model().workflow = workflow

    def addWorkflow(self, workflow: Workflow, name: str = None):
        if name is None:
            name = workflow.name
        if name in self.workflows:
            raise ValueError(
                f'A workflow already exists in this editor with the name "{name}"'
            )
        self.workflows[name] = workflow

    def removeWorkflow(self, workflow):
        for name, match_workflow in self.workflows.items():
            if workflow == match_workflow:
                del self.workflows[name]

    def addOperation(self, operation: OperationPlugin, autoconnectall=True):
        self.view.model().workflow.add_operation(operation())
        if autoconnectall:
            self.view.model().workflow.auto_connect_all()
        print("selected new row:", self.view.model().rowCount() - 1)
        self.view.setCurrentIndex(self.view.model().index(
            self.view.model().rowCount() - 1, 0))

    def deleteOperation(self):
        index = self.view.currentIndex()
        operation = self.view.model().workflow.operations[index.row()]
        self.view.model().workflow.remove_operation(operation)
        self.view.setCurrentIndex(QModelIndex())
Ejemplo n.º 31
0
class Projects(SpyderPluginWidget):
    """Projects plugin."""

    CONF_SECTION = 'project_explorer'
    CONF_FILE = False

    # This is required for the new API
    NAME = 'project_explorer'
    REQUIRES = []
    OPTIONAL = [Plugins.Completions, Plugins.IPythonConsole, Plugins.Explorer]

    # 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_requested.connect(self.main.open_file)

        treewidget.sig_delete_project.connect(self.delete_project)
        treewidget.sig_open_file_requested.connect(self.main.editor.load)
        treewidget.sig_removed.connect(self.main.editor.removed)
        treewidget.sig_tree_removed.connect(self.main.editor.removed_tree)
        treewidget.sig_renamed.connect(self.main.editor.renamed)
        treewidget.sig_tree_renamed.connect(self.main.editor.renamed_tree)
        treewidget.sig_module_created.connect(self.main.editor.new)
        treewidget.sig_file_created.connect(
            lambda t: self.main.editor.new(text=t))
        treewidget.sig_open_interpreter_requested.connect(
            ipyconsole.create_client_from_path)
        treewidget.sig_redirect_stdio_requested.connect(
            self.main.redirect_internalshell_stdio)
        treewidget.sig_run_requested.connect(
            lambda fname: ipyconsole.run_script(fname, osp.dirname(
                fname), '', False, False, False, True, False))

        # TODO: This is not necessary anymore due to us starting workspace
        # services in the editor. However, we could restore it in the future.
        #lspmgr.sig_language_completions_available.connect(
        #    lambda settings, language:
        #        self.start_workspace_services())
        lspmgr.sig_stop_completions.connect(self.stop_workspace_services)

        # 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,
                              instance=self))
        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,
                              instance=self))
        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
        # TODO: Remove this once projects is migrated
        CONF.observe_configuration(self, 'explorer', 'single_click_to_open')
        self.register_project_type(self, EmptyProject)

    def on_configuration_change(self, option, section, 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_id))

    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, _language):
        """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=CompletionRequestTypes.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=CompletionRequestTypes.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=CompletionRequestTypes.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=CompletionRequestTypes.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=CompletionRequestTypes.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=CompletionRequestTypes.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(CompletionRequestTypes.WORKSPACE_APPLY_EDIT)
    @request(method=CompletionRequestTypes.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]
Ejemplo n.º 32
0
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)
        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, 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('trex'),
                                       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_trex([fname])
        
    def open_outside_trex(self, fnames):
        """Open file outside TRex with the appropriate application
        If this does not work, opening unknown file in TRex, 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:
            spyproject_path = osp.join(fname,'.spyproject')
            if osp.isdir(fname) and osp.exists(spyproject_path):
                QMessageBox.information(self, _('File Explorer'),
                                        _("The current directory contains a "
                                        "project.<br><br>"
                                        "If you want to delete"
                                        " the project, please go to "
                                        "<b>Projects</b> &raquo; <b>Delete "
                                        "Project</b>"))
            else:    
                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:
            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)

    def go_to_parent_directory(self):
        pass
        
    #----- 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 TRex
            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)
                
    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)
Ejemplo n.º 33
0
class NotebookPlugin(SpyderPluginWidget):
    """IPython Notebook plugin."""

    CONF_SECTION = 'notebook'
    focus_changed = Signal()

    def __init__(self, parent, testing=False):
        """Constructor."""
        SpyderPluginWidget.__init__(self, parent)
        self.testing = testing

        self.fileswitcher_dlg = None
        self.tabwidget = None
        self.menu_actions = None

        self.main = parent

        self.clients = []
        self.untitled_num = 0
        self.recent_notebooks = self.get_option('recent_notebooks', default=[])
        self.recent_notebook_menu = QMenu(_("Open recent"), self)
        self.options_menu = QMenu(self)

        # Initialize plugin
        self.initialize_plugin()

        layout = QVBoxLayout()

        new_notebook_btn = create_toolbutton(self,
                                             icon=ima.icon('project_expanded'),
                                             tip=_('Open a new notebook'),
                                             triggered=self.create_new_client)
        menu_btn = create_toolbutton(self,
                                     icon=ima.icon('tooloptions'),
                                     tip=_('Options'))

        menu_btn.setMenu(self.options_menu)
        menu_btn.setPopupMode(menu_btn.InstantPopup)
        add_actions(self.options_menu, self.menu_actions)
        corner_widgets = {Qt.TopRightCorner: [new_notebook_btn, menu_btn]}
        self.tabwidget = Tabs(self,
                              menu=self.options_menu,
                              actions=self.menu_actions,
                              corner_widgets=corner_widgets)

        if hasattr(self.tabwidget, 'setDocumentMode') \
           and not sys.platform == 'darwin':
            # Don't set document mode to true on OSX because it generates
            # a crash when the console is detached from the main window
            # Fixes Issue 561
            self.tabwidget.setDocumentMode(True)
        self.tabwidget.currentChanged.connect(self.refresh_plugin)
        self.tabwidget.move_data.connect(self.move_tab)

        self.tabwidget.set_close_function(self.close_client)

        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."""
        for cl in self.clients:
            cl.close()
        self.set_option('recent_notebooks', self.recent_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."""
        self.focus_changed.connect(self.main.plugin_focus_changed)
        self.main.add_dockwidget(self)
        self.ipyconsole = self.main.ipyconsole
        self.create_new_client(give_focus=False)
        icon_path = os.path.join(PACKAGE_PATH, 'images', 'icon.svg')
        self.main.add_to_fileswitcher(self, self.tabwidget, self.clients,
                                      QIcon(icon_path))
        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)
        client = self.get_current_client()
        if client:
            if client.get_filename() != WELCOME:
                self.save_as_action.setEnabled(True)
                self.open_console_action.setEnabled(True)
                self.options_menu.clear()
                add_actions(self.options_menu, self.menu_actions)
                return
        self.save_as_action.setEnabled(False)
        self.open_console_action.setEnabled(False)
        self.options_menu.clear()
        add_actions(self.options_menu, self.menu_actions)

    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 get_clients(self):
        """Return notebooks list."""
        return [cl for cl in self.clients if isinstance(cl, NotebookClient)]

    def get_focus_client(self):
        """Return current notebook with focus, if any."""
        widget = QApplication.focusWidget()
        for client in self.get_clients():
            if widget is client or widget is client.notebookwidget:
                return client

    def get_current_client(self):
        """Return the currently selected notebook."""
        try:
            client = self.tabwidget.currentWidget()
        except AttributeError:
            client = None
        if client is not None:
            return client

    def get_current_nbwidget(self):
        """Return the notebookwidget of the current client."""
        client = self.get_current_client()
        if client is not None:
            return client.notebookwidget

    def get_current_client_name(self, short=False):
        """Get the current client name."""
        client = self.get_current_client()
        if client:
            if short:
                return client.get_short_name()
            else:
                return client.get_filename()

    def create_new_client(self, filename=None, give_focus=True):
        """Create a new notebook or load a pre-existing one."""
        # Generate the notebook name (in case of a new one)
        if not filename:
            if not osp.isdir(NOTEBOOK_TMPDIR):
                os.makedirs(NOTEBOOK_TMPDIR)
            nb_name = 'untitled' + str(self.untitled_num) + '.ipynb'
            filename = osp.join(NOTEBOOK_TMPDIR, nb_name)
            nb_contents = nbformat.v4.new_notebook()
            nbformat.write(nb_contents, filename)
            self.untitled_num += 1

        # Save spyder_pythonpath before creating a client
        # because it's needed by our kernel spec.
        if not self.testing:
            CONF.set('main', 'spyder_pythonpath',
                     self.main.get_spyder_pythonpath())

        # Open the notebook with nbopen and get the url we need to render
        try:
            server_info = nbopen(filename)
        except (subprocess.CalledProcessError, NBServerError):
            QMessageBox.critical(
                self, _("Server error"),
                _("The Jupyter Notebook server failed to start or it is "
                  "taking too much time to do it. Please start it in a "
                  "system terminal with the command 'jupyter notebook' to "
                  "check for errors."))
            # Create a welcome widget
            # See issue 93
            self.untitled_num -= 1
            self.create_welcome_client()
            return

        welcome_client = self.create_welcome_client()
        client = NotebookClient(self, filename)
        self.add_tab(client)
        if NOTEBOOK_TMPDIR not in filename:
            self.add_to_recent(filename)
            self.setup_menu_actions()
        client.register(server_info)
        client.load_notebook()
        if welcome_client and not self.testing:
            self.tabwidget.setCurrentIndex(0)

    def close_client(self, index=None, client=None, save=False):
        """Close client tab from index or widget (or close current tab)."""
        if not self.tabwidget.count():
            return
        if client is not None:
            index = self.tabwidget.indexOf(client)
        if index is None and client is None:
            index = self.tabwidget.currentIndex()
        if index is not None:
            client = self.tabwidget.widget(index)

        is_welcome = client.get_filename() == WELCOME
        if not save and not is_welcome:
            client.save()
            wait_save = QEventLoop()
            QTimer.singleShot(1000, wait_save.quit)
            wait_save.exec_()
            path = client.get_filename()
            fname = osp.basename(path)
            nb_contents = nbformat.read(path, as_version=4)

            if ('untitled' in fname and len(nb_contents['cells']) > 0
                    and len(nb_contents['cells'][0]['source']) > 0):
                buttons = QMessageBox.Yes | QMessageBox.No
                answer = QMessageBox.question(
                    self, self.get_plugin_title(),
                    _("<b>{0}</b> has been modified."
                      "<br>Do you want to "
                      "save changes?".format(fname)), buttons)
                if answer == QMessageBox.Yes:
                    self.save_as(close=True)
        if not is_welcome:
            client.shutdown_kernel()
        client.close()

        # Delete notebook file if it is in temporary directory
        filename = client.get_filename()
        if filename.startswith(get_temp_dir()):
            try:
                os.remove(filename)
            except EnvironmentError:
                pass

        # Note: notebook index may have changed after closing related widgets
        self.tabwidget.removeTab(self.tabwidget.indexOf(client))
        self.clients.remove(client)

        self.create_welcome_client()

    def create_welcome_client(self):
        """Create a welcome client with some instructions."""
        if self.tabwidget.count() == 0:
            welcome = open(WELCOME).read()
            client = NotebookClient(self, WELCOME, ini_message=welcome)
            self.add_tab(client)
            return client

    def save_as(self, name=None, close=False):
        """Save notebook as."""
        current_client = self.get_current_client()
        current_client.save()
        original_path = current_client.get_filename()
        if not name:
            original_name = osp.basename(original_path)
        else:
            original_name = name
        filename, _selfilter = getsavefilename(self, _("Save notebook"),
                                               original_name, FILES_FILTER)
        if filename:
            nb_contents = nbformat.read(original_path, as_version=4)
            nbformat.write(nb_contents, filename)
            if not close:
                self.close_client(save=True)
            self.create_new_client(filename=filename)

    def open_notebook(self, filenames=None):
        """Open a notebook from file."""
        if not filenames:
            filenames, _selfilter = getopenfilenames(self, _("Open notebook"),
                                                     '', FILES_FILTER)
        if filenames:
            for filename in filenames:
                self.create_new_client(filename=filename)

    def open_console(self, client=None):
        """Open an IPython console for the given client or the current one."""
        if not client:
            client = self.get_current_client()
        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 tabs) --------------------------------------------
    def add_tab(self, widget):
        """Add tab."""
        self.clients.append(widget)
        index = self.tabwidget.addTab(widget, widget.get_short_name())
        self.tabwidget.setCurrentIndex(index)
        self.tabwidget.setTabToolTip(index, widget.get_filename())
        if self.dockwidget and not self.ismaximized:
            self.dockwidget.setVisible(True)
            self.dockwidget.raise_()
        self.activateWindow()
        widget.notebookwidget.setFocus()

    def move_tab(self, index_from, index_to):
        """Move tab."""
        client = self.clients.pop(index_from)
        self.clients.insert(index_to, client)

    # ------ Public API (for FileSwitcher) ------------------------------------
    def set_stack_index(self, index, instance):
        """Set the index of the current notebook."""
        if instance == self:
            self.tabwidget.setCurrentIndex(index)

    def get_current_tab_manager(self):
        """Get the widget with the TabWidget attribute."""
        return self
Ejemplo n.º 34
0
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)

        # Initialize plugin
        self.initialize_plugin()
        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.main.add_dockwidget(self)
        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 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()
        self.watcher.start(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()

    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]

    @Slot(str, str, bool)
    def file_moved(self, src_file, dest_file, is_dir):
        # TODO: Handle calls to LSP workspace
        pass

    @Slot(str, bool)
    def file_created(self, src_file, is_dir):
        # TODO: Handle calls to LSP workspace
        pass

    @Slot(str, bool)
    def file_deleted(self, src_file, is_dir):
        # TODO: Handle calls to LSP workspace
        pass

    @Slot(str, bool)
    def file_modified(self, src_file, is_dir):
        # TODO: Handle calls to LSP workspace
        pass
Ejemplo n.º 35
0
class PyDMLineEdit(QLineEdit, TextFormatter, PyDMWritableWidget, DisplayFormat):
    Q_ENUMS(DisplayFormat)
    DisplayFormat = DisplayFormat
    """
    A QLineEdit (writable text field) with support for Channels and more
    from PyDM.
    This widget offers an unit conversion menu when users Right Click
    into it.

    Parameters
    ----------
    parent : QWidget
        The parent widget for the Label
    init_channel : str, optional
        The channel to be used by the widget.
    """

    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()

    @Property(DisplayFormat)
    def displayFormat(self):
        return self._display_format_type

    @displayFormat.setter
    def displayFormat(self, new_type):
        if self._display_format_type != new_type:
            self._display_format_type = new_type
            # Trigger the update of display format
            self.value_changed(self.value)

    def value_changed(self, new_val):
        """
        Receive and update the PyDMLineEdit for a new channel value

        The actual value of the input is saved as well as the type received.
        This also resets the PyDMLineEdit display text using
        :meth:`.set_display`

        Parameters
        ----------
        value: str, float or int
            The new value of the channel
        """
        super(PyDMLineEdit, self).value_changed(new_val)
        self.set_display()

    def send_value(self):
        """
        Emit a :attr:`send_value_signal` to update channel value.

        The text is cleaned of all units, user-formatting and scale values
        before being sent back to the channel. This function is attached the
        ReturnPressed signal of the PyDMLineEdit
        """
        send_value = str(self.text())
        # Clean text of unit string
        if self._show_units and self._unit and self._unit in send_value:
            send_value = send_value[:-len(self._unit)].strip()
        try:
            if self.channeltype not in [str, np.ndarray]:
                scale = self._scale
                if scale is None or scale == 0:
                    scale = 1.0

                if self._display_format_type in [DisplayFormat.Default, DisplayFormat.String]:
                    if self.channeltype == float:
                        num_value = locale.atof(send_value)
                    else:
                        num_value = self.channeltype(send_value)
                    scale = self.channeltype(scale)
                elif self._display_format_type == DisplayFormat.Hex:
                    num_value = int(send_value, 16)
                elif self._display_format_type == DisplayFormat.Binary:
                    num_value = int(send_value, 2)
                elif self._display_format_type in [DisplayFormat.Exponential, DisplayFormat.Decimal]:
                    num_value = locale.atof(send_value)

                num_value = num_value / scale
                self.send_value_signal[self.channeltype].emit(num_value)
            elif self.channeltype == np.ndarray:
                # Arrays will be in the [1.2 3.4 22.214] format
                if self._display_format_type == DisplayFormat.String:
                    self.send_value_signal[str].emit(send_value)
                else:
                    arr_value = list(filter(None, send_value.replace("[", "").replace("]", "").split(" ")))
                    arr_value = np.array(arr_value, dtype=self.subtype)
                    self.send_value_signal[np.ndarray].emit(arr_value)
            else:
                # Channel Type is String
                # Lets just send what we have after all
                self.send_value_signal[str].emit(send_value)
        except ValueError:
            logger.exception("Error trying to set data '{0}' with type '{1}' and format '{2}' at widget '{3}'."
                         .format(self.text(), self.channeltype, self._display_format_type, self.objectName()))

        self.clearFocus()
        self.set_display()

    def write_access_changed(self, new_write_access):
        """
        Change the PyDMLineEdit to read only if write access is denied
        """
        super(PyDMLineEdit, self).write_access_changed(new_write_access)
        self.setReadOnly(not new_write_access)

    def unit_changed(self, new_unit):
        """
        Accept a unit to display with a channel's value

        The unit may or may not be displayed based on the :attr:`showUnits`
        attribute. Receiving a new value for the unit causes the display to
        reset.
        """
        super(PyDMLineEdit, self).unit_changed(new_unit)
        self._scale = 1
        self.create_unit_options()

    def create_unit_options(self):
        """
        Create the menu for displaying possible unit values

        The menu is filled with possible unit conversions based on the
        current PyDMLineEdit. If either the unit is not found in the by
        the :func:`utilities.find_unit_options` function, or, the
        :attr:`.showUnits` attribute is set to False, the menu will tell
        the user that there are no available conversions
        """
        self.unitMenu.clear()
        units = utilities.find_unit_options(self._unit)
        if units and self._show_units:
            for choice in units:
                self.unitMenu.addAction(choice,
                                        partial(
                                            self.apply_conversion,
                                            choice
                                            )
                                        )
        else:
            self.unitMenu.addAction('No Unit Conversions found')

    def apply_conversion(self, unit):
        """
        Convert the current unit to a different one

        This function will attempt to find a scalar to convert the current
        unit type to the desired one and reset the display with the new
        conversion.

        Parameters
        ----------
        unit : str
            String name of desired units
        """
        if not self._unit:
            logger.warning("Warning: Attempting to convert PyDMLineEdit unit, but no initial units supplied.")
            return None

        scale = utilities.convert(str(self._unit), unit)
        if scale:
            self._scale = scale * float(self._scale)
            self._unit = unit
            self.update_format_string()
            self.clearFocus()
            self.set_display()
        else:
            logging.warning("Warning: Attempting to convert PyDMLineEdit unit, but '{0}' can not be converted to '{1}'."
                            .format(self._unit, unit))

    def widget_ctx_menu(self):
        """
        Fetch the Widget specific context menu which will be populated with additional tools by `assemble_tools_menu`.

        Returns
        -------
        QMenu or None
            If the return of this method is None a new QMenu will be created by `assemble_tools_menu`.
        """
        menu = self.createStandardContextMenu()
        menu.addSeparator()
        menu.addMenu(self.unitMenu)
        return menu

    def set_display(self):
        """
        Set the text display of the PyDMLineEdit.

        The original value given by the PV is converted to a text entry based
        on the current settings for scale value, precision, a user-defined
        format, and the current units. If the user is currently entering a
        value in the PyDMLineEdit the text will not be changed.
        """
        if self.value is None:
            return

        if self.hasFocus():
            return

        new_value = self.value

        if self._display_format_type in [DisplayFormat.Default,
                                         DisplayFormat.Decimal,
                                         DisplayFormat.Exponential,
                                         DisplayFormat.Hex,
                                         DisplayFormat.Binary]:
            if self.channeltype not in (str, np.ndarray):
                try:
                    new_value *= self.channeltype(self._scale)
                except TypeError:
                    logger.error("Cannot convert the value '{0}', for channel '{1}', to type '{2}'. ".format(
                        self._scale, self._channel, self.channeltype))

        new_value = parse_value_for_display(value=new_value,  precision=self.precision,
                                            display_format_type=self._display_format_type,
                                            string_encoding=self._string_encoding,
                                            widget=self)

        if type(new_value) in str_types:
            self._display = new_value
        else:
            self._display = str(new_value)

        if self._display_format_type == DisplayFormat.Default:
            if isinstance(new_value, (int, float)):
                self._display = str(self.format_string.format(new_value))
                self.setText(self._display)
                return

        if self._show_units:
            self._display = "{} {}".format(self._display, self._unit)

        self.setText(self._display)

    def focusOutEvent(self, event):
        """
        Overwrites the function called when a user leaves a PyDMLineEdit
        without pressing return.  Resets the value of the text field to the
        current channel value.
        """
        if self._display is not None:
            self.setText(self._display)
        super(PyDMLineEdit, self).focusOutEvent(event)
Ejemplo n.º 36
0
class Projects(ProjectExplorerWidget, SpyderPluginMixin):
    """Projects plugin"""

    CONF_SECTION = "project_explorer"

    open_terminal = Signal(str)
    open_interpreter = Signal(str)
    pythonpath_changed = Signal()

    # File operations
    create_module = Signal(str)
    edit = Signal(str)
    removed = Signal(str)
    removed_tree = Signal(str)
    renamed = Signal(str, str)
    redirect_stdio = Signal(bool)

    # Project handling
    sig_project_created = Signal(object, object, object)
    sig_project_loaded = Signal(object)
    sig_project_closed = Signal(object)

    def __init__(self, parent=None):
        ProjectExplorerWidget.__init__(
            self,
            parent=parent,
            name_filters=self.get_option("name_filters"),
            show_all=self.get_option("show_all"),
            show_hscrollbar=self.get_option("show_hscrollbar"),
        )
        SpyderPluginMixin.__init__(self, parent)

        self.recent_projects = self.get_option("recent_projects", default=[])
        self.current_active_project = None
        self.latest_project = None

        self.editor = None
        self.workingdirectory = None

        # Initialize plugin
        self.initialize_plugin()
        self.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.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.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 register_plugin(self):
        """Register plugin in Spyder's main window"""
        self.editor = self.main.editor
        self.workingdirectory = self.main.workingdirectory

        self.main.pythonpath_changed()
        self.main.restore_scrollbar_position.connect(self.restore_scrollbar_position)
        self.pythonpath_changed.connect(self.main.pythonpath_changed)
        self.create_module.connect(self.editor.new)
        self.edit.connect(self.editor.load)
        self.removed.connect(self.editor.removed)
        self.removed_tree.connect(self.editor.removed_tree)
        self.renamed.connect(self.editor.renamed)
        self.editor.set_projects(self)
        self.main.add_dockwidget(self)

        self.sig_open_file.connect(self.main.open_file)

        # New project connections. Order matters!
        self.sig_project_loaded.connect(lambda v: self.workingdirectory.chdir(v))
        self.sig_project_loaded.connect(lambda v: self.main.update_window_title())
        self.sig_project_loaded.connect(lambda v: self.editor.setup_open_files())
        self.sig_project_loaded.connect(self.update_explorer)
        self.sig_project_closed[object].connect(lambda v: self.workingdirectory.chdir(self.get_last_working_dir()))
        self.sig_project_closed.connect(lambda v: self.main.update_window_title())
        self.sig_project_closed.connect(lambda v: self.editor.setup_open_files())
        self.recent_project_menu.aboutToShow.connect(self.setup_menu_actions)

    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.closing_widget()
        return 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 v, path=project: self.open_project(path=path),
                    )
                    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.edit_project_preferences_action.setEnabled(active)

    def edit_project_preferences(self):
        """Edit Spyder active project preferences"""
        from spyder.widgets.projects.configdialog 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"""
        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_():
            pass
            if active_project is None:
                self.show_explorer()
            self.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):
        """Open the project located in `path`"""
        if path is None:
            basedir = get_home_dir()
            path = getexistingdirectory(parent=self, caption=_("Open project"), basedir=basedir)
            if not self.is_valid_project(path):
                if path:
                    QMessageBox.critical(self, _("Error"), _("<b>%s</b> is not a Spyder project!") % path)
                return
            else:
                self.add_to_recent(path)

        # A project was not open before
        if self.current_active_project is None:
            self.editor.save_open_files()
            self.editor.set_option("last_working_dir", getcwd())
            self.show_explorer()
        else:  # we are switching projects
            self.set_project_filenames(self.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.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:
            path = self.current_active_project.root_path
            self.set_project_filenames(self.editor.get_open_filenames())
            self.current_active_project = None
            self.set_option("current_project_path", None)
            self.setup_menu_actions()
            self.sig_project_closed.emit(path)
            self.pythonpath_changed.emit()
            self.dockwidget.close()
            self.clear()
            self.restart_consoles()

    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)
            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:
            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.editor.get_option("last_working_dir", default=getcwd())

    def save_config(self):
        """Save configuration: opened projects & tree widget state"""
        self.set_option("recent_projects", self.recent_projects)
        self.set_option("expanded_state", self.treewidget.get_expanded_state())
        self.set_option("scrollbar_position", self.treewidget.get_scrollbar_position())

    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.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.treewidget.set_scrollbar_position(scrollbar_pos)

    def update_explorer(self):
        """Update explorer tree"""
        self.setup_project(self.get_active_project_path())

    def show_explorer(self):
        """Show the explorer"""
        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"""
        self.main.extconsole.restart()
        if self.main.ipyconsole:
            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]