Beispiel #1
0
class FileSystemTable(QTableView, TableType):
    if IN_DESIGNER:
        from PyQt5.QtCore import Q_ENUMS
        Q_ENUMS(TableType)

    gcodeFileSelected = Signal(bool)
    filePreviewText = Signal(str)
    fileNamePreviewText = Signal(str)
    transferFileRequest = Signal(str)
    rootChanged = Signal(str)

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

        self._table_type = TableType.Local
        self._hidden_columns = ''

        # This prevents doing unneeded initialization
        # when QtDesginer loads the plugin.
        if parent is None:
            return

        self.parent = parent
        self.path_data = dict()
        self.doubleClicked.connect(self.openSelectedItem)
        self.selected_row = None
        self.clipboard = QApplication.clipboard()

        self.model = QFileSystemModel()
        self.model.setReadOnly(True)
        self.model.setFilter(QDir.AllDirs | QDir.NoDotAndDotDot
                             | QDir.AllEntries)

        self.setModel(self.model)

        self.verticalHeader().hide()
        self.horizontalHeader().setStretchLastSection(True)
        self.setAlternatingRowColors(True)
        self.setSelectionBehavior(QAbstractItemView.SelectRows)

        self.selection_model = self.selectionModel()
        self.selection_model.selectionChanged.connect(self.onSelectionChanged)

        self.info = Info()
        self.editor = self.info.getEditor()
        self._nc_file_dir = self.info.getProgramPrefix()
        self.nc_file_exts = self.info.getProgramExtentions()
        self.setRootPath(self._nc_file_dir)

    def showEvent(self, event=None):
        self.rootChanged.emit(self._nc_file_dir)

    def onSelectionChanged(self, selected, deselected):

        if len(selected) == 0:
            return

        index = selected.indexes()[0]
        path = self.model.filePath(index)

        if os.path.isfile(path):
            self.gcodeFileSelected.emit(True)
            with open(path, 'r') as fh:
                content = fh.read()
            self.filePreviewText.emit(content)
            self.fileNamePreviewText.emit(path)
        else:
            self.gcodeFileSelected.emit(False)
            self.filePreviewText.emit('')
            self.fileNamePreviewText.emit('')

    @Slot()
    def openSelectedItem(self, index=None):
        """If ngc file, opens in LinuxCNC, if dir displays dir."""
        if index is None:
            selection = self.getSelection()
            if selection is None:
                return
            index = selection[0]

        path = self.model.filePath(self.rootIndex())
        name = self.model.filePath(index)

        absolute_path = os.path.join(path, name)

        file_info = QFileInfo(absolute_path)
        if file_info.isDir():
            self.model.setRootPath(absolute_path)
            self.setRootIndex(self.model.index(absolute_path))
            self.rootChanged.emit(absolute_path)

        elif file_info.isFile():
            # if file_info.completeSuffix() not in self.nc_file_exts:
            #     LOG.warn("Unsuported NC program type with extention .%s",
            #              file_info.completeSuffix())
            loadProgram(absolute_path)

    @Slot()
    def editSelectedFile(self):
        """Open the selected file in editor."""
        selection = self.getSelection()
        if selection is not None:
            path = self.model.filePath(selection[0])
            subprocess.Popen([self.editor, path])
        return False

    @Slot()
    def loadSelectedFile(self):
        """Loads the selected file into LinuxCNC."""
        selection = self.getSelection()
        if selection is not None:
            path = self.model.filePath(selection[0])
            loadProgram(path)
            return True
        return False

    @Slot()
    def selectPrevious(self):
        """Select the previous item in the view."""
        selection = self.getSelection()
        if selection is None:
            # select last item in view
            self.selectRow(self.model.rowCount(self.rootIndex()) - 1)
        else:
            self.selectRow(selection[0].row() - 1)
        return True

    @Slot()
    def selectNext(self):
        """Select the next item in the view."""
        selection = self.getSelection()
        if selection is None:
            # select first item in view
            self.selectRow(0)
        else:
            self.selectRow(selection[-1].row() + 1)
        return True

    @Slot()
    def rename(self):
        """renames the selected file or folder"""
        index = self.selectionModel().currentIndex()
        path = self.model.filePath(index)
        if path:
            file_info = QFileInfo(path)

            if file_info.isFile():
                filename = self.rename_dialog("file")

                if filename:
                    q_file = QFile(path)
                    file_info.absolutePath()
                    new_path = os.path.join(file_info.absolutePath(),
                                            str(filename))
                    q_file.rename(new_path)

            elif file_info.isDir():
                filename = self.rename_dialog("directory")

                if filename:
                    directory = QDir(path)
                    file_info.absolutePath()
                    new_path = os.path.join(file_info.absolutePath(),
                                            str(filename))
                    directory.rename(path, new_path)

    @Slot()
    def newFile(self):
        """Create a new empty file"""
        path = self.model.filePath(self.rootIndex())
        new_file_path = os.path.join(path, "New File.ngc")

        count = 1
        while os.path.exists(new_file_path):
            new_file_path = os.path.join(path, "New File {}.ngc".format(count))
            count += 1

        new_file = QFile(new_file_path)
        new_file.open(QIODevice.ReadWrite)

    @Slot()
    def newFolder(self):
        path = self.model.filePath(self.rootIndex())

        new_name = 'New Folder'

        count = 1
        while os.path.exists(os.path.join(path, new_name)):
            new_name = "New Folder {}".format(count)
            count += 1

        directory = QDir(path)
        directory.mkpath(new_name)
        directory.setPath(new_name)

    @Slot()
    @deprecated(replaced_by='newFolder',
                reason='for consistency with newFile method name')
    def createDirectory(self):
        self.newFolder()

    @Slot()
    def deleteItem(self):
        """Delete the selected item (either a file or folder)."""
        # ToDo: use Move2Trash, instead of deleting the file
        index = self.selectionModel().currentIndex()
        path = self.model.filePath(index)
        if path:
            file_info = QFileInfo(path)
            if file_info.isFile():
                if not self.ask_dialog(
                        "Do you wan't to delete the selected file?"):
                    return
                q_file = QFile(path)
                q_file.remove()

            elif file_info.isDir():
                if not self.ask_dialog(
                        "Do you wan't to delete the selected directory?"):
                    return
                directory = QDir(path)
                directory.removeRecursively()

    @Slot()
    @deprecated(replaced_by='deleteItem',
                reason='because of unclear method name')
    def deleteFile(self):
        self.deleteItem()

    @Slot(str)
    def setRootPath(self, root_path):
        """Sets the currently displayed path."""

        self.rootChanged.emit(root_path)
        self.model.setRootPath(root_path)
        self.setRootIndex(self.model.index(root_path))

        return True

    @Slot()
    def viewParentDirectory(self):
        """View the parent directory of the current view."""

        path = self.model.filePath(self.rootIndex())

        file_info = QFileInfo(path)
        directory = file_info.dir()
        new_path = directory.absolutePath()

        currentRoot = self.rootIndex()

        self.model.setRootPath(new_path)
        self.setRootIndex(currentRoot.parent())
        self.rootChanged.emit(new_path)

    @Slot()
    @deprecated(replaced_by='viewParentDirectory')
    def goUP(self):
        self.viewParentDirecotry()

    @Slot()
    def viewHomeDirectory(self):
        self.setRootPath(os.path.expanduser('~/'))

    @Slot()
    def viewNCFilesDirectory(self):
        # ToDo: Make preset user definable
        path = os.path.expanduser('~/linuxcnc/nc_files')
        self.setRootPath(path)

    @Slot()
    def viewPresetDirectory(self):
        # ToDo: Make preset user definable
        preset = os.path.expanduser('~/linuxcnc/nc_files')
        self.setRootPath(preset)

    @Slot()
    def doFileTransfer(self):
        index = self.selectionModel().currentIndex()
        path = self.model.filePath(index)
        self.transferFileRequest.emit(path)

    @Slot(str)
    def transferFile(self, src_path):
        dest_path = self.model.filePath(self.rootIndex())

        src_file = QFile()
        src_file.setFileName(src_path)

        src_file_info = QFileInfo(src_path)

        dst_path = os.path.join(dest_path, src_file_info.fileName())

        src_file.copy(dst_path)

    @Slot()
    def getSelection(self):
        """Returns list of selected indexes, or None."""
        selection = self.selection_model.selectedIndexes()
        if len(selection) == 0:
            return None
        return selection

    @Slot()
    def getCurrentDirectory(self):
        return self.model.rootPath()

    @Property(TableType)
    def tableType(self):
        return self._table_type

    @tableType.setter
    def tableType(self, table_type):
        self._table_type = table_type
        if table_type == TableType.Local:
            self.setRootPath(self._nc_file_dir)
        else:
            self.setRootPath('/media/')

    @Property(str)
    def hiddenColumns(self):
        """String of comma separated column numbers to hide."""
        return self._hidden_columns

    @hiddenColumns.setter
    def hiddenColumns(self, columns):
        try:
            col_list = [int(c) for c in columns.split(',') if c != '']
        except:
            return False

        self._hidden_columns = columns

        header = self.horizontalHeader()
        for col in range(4):
            if col in col_list:
                header.hideSection(col)
            else:
                header.showSection(col)

    def ask_dialog(self, message):
        box = QMessageBox.question(self.parent, 'Are you sure?', message,
                                   QMessageBox.Yes, QMessageBox.No)
        if box == QMessageBox.Yes:
            return True
        else:
            return False

    def rename_dialog(self, data_type):
        text, ok_pressed = QInputDialog.getText(
            self.parent, "Rename", "New {} name:".format(data_type),
            QLineEdit.Normal, "")

        if ok_pressed and text != '':
            return text
        else:
            return False
