class FileBrowserInputWidget(AbstractListInputWidget, iTansuGroupInput): def __init__(self, parent=None): super(FileBrowserInputWidget, self).__init__(parent=parent) # setup model self.model = QFileSystemModel() self.model.setRootPath('/home/') filters = self.model.filter() self.model.setFilter(filters | QDir.Hidden) # setup completer completer = QCompleter(self.model, self) self.setCompleter(completer) installCompleterPopup(completer) self.setCompleter(completer) completer.setCaseSensitivity(Qt.CaseInsensitive) self.autoCompleteList = [] def checkDirectory(self): directory = str(self.text()) if os.path.isdir(directory): self.model.setRootPath(str(self.text())) def event(self, event, *args, **kwargs): # I think this is the / key... lol if (event.type() == QEvent.KeyRelease) and event.key() == 47: self.checkDirectory() #self.completer().popup().show() self.completer().complete() return AbstractListInputWidget.event(self, event, *args, **kwargs)
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
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)
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> » <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)
def __init__(self, settings, path=None): super(RootWidget, self).__init__() # --- setup the file system model --- model = QFileSystemModel() model.setRootPath('This PC') model.setReadOnly(False) model.setIconProvider( JSONFileIconProvider('file_view_icons.json') ) model.setFilter( QDir.AllEntries | QDir.NoDotAndDotDot | QDir.AllDirs | QDir.Hidden ) self._model = FileSystemProxyModel() self._model.setSourceModel(model) # --- setup the tree view --- self._view = QTreeView() self._view.setModel(self._model) self._view.setSelectionMode(QTreeView.ExtendedSelection) self._view.activated.connect(self._handle_activated_index) self._view.setSortingEnabled(True) self._view.setHeaderHidden(True) self._view.setExpandsOnDoubleClick(False) self._view.sortByColumn(0, Qt.AscendingOrder) # Setup drag and drop. self._view.setDragDropMode(QTreeView.DragDrop) self._view.setDefaultDropAction(Qt.MoveAction) # Setup the columns that show. self._view.hideColumn(1) self._view.hideColumn(2) self._view.hideColumn(3) # Note that the double click trigger cannot be in this list, otherwise it flashes in edit # mode before opening the file self._view.setEditTriggers( QTreeView.SelectedClicked | QTreeView.EditKeyPressed) # setup the context menu self._view.setContextMenuPolicy(Qt.CustomContextMenu) self._view.customContextMenuRequested.connect(self._context_menu) # Unselect items on collapse. self._view.collapsed.connect(self._view.clearSelection) # --- setup the current root path label --- self._root_edit = PathEdit() self._root_edit.new_path.connect(self._set_root_path) # --- setup the tool bar --- tool_bar = QToolBar() new_file_action = QAction('New File', self) tool_bar.addAction(new_file_action) tool_bar.widgetForAction(new_file_action).setObjectName('new_file') new_file_action.triggered.connect(self._create_file) new_directory_action = QAction('New Directory', self) tool_bar.addAction(new_directory_action) tool_bar.widgetForAction(new_directory_action).setObjectName('new_directory') new_directory_action.triggered.connect(self._create_directory) close_action = QAction('Close', self) tool_bar.addAction(close_action) self._close_widget = tool_bar.widgetForAction(close_action) self._close_widget.setObjectName('close_root') close_action.triggered.connect(self.close_request.emit) # --- setup the layout --- main_layout = QVBoxLayout() main_layout.setContentsMargins(0, 0, 0, 0) main_layout.setSpacing(0) path_layout = QHBoxLayout() path_layout.setContentsMargins(0, 0, 0, 0) main_layout.setSpacing(0) path_layout.addWidget(self._root_edit) path_layout.addWidget(tool_bar) main_layout.addLayout(path_layout) main_layout.addWidget(self._view) self.setLayout(main_layout) if path is not None: self._set_root_path(path) self._root_edit.update(path) self._settings = None self.update_settings(settings)