Beispiel #2
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)
Beispiel #3
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)
Beispiel #4
0
class Q7File(QWidget, Ui_Q7FileWindow):
    def __init__(self, parent, mode=LOADMODE):
        super(Q7File, self).__init__()
        self.setupUi(self)
        self.setWindowTitle("Load/Save")
        self.parent = parent
        self.iconProvider = Q7FileIconProvider()
        self.model = QFileSystemModel()
        self.model.setIconProvider(self.iconProvider)
        self.proxy = Q7FileFilterProxy(self)
        self.proxy.setSourceModel(self.model)
        self.treeview.setModel(self.proxy)
        self.setAttribute(Qt.WA_DeleteOnClose)
        #
        if HAS_PY2:
            self.model.directoryLoaded[unicode].connect(self.expandCols)
        else:
            self.model.directoryLoaded[str].connect(self.expandCols)
        self.treeview.expanded[QModelIndex].connect(self.expandCols)
        self.treeview.clicked[QModelIndex].connect(self.clickedNode)
        self.treeview.doubleClicked[QModelIndex].connect(
            self.clickedNodeAndLoad)
        self.direntries.lineEdit().returnPressed.connect(self.changeDirEdit)
        self.direntries.currentIndexChanged[int].connect(self.changeDirIndex)
        self.fileentries.currentIndexChanged[int].connect(self.changeFile)
        self.fileentries.lineEdit().editingFinished.connect(self.changeFile)
        self.tabs.currentChanged[int].connect(self.currentTabChanged)
        self.cShowAll.stateChanged[int].connect(self.updateView)
        self.cShowDirs.stateChanged[int].connect(self.updateView)
        self.rClearSelectedDirs.toggled[bool].connect(self.updateClearDirs)
        self.rClearSelectedFiles.toggled[bool].connect(self.updateClearFiles)
        self.rClearAllDirs.toggled[bool].connect(self.updateClearNone)
        self.rClearNoHDF.toggled[bool].connect(self.updateClearNoHDF)
        self.rClearNotFound.toggled[bool].connect(self.updateClearNotFound)
        #
        self.bClose.clicked.connect(self.closeUnlock)
        self.bCurrent.clicked.connect(self.currentDir)
        self.bBack.clicked.connect(self.backDir)
        self.bInfo.clicked.connect(self.infoFileView)
        self.bInfo2.clicked.connect(self.infoFileView)
        self.bClearHistory.clicked.connect(self.doClearHistory)
        self.mode = mode
        if self.mode == SAVEMODE:
            self.setMode(False)
        else:
            self.setMode(True)
        self.setBoxes()
        self.parent.getHistory()
        self.updateHistory()
        self.updateClearNotFound()

    def closeUnlock(self):
        self.parent.signals.cancel.emit()
        self.close()

    def closeEvent(self, event):
        self.closeUnlock()

    def infoFileView(self):
        self.parent.helpWindow('File')

    def updateView(self):
        p = self.direntries.currentText()
        self.setCurrentDir(p)
        print('CURRENT', p)
        # self.proxy.reset()
        self.proxy.beginResetModel()
        self.proxy.endResetModel()
        self.setCurrentDir(p)
        self.updateFileIfFound()

    def currentTabChanged(self, tabno):
        self.expandCols()
        self.setBoxes()

    def getOpt(self, name):
        return getattr(self, '_Ui_Q7FileWindow__O_' + name.lower())

    def setBoxes(self):
        ckh = self.getOpt('FilterHDFFiles')
        ckg = self.getOpt('FilterCGNSFiles')
        if self.parent.getOptionValue('FilterHDFFiles'):
            ckh.setCheckState(Qt.Checked)
        else:
            ckh.setCheckState(Qt.Unchecked)
        if self.parent.getOptionValue('FilterCGNSFiles'):
            ckg.setCheckState(Qt.Checked)
        else:
            ckg.setCheckState(Qt.Unchecked)
        if ((ckh.checkState() == Qt.Unchecked)
                and (ckg.checkState() == Qt.Unchecked)):
            self.cShowAll.setCheckState(Qt.Checked)

    def getBoxes(self):
        if self.getOpt('FilterHDFFiles').checkState() == Qt.Unchecked:
            self.parent.setOptionValue('FilterHDFFiles', False)
        else:
            self.parent.setOptionValue('FilterHDFFiles', True)
        if self.getOpt('FilterCGNSFiles').checkState() == Qt.Unchecked:
            self.parent.setOptionValue('FilterCGNSFiles', False)
        else:
            self.parent.setOptionValue('FilterCGNSFiles', True)

    def expandCols(self, *args):
        self.getBoxes()
        for n in range(3):
            self.treeview.resizeColumnToContents(n)

    def currentDir(self, *args):
        # p=os.path.split(self.path())[0]
        p = os.getcwd()
        self.setCurrentDir(p)

    def backDir(self, *args):
        p = os.path.split(self.path())[0]
        self.setCurrentDir(p)

    def changeDirEdit(self, *args):
        self.changeDir(args)

    def changeDirText(self, *args):
        self.changeDir(args)

    def changeDirIndex(self, *args):
        self.changeDir(args)

    def changeDir(self, *args):
        if self.updateMode:
            return
        p = str(self.direntries.currentText())
        if os.path.isdir(p):
            self.updateView()
        else:
            reply = MSG.wQuestion(
                self.parent, 110, 'Directory not found...',
                """The path below doesn't exist, do you want to remove<br>
                                     it from the history?<br>%s""" % p)
            if reply == MSG.OK:
                ix = self.direntries.currentIndex()
                self.direntries.removeItem(ix)

    def changeFile(self, *args):
        self.selectedfile = str(self.fileentries.lineEdit().text())
        d = None
        if self.cAutoDir.checkState() == Qt.Checked:
            d = self.parent.getHistoryFile(self.selectedfile)
        if d is not None:
            self.selecteddir = d[0]
            ix = self.direntries.findText(self.selecteddir)
            if ix != -1:
                self.direntries.setCurrentIndex(ix)
        else:
            self.selecteddir = self.direntries.lineEdit().text()
        self.updateFileIfFound()

    def selectedPath(self):
        return os.path.join(self.selecteddir, self.selectedfile)

    def updateFileIfFound(self):
        filepath = self.selectedPath()
        midx = self.model.index(filepath)
        if midx.row == 1:
            return
        fidx = self.proxy.mapFromSource(midx)
        if fidx.row == 1:
            return
        self.treeview.setCurrentIndex(fidx)
        self.treeview.scrollTo(fidx)

    def setCurrentDir(self, newpath):
        self.model.setRootPath(newpath)
        midx = self.model.index(newpath)
        fidx = self.proxy.mapFromSource(midx)
        self.treeview.setRootIndex(fidx)
        self.treeview.setCurrentIndex(fidx)
        self.direntries.setItemText(self.direntries.currentIndex(), newpath)
        self.selecteddir = newpath

    def setMode(self, load=True):
        if load:
            self.bAction.clicked.connect(self.load)
            self.bAction.setToolTip(LOADBUTTON[1])
            self.bAction.setText(LOADBUTTON[0])
            self.cOverwrite.setEnabled(False)
            self.cReadOnly.setEnabled(True)
            self.cNoLargeData.setEnabled(True)
            self.cFollowLinks.setEnabled(True)
            self.cDeleteMissing.setEnabled(False)
            if OCTXT.DoNotLoadLargeArrays:
                self.cNoLargeData.setCheckState(Qt.Checked)
            else:
                self.cNoLargeData.setCheckState(Qt.Unchecked)
            if OCTXT.FollowLinksAtLoad:
                self.cFollowLinks.setCheckState(Qt.Checked)
            else:
                self.cFollowLinks.setCheckState(Qt.Unchecked)
        else:
            self.bAction.clicked.connect(self.save)
            self.bAction.setToolTip(SAVEBUTTON[1])
            self.bAction.setText(SAVEBUTTON[0])
            self.cOverwrite.setEnabled(True)
            self.cReadOnly.setEnabled(False)
            self.cNoLargeData.setEnabled(False)
            self.cFollowLinks.setEnabled(False)
            self.cDeleteMissing.setEnabled(True)
            if OCTXT.FileUpdateRemovesChildren:
                self.cDeleteMissing.setCheckState(Qt.Checked)
            else:
                self.cDeleteMissing.setCheckState(Qt.Unchecked)

    def updateHistory(self):
        self.updateMode = True
        self.direntries.clear()
        self.fileentries.clear()
        hlist = self.parent.getHistory(fromfile=False)
        flist = []
        self.fileentries.addItem("")
        for i in list(hlist):
            if i != self.parent.getHistoryLastKey():
                self.direntries.addItem(str(i))
                flist = flist + hlist[i]
        for i in flist:
            self.fileentries.addItem(str(i))
        self.historyfiles = flist
        self.historydirs = list(hlist)
        if self.parent.getHistoryLastKey() in list(hlist):
            self.selecteddir = hlist[self.parent.getHistoryLastKey()][0]
            self.selectedfile = hlist[self.parent.getHistoryLastKey()][1]
            # ixd=self.direntries.findText(self.selecteddir)
            self.setCurrentDir(self.selecteddir)
            ixf = self.fileentries.findText(self.selectedfile)
            self.fileentries.setCurrentIndex(ixf)
            self.changeFile()
        else:
            self.selecteddir = os.getcwd()
            self.selectedfile = ""
            self.setCurrentDir(self.selecteddir)
        self.updateMode = False

    def doClearHistory(self):
        if self.rClearNoHDF.isChecked():
            reply = MSG.wQuestion(
                self.parent, 120, 'Clear history',
                """You really want to remove directory entries from history<br>
                                     where no file with defined extensions has been found?<br>"""
            )
            if reply == MSG.OK:
                for d in self.parent.getDirNoHDFFromHistory():
                    self.parent.removeDirFromHistory(d)
                    self.updateHistory()
                    self.lClear.clear()
        if self.rClearNotFound.isChecked():
            reply = MSG.wQuestion(
                self.parent, 121, 'Clear history',
                """You really want to remove <b>NOT FOUND</b> entries from<br>
                                     the history of used directories?<br>""")
            if reply == MSG.OK:
                for d in self.parent.getDirNotFoundFromHistory():
                    self.parent.removeDirFromHistory(d)
                    self.updateHistory()
                    self.lClear.clear()
        if self.rClearAllDirs.isChecked():
            reply = MSG.wQuestion(
                self.parent, 122, 'Clear history',
                """You really want to remove <b>ALL</b> entries from the<br>
                                     the history of used files and directories?<br>"""
            )
            if reply == MSG.OK:
                self.parent.destroyHistory()
                self.updateHistory()
        if self.rClearSelectedDirs.isChecked():
            for it in self.lClear.selectedItems():
                self.parent.removeDirFromHistory(it.text())
            self.updateHistory()
            self.updateClearDirs()
        if self.rClearSelectedFiles.isChecked():
            for it in self.lClear.selectedItems():
                fd = self.parent.getHistoryFile(it.text())
                if fd is not None:
                    self.parent.removeFileFromHistory(*fd)
            self.updateHistory()
            self.updateClearFiles()

    def updateClearNone(self):
        self.lClear.clear()

    def updateClearNoHDF(self):
        self.lClear.clear()
        self.lClear.setSelectionMode(QAbstractItemView.NoSelection)
        for d in self.parent.getDirNoHDFFromHistory():
            self.lClear.addItem(d)

    def updateClearNotFound(self):
        self.lClear.clear()
        self.lClear.setSelectionMode(QAbstractItemView.NoSelection)
        for d in self.parent.getDirNotFoundFromHistory():
            self.lClear.addItem(d)

    def updateClearDirs(self):
        self.lClear.clear()
        self.lClear.setSelectionMode(QAbstractItemView.MultiSelection)
        for d in self.historydirs:
            self.lClear.addItem(d)

    def updateClearFiles(self):
        self.lClear.clear()
        self.lClear.setSelectionMode(QAbstractItemView.MultiSelection)
        for d in self.historyfiles:
            self.lClear.addItem(d)

    def load(self):
        diag = self.checkTarget(self.selectedPath())
        if diag is None:
            self.parent.signals.buffer = self.selectedPath()
            self.hide()
            self.parent.signals.loadFile.emit()
            self.close()
        else:
            MSG.message("Load file: %s" % self.selectedPath(), diag, MSG.INFO)

    def save(self):
        diag = self.checkTarget(self.selectedPath(), write=True)
        if diag is None:
            self.parent.signals.buffer = self.selectedPath()
            self.hide()
            self.parent.signals.saveFile.emit()
            self.close()
        else:
            MSG.message("Save file: %s" % self.selectedPath(), diag, MSG.INFO)

    def checkTarget(self, filename, write=False):
        if os.path.isfile(filename) and not write:
            return None
        if not os.path.isfile(filename) and write:
            return None
        if self.cOverwrite.isChecked():
            sc = "The file is <b>completely replaced</b> by the current tree"
        else:
            sc = "The file is <b>updated</b> with the current tree contents."
            if self.cDeleteMissing.isChecked():
                sc += "The nodes <b>NOT</b> found in the current tree are removed from the updated file"
            else:
                sc += "The nodes <b>NOT</b> found in the current tree are kept unchanged"
        reply = MSG.wQuestion(self,
                              132,
                              'Saving on an already existing file',
                              """You are going to save into an existing file,
                              based on the current options you have and the target tree you want to save,
                              the result would be the following:<p>%s<p>
                              If this not the way you want the save to operate, please <i>abort</b> this
                              file selection and check the <i>Load/Save option</i> tab.
                              You still want to write on this file?""" % sc,
                              buttons=('Continue to save on existing file',
                                       'Abort save'))
        if reply:
            return None
        return 'User Abort'

    def path(self, index=None):
        if index is None:
            idx = self.treeview.currentIndex()
            p = str(self.model.filePath(self.proxy.mapToSource(idx)))
        else:
            p = str(self.model.filePath(self.proxy.mapToSource(index)))
        return p

    def clickedNodeAndLoad(self, index):
        self.clickedNode(index)
        if self.mode == SAVEMODE:
            self.save()
        else:
            self.load()

    def clickedNode(self, index):
        self.treeview.resizeColumnToContents(0)
        p = self.path(index)
        if os.path.isdir(p):
            f = ''
            d = p
            self.setCurrentDir(d)
        else:
            f = os.path.basename(self.path(index))
            d = os.path.dirname(self.path(index))
        ix = self.direntries.findText(d)
        if ix != -1:
            self.direntries.setCurrentIndex(ix)
        else:
            self.direntries.addItem(d)
            self.direntries.setCurrentIndex(self.direntries.findText(d))
        ix = self.fileentries.findText(f)
        if ix != -1:
            self.fileentries.setCurrentIndex(ix)
        else:
            self.fileentries.addItem(f)
            self.fileentries.setCurrentIndex(self.fileentries.findText(f))
        self.selecteddir = self.direntries.lineEdit().text()
        self.selectedfile = self.fileentries.lineEdit().text()