diff -r c04e878aa308 -r e4069ddd7dc7 src/eric7/MicroPython/MicroPythonFileManagerWidget.py --- a/src/eric7/MicroPython/MicroPythonFileManagerWidget.py Tue Jan 23 12:21:15 2024 +0100 +++ b/src/eric7/MicroPython/MicroPythonFileManagerWidget.py Wed Jan 24 18:52:50 2024 +0100 @@ -7,6 +7,7 @@ Module implementing a file manager for MicroPython devices. """ +import contextlib import os import shutil @@ -86,6 +87,7 @@ self.openButton.setEnabled(False) self.saveButton.setEnabled(False) + self.saveAsButton.setEnabled(False) self.localFileTreeWidget.header().setSortIndicator( 0, Qt.SortOrder.AscendingOrder @@ -144,6 +146,10 @@ act.setCheckable(True) act.setChecked(Preferences.getMicroPython("ShowHiddenLocal")) act.triggered[bool].connect(self.__localHiddenChanged) + self.__localMenu.addSeparator() + self.__localClearSelectionAct = self.__localMenu.addAction( + self.tr("Clear Selection"), self.__clearLocalSelection + ) self.__deviceMenu = QMenu(self) if not isMicrobitDeviceWithMPy: @@ -177,14 +183,20 @@ self.__deviceMenu.addAction( self.tr("Show Filesystem Info"), self.__showFileSystemInfo ) + self.__deviceMenu.addSeparator() + self.__deviceClearSelectionAct = self.__deviceMenu.addAction( + self.tr("Clear Selection"), self.__clearDeviceSelection + ) def start(self): """ Public method to start the widget. """ + self.__viewmanager = ericApp().getObject("ViewManager") + self.__viewmanager.editorCountChanged.connect(self.__updateSaveButtonStates) + dirname = "" - vm = ericApp().getObject("ViewManager") - aw = vm.activeWindow() + aw = self.__viewmanager.activeWindow() if aw and FileSystemUtilities.isPlainFileName(aw.getFileName()): dirname = os.path.dirname(aw.getFileName()) if not dirname: @@ -193,22 +205,38 @@ or Preferences.getMultiProject("Workspace") or os.path.expanduser("~") ) - self.__listLocalFiles(dirname) + self.__listLocalFiles(dirname=dirname) if self.__repl.deviceSupportsLocalFileAccess(): dirname = self.__repl.getDeviceWorkspace() if dirname: - self.__listLocalFiles(dirname, True) + self.__listLocalFiles(dirname=dirname, localDevice=True) return # list files via device script + self.__expandedDeviceEntries = [] self.__fileManager.pwd() def stop(self): """ Public method to stop the widget. """ - pass + self.__viewmanager.editorCountChanged.disconnect(self.__updateSaveButtonStates) + + @pyqtSlot() + def __updateSaveButtonStates(self): + """ + Private slot to update the enabled state of the save buttons. + """ + enable = bool(len(self.deviceFileTreeWidget.selectedItems())) + if enable: + enable &= not ( + self.deviceFileTreeWidget.selectedItems()[0].text(0).endswith("/") + ) + editorsCount = self.__viewmanager.getOpenEditorsCount() + + self.saveButton.setEnabled(enable and bool(editorsCount)) + self.saveAsButton.setEnabled(bool(editorsCount)) @pyqtSlot(str, str) def __handleError(self, method, error): @@ -240,6 +268,25 @@ self.deviceCwd.setText(dirname) self.__newDeviceList() + def __findDirectoryItem(self, dirPath, fileTreeWidget): + """ + Private method to find a file tree item for the given path. + + @param dirPath path to be searched for + @type str + @param fileTreeWidget reference to the file list to be searched + @type QTreeWidget + @return reference to the item for the path + @rtype QTreeWidgetItem + """ + itm = fileTreeWidget.topLevelItem(0) + while itm is not None: + if itm.data(0, Qt.ItemDataRole.UserRole) == dirPath: + return itm + itm = fileTreeWidget.itemBelow(itm) + + return None + @pyqtSlot(tuple) def __handleLongListFiles(self, filesList): """ @@ -249,36 +296,71 @@ for each directory entry @type tuple of (str, str, str, str) """ - self.deviceFileTreeWidget.clear() - for name, mode, size, dateTime in filesList: - itm = QTreeWidgetItem( - self.deviceFileTreeWidget, [name, mode, size, dateTime] + if filesList: + dirPath = os.path.dirname(filesList[0][-1]) + dirItem = ( + self.__findDirectoryItem(dirPath, self.deviceFileTreeWidget) + if dirPath != self.deviceCwd.text() + else None ) - itm.setTextAlignment(1, Qt.AlignmentFlag.AlignHCenter) - itm.setTextAlignment(2, Qt.AlignmentFlag.AlignRight) + + if dirItem: + dirItem.takeChildren() + else: + self.deviceFileTreeWidget.clear() + + for name, mode, size, dateTime, filePath in filesList: + itm = QTreeWidgetItem( + self.deviceFileTreeWidget if dirItem is None else dirItem, + [name, mode, size, dateTime], + ) + itm.setTextAlignment(1, Qt.AlignmentFlag.AlignHCenter) + itm.setTextAlignment(2, Qt.AlignmentFlag.AlignRight) + itm.setData(0, Qt.ItemDataRole.UserRole, filePath) + if name.endswith("/"): + itm.setChildIndicatorPolicy( + QTreeWidgetItem.ChildIndicatorPolicy.ShowIndicator + ) self.deviceFileTreeWidget.header().resizeSections( QHeaderView.ResizeMode.ResizeToContents ) - def __listLocalFiles(self, dirname="", localDevice=False): + if self.__expandedDeviceEntries: + dirPath = self.__expandedDeviceEntries.pop(0) + dirItem = self.__findDirectoryItem(dirPath, self.deviceFileTreeWidget) + if dirItem: + dirItem.setExpanded(True) + + def __listLocalFiles(self, dirname="", localDevice=False, parentItem=None): """ Private method to populate the local files list. - @param dirname name of the local directory to be listed - @type str + @param dirname name of the local directory to be listed (defaults to "") + @type str (optional) @param localDevice flag indicating device access via local file system - @type bool + (defaults to False) + @type bool (optional) + @param parentItem reference to the parent item (defaults to None) + @type QTreeWidgetItem (optional) """ - if not dirname: - dirname = os.getcwd() - if dirname != os.sep and dirname.endswith(os.sep): - dirname = dirname[:-1] - if localDevice: - self.deviceCwd.setText(dirname) - showHidden = Preferences.getMicroPython("ShowHiddenDevice") + if parentItem: + dirname = parentItem.data(0, Qt.ItemDataRole.UserRole) + showHidden = ( + Preferences.getMicroPython("ShowHiddenDevice") + if localDevice + else Preferences.getMicroPython("ShowHiddenLocal") + ) else: - self.localCwd.setText(dirname) - showHidden = Preferences.getMicroPython("ShowHiddenLocal") + if not dirname: + dirname = os.getcwd() + if dirname != os.sep and dirname.endswith(os.sep): + dirname = dirname[:-1] + if localDevice: + self.deviceCwd.setText(dirname) + showHidden = Preferences.getMicroPython("ShowHiddenDevice") + else: + self.localCwd.setText(dirname) + showHidden = Preferences.getMicroPython("ShowHiddenLocal") filesStatList = listdirStat(dirname, showHidden=showHidden) filesList = [ @@ -287,19 +369,61 @@ mode2string(s[0]), str(s[6]), mtime2string(s[8]), + os.path.join(dirname, f), ) for f, s in filesStatList ] fileTreeWidget = ( self.deviceFileTreeWidget if localDevice else self.localFileTreeWidget ) - fileTreeWidget.clear() + if parentItem: + parentItem.takeChildren() + else: + fileTreeWidget.clear() + parentItem = fileTreeWidget for item in filesList: - itm = QTreeWidgetItem(fileTreeWidget, item) + itm = QTreeWidgetItem(parentItem, item[:4]) itm.setTextAlignment(1, Qt.AlignmentFlag.AlignHCenter) itm.setTextAlignment(2, Qt.AlignmentFlag.AlignRight) + itm.setData(0, Qt.ItemDataRole.UserRole, item[4]) + if os.path.isdir(item[4]): + itm.setChildIndicatorPolicy( + QTreeWidgetItem.ChildIndicatorPolicy.ShowIndicator + ) fileTreeWidget.header().resizeSections(QHeaderView.ResizeMode.ResizeToContents) + def __repopulateLocalFilesList(self, dirname="", localDevice=False): + """ + Private method to re-populate the local files tree. + + @param dirname name of the local directory to be listed (defaults to "") + @type str (optional) + @param localDevice flag indicating device access via local file system + (defaults to False) + @type bool (optional) + """ + fileTreeWidget = ( + self.deviceFileTreeWidget if localDevice else self.localFileTreeWidget + ) + + # Step 1: record all expanded directories + expanded = [] + itm = fileTreeWidget.topLevelItem(0) + while itm: + if itm.isExpanded(): + expanded.append(itm.data(0, Qt.ItemDataRole.UserRole)) + itm = fileTreeWidget.itemBelow(itm) + + # Step 2: re-populate the top level directory + self.__listLocalFiles(dirname=dirname, localDevice=localDevice) + + # Step 3: re-populate expanded directories + itm = fileTreeWidget.topLevelItem(0) + while itm: + if itm.data(0, Qt.ItemDataRole.UserRole) in expanded: + itm.setExpanded(True) + itm = fileTreeWidget.itemBelow(itm) + @pyqtSlot(QTreeWidgetItem, int) def on_localFileTreeWidget_itemActivated(self, item, column): """ @@ -313,19 +437,21 @@ @param column column of the activation @type int """ - name = os.path.join(self.localCwd.text(), item.text(0)) - if name.endswith("/"): + name = item.data(0, Qt.ItemDataRole.UserRole) + if item.text(0).endswith("/"): # directory names end with a '/' - self.__listLocalFiles(name[:-1]) + self.__listLocalFiles(dirname=name) elif MimeTypes.isTextFile(name): - ericApp().getObject("ViewManager").getEditor(name) + self.__viewmanager.getEditor(name) @pyqtSlot() def on_localFileTreeWidget_itemSelectionChanged(self): """ Private slot handling a change of selection in the local pane. """ - enable = bool(len(self.localFileTreeWidget.selectedItems())) + enable = bool(self.localFileTreeWidget.selectedItems()) + self.__localClearSelectionAct.setEnabled(enable) + if enable: enable &= not ( self.localFileTreeWidget.selectedItems()[0].text(0).endswith("/") @@ -333,6 +459,18 @@ self.putButton.setEnabled(enable) self.putAsButton.setEnabled(enable) + @pyqtSlot(QTreeWidgetItem) + def on_localFileTreeWidget_itemExpanded(self, item): + """ + Private slot handling the expansion of a local directory item. + + @param item reference to the directory item + @type QTreeWidgetItem + """ + if item.childCount() == 0: + # it was not populated yet + self.__listLocalFiles(parentItem=item) + @pyqtSlot(str) def on_localCwd_textChanged(self, cwd): """ @@ -350,7 +488,7 @@ """ cwd = self.localCwd.text() dirname = os.path.dirname(cwd) - self.__listLocalFiles(dirname) + self.__listLocalFiles(dirname=dirname) @pyqtSlot() def on_localHomeButton_clicked(self): @@ -362,7 +500,7 @@ or Preferences.getMultiProject("Workspace") or os.path.expanduser("~") ) - self.__listLocalFiles(dirname) + self.__listLocalFiles(dirname=dirname) @pyqtSlot() def on_localReloadButton_clicked(self): @@ -370,7 +508,7 @@ Private slot to reload the local list. """ dirname = self.localCwd.text() - self.__listLocalFiles(dirname) + self.__repopulateLocalFilesList(dirname=dirname) @pyqtSlot(QTreeWidgetItem, int) def on_deviceFileTreeWidget_itemActivated(self, item, column): @@ -385,11 +523,11 @@ @param column column of the activation @type int """ + name = item.data(0, Qt.ItemDataRole.UserRole) if self.__repl.deviceSupportsLocalFileAccess(): - name = os.path.join(self.deviceCwd.text(), item.text(0)) - if name.endswith("/"): + if item.text(0).endswith("/"): # directory names end with a '/' - self.__listLocalFiles(name[:-1], True) + self.__listLocalFiles(dirname=name) else: if not os.path.exists(name): EricMessageBox.warning( @@ -401,25 +539,16 @@ ) return if MimeTypes.isTextFile(name): - ericApp().getObject("ViewManager").getEditor(name) + self.__viewmanager.getEditor(name) else: - cwd = self.deviceCwd.text() - if cwd: - name = ( - cwd + item.text(0) - if cwd.endswith("/") - else "{0}/{1}".format(cwd, item.text(0)) - ) - else: - name = item.text(0) - if name.endswith("/"): + if item.text(0).endswith("/"): # directory names end with a '/' - self.__fileManager.cd(name[:-1]) + self.__fileManager.cd(name) else: data = self.__fileManager.getData(name) try: text = data.decode(encoding="utf-8") - ericApp().getObject("ViewManager").newEditorWithText( + self.__viewmanager.newEditorWithText( text, fileName=FileSystemUtilities.deviceFileName(name) ) except UnicodeDecodeError: @@ -437,16 +566,38 @@ """ Private slot handling a change of selection in the local pane. """ - enable = bool(len(self.deviceFileTreeWidget.selectedItems())) + enable = bool(self.deviceFileTreeWidget.selectedItems()) + self.__deviceClearSelectionAct.setEnabled(enable) + if enable: enable &= not ( self.deviceFileTreeWidget.selectedItems()[0].text(0).endswith("/") ) + self.getButton.setEnabled(enable) self.getAsButton.setEnabled(enable) self.openButton.setEnabled(enable) - self.saveButton.setEnabled(enable) + + self.__updateSaveButtonStates() + + @pyqtSlot(QTreeWidgetItem) + def on_deviceFileTreeWidget_itemExpanded(self, item): + """ + Private slot handling the expansion of a local directory item. + + @param item reference to the directory item + @type QTreeWidgetItem + """ + if item.childCount() == 0: + # it was not populated yet + if self.__repl.deviceSupportsLocalFileAccess(): + self.__listLocalFiles(localDevice=True, parentItem=item) + else: + self.__fileManager.lls( + item.data(0, Qt.ItemDataRole.UserRole), + showHidden=Preferences.getMicroPython("ShowHiddenDevice"), + ) @pyqtSlot(str) def on_deviceCwd_textChanged(self, cwd): @@ -466,7 +617,7 @@ cwd = self.deviceCwd.text() dirname = os.path.dirname(cwd) if self.__repl.deviceSupportsLocalFileAccess(): - self.__listLocalFiles(dirname, True) + self.__listLocalFiles(dirname=dirname, localDevice=True) else: self.__fileManager.cd(dirname) @@ -478,7 +629,7 @@ if self.__repl.deviceSupportsLocalFileAccess(): dirname = self.__repl.getDeviceWorkspace() if dirname: - self.__listLocalFiles(dirname, True) + self.__listLocalFiles(dirname=dirname, localDevice=True) return # list files via device script @@ -491,28 +642,34 @@ """ dirname = self.deviceCwd.text() if self.__repl.deviceSupportsLocalFileAccess(): - self.__listLocalFiles(dirname, True) + self.__repopulateLocalFilesList(dirname=dirname, localDevice=True) else: if dirname: self.__newDeviceList() else: self.__fileManager.pwd() - def __isFileInList(self, filename, treeWidget): + def __isFileInList(self, filename, parent): """ Private method to check, if a file name is contained in a tree widget. @param filename name of the file to check @type str - @param treeWidget reference to the tree widget to be checked against - @type QTreeWidget + @param parent reference to the parent to be checked against + @type QTreeWidget or QTreeWidgetItem @return flag indicating that the file name is present @rtype bool """ - itemCount = treeWidget.topLevelItemCount() - return itemCount > 0 and any( - treeWidget.topLevelItem(row).text(0) == filename for row in range(itemCount) - ) + if isinstance(parent, QTreeWidgetItem): + itemCount = parent.childCount() + return itemCount > 0 and any( + parent.child(row).text(0) == filename for row in range(itemCount) + ) + else: + itemCount = parent.topLevelItemCount() + return itemCount > 0 and any( + parent.topLevelItem(row).text(0) == filename for row in range(itemCount) + ) @pyqtSlot() def on_putButton_clicked(self, putAs=False): @@ -524,8 +681,9 @@ """ selectedItems = self.localFileTreeWidget.selectedItems() if selectedItems: - filename = selectedItems[0].text(0).strip() - if not filename.endswith("/"): + filepath = selectedItems[0].data(0, Qt.ItemDataRole.UserRole) + filename = os.path.basename(filepath) + if not selectedItems[0].text(0).endswith("/"): # it is really a file if putAs: deviceFilename, ok = QInputDialog.getText( @@ -535,12 +693,28 @@ QLineEdit.EchoMode.Normal, filename, ) - if not ok or not filename: + if not ok or not deviceFilename: return else: deviceFilename = filename - if self.__isFileInList(deviceFilename, self.deviceFileTreeWidget): + selectedDeviceItems = self.deviceFileTreeWidget.selectedItems() + if selectedDeviceItems: + item = selectedDeviceItems[0] + if not item.text(0).endswith("/"): + # it is no directory, take its parent + item = item.parent() + devicePath = ( + self.deviceCwd.text() + if item is None + else item.data(0, Qt.ItemDataRole.UserRole) + ) + deviceParent = item + else: + devicePath = self.deviceCwd.text() + deviceParent = self.deviceFileTreeWidget + + if self.__isFileInList(deviceFilename, deviceParent): # ask for overwrite permission action, resultFilename = confirmOverwrite( deviceFilename, @@ -557,21 +731,16 @@ deviceFilename = os.path.basename(resultFilename) if self.__repl.deviceSupportsLocalFileAccess(): - shutil.copy2( - os.path.join(self.localCwd.text(), filename), - os.path.join(self.deviceCwd.text(), deviceFilename), - ) - self.__listLocalFiles(self.deviceCwd.text(), localDevice=True) + shutil.copy2(filepath, os.path.join(devicePath, deviceFilename)) + self.__listLocalFiles(dirname=devicePath, localDevice=True) else: - deviceCwd = self.deviceCwd.text() - if deviceCwd: - if deviceCwd != "/": - deviceFilename = deviceCwd + "/" + deviceFilename - else: - deviceFilename = "/" + deviceFilename - self.__fileManager.put( - os.path.join(self.localCwd.text(), filename), deviceFilename - ) + if devicePath: + deviceFilename = ( + f"{devicePath}/{deviceFilename}" + if devicePath != "/" + else f"/{devicePath}" + ) + self.__fileManager.put(filepath, deviceFilename) @pyqtSlot() def on_putAsButton_clicked(self): @@ -592,6 +761,7 @@ selectedItems = self.deviceFileTreeWidget.selectedItems() if selectedItems: filename = selectedItems[0].text(0).strip() + deviceFilename = selectedItems[0].data(0, Qt.ItemDataRole.UserRole) if not filename.endswith("/"): # it is really a file if getAs: @@ -607,7 +777,23 @@ else: localFilename = filename - if self.__isFileInList(localFilename, self.localFileTreeWidget): + selectedLocalItems = self.localFileTreeWidget.selectedItems() + if selectedLocalItems: + item = selectedLocalItems[0] + if not item.text(0).endswith("/"): + # it is no directory, take its parent + item = item.parent() + localPath = ( + self.localCwd.text() + if item is None + else item.data(0, Qt.ItemDataRole.UserRole) + ) + localParent = item + else: + localPath = self.localCwd.text() + localParent = self.localFileTreeWidget + + if self.__isFileInList(localFilename, localParent): # ask for overwrite permission action, resultFilename = confirmOverwrite( localFilename, @@ -623,16 +809,16 @@ if self.__repl.deviceSupportsLocalFileAccess(): shutil.copy2( - os.path.join(self.deviceCwd.text(), filename), - os.path.join(self.localCwd.text(), localFilename), + deviceFilename, + os.path.join(localPath, localFilename), ) - self.__listLocalFiles(self.localCwd.text()) + if isinstance(localParent, QTreeWidgetItem): + self.__listLocalFiles(parentItem=localParent) + else: + self.__listLocalFiles(dirname=localPath) else: - deviceCwd = self.deviceCwd.text() - if deviceCwd: - filename = deviceCwd + "/" + filename self.__fileManager.get( - filename, os.path.join(self.localCwd.text(), localFilename) + deviceFilename, os.path.join(localPath, localFilename) ) @pyqtSlot() @@ -653,16 +839,61 @@ @param localFile name of the local file @type str """ - self.__listLocalFiles(self.localCwd.text()) + localPath = os.path.dirname(localFile) + + # find the directory entry associated with the new file + localParent = self.__findDirectoryItem(localPath, self.localFileTreeWidget) + + if localParent: + self.__listLocalFiles(parentItem=localParent) + else: + self.__listLocalFiles(dirname=self.localCwd.text()) @pyqtSlot() def on_syncButton_clicked(self): """ Private slot to synchronize the local directory to the device. """ + # 1. local directory + selectedItems = self.localFileTreeWidget.selectedItems() + if selectedItems: + localName = selectedItems[0].text(0) + if localName.endswith("/"): + localDirPath = selectedItems[0].data(0, Qt.ItemDataRole.UserRole) + else: + # it is not a directory + localDirPath = os.path.dirname( + selectedItems[0].data(0, Qt.ItemDataRole.UserRole) + ) + else: + localName = "" + localDirPath = self.localCwd.text() + + # 2. device directory + selectedItems = self.deviceFileTreeWidget.selectedItems() + if selectedItems: + if not selectedItems[0].text(0).endswith("/"): + # it is not a directory + deviceDirPath = os.path.dirname( + selectedItems[0].data(0, Qt.ItemDataRole.UserRole) + ) + else: + deviceDirPath = selectedItems[0].data(0, Qt.ItemDataRole.UserRole) + else: + if localDirPath == self.localCwd.text(): + # syncronize complete local directory + deviceDirPath = self.deviceCwd.text() + else: + deviceCwd = self.deviceCwd.text() + deviceDirPath = ( + f"{deviceCwd}{localName[:-1]}" + if deviceCwd.endswith("/") + else f"{deviceCwd}/{localName[:-1]}" + ) + self.__fileManager.rsync( - self.localCwd.text(), - self.deviceCwd.text(), + localDirPath, + deviceDirPath, mirror=True, localDevice=self.__repl.deviceSupportsLocalFileAccess(), ) @@ -712,6 +943,15 @@ """ Private slot to initiate a new long list of the device directory. """ + self.__expandedDeviceEntries.clear() + itm = self.deviceFileTreeWidget.topLevelItem(0) + while itm: + if itm.isExpanded(): + self.__expandedDeviceEntries.append( + itm.data(0, Qt.ItemDataRole.UserRole) + ) + itm = self.deviceFileTreeWidget.itemBelow(itm) + self.__fileManager.lls( self.deviceCwd.text(), showHidden=Preferences.getMicroPython("ShowHiddenDevice"), @@ -724,25 +964,17 @@ """ selectedItems = self.deviceFileTreeWidget.selectedItems() if selectedItems: - filename = selectedItems[0].text(0).strip() + name = selectedItems[0].data(0, Qt.ItemDataRole.UserRole) if self.__repl.deviceSupportsLocalFileAccess(): - name = os.path.join(self.deviceCwd.text(), filename) - if not name.endswith("/") and MimeTypes.isTextFile(name): - ericApp().getObject("ViewManager").getEditor(name) + if not selectedItems[0].text(0).endswith("/") and MimeTypes.isTextFile( + name + ): + self.__viewmanager.getEditor(name) else: - cwd = self.deviceCwd.text() - if cwd: - name = ( - cwd + filename - if cwd.endswith("/") - else "{0}/{1}".format(cwd, filename) - ) - else: - name = filename - if not name.endswith("/"): + if not selectedItems[0].text(0).endswith("/"): data = self.__fileManager.getData(name) text = data.decode(encoding="utf-8") - ericApp().getObject("ViewManager").newEditorWithText( + self.__viewmanager.newEditorWithText( text, "Python3", FileSystemUtilities.deviceFileName(name) ) @@ -754,12 +986,14 @@ @param saveAs flag indicating to save the file with a new name @type bool """ - aw = ericApp().getObject("ViewManager").activeWindow() + aw = self.__viewmanager.activeWindow() if aw: selectedItems = self.deviceFileTreeWidget.selectedItems() + if selectedItems: - filename = selectedItems[0].text(0).strip() - if filename.endswith("/"): + filepath = selectedItems[0].data(0, Qt.ItemDataRole.UserRole) + filename = os.path.basename(filepath) + if selectedItems[0].text(0).endswith("/"): saveAs = True else: saveAs = True @@ -784,7 +1018,22 @@ if editorFileName != filename: saveAs = True - if saveAs and self.__isFileInList(filename, self.deviceFileTreeWidget): + if selectedItems: + item = selectedItems[0] + if not item.text(0).endswith("/"): + # it is no directory, take its parent + item = item.parent() + devicePath = ( + self.deviceCwd.text() + if item is None + else item.data(0, Qt.ItemDataRole.UserRole) + ) + deviceParent = item + else: + devicePath = self.deviceCwd.text() + deviceParent = self.deviceFileTreeWidget + + if saveAs and self.__isFileInList(filename, deviceParent): # ask for overwrite permission action, resultFilename = confirmOverwrite( filename, @@ -800,28 +1049,26 @@ text = aw.text() if self.__repl.deviceSupportsLocalFileAccess(): - filename = os.path.join(self.deviceCwd.text(), filename) + filename = os.path.join(devicePath, filename) os.makedirs(os.path.dirname(filename), exist_ok=True) with open(filename, "w") as f: f.write(text) self.__newDeviceList() aw.setFileName(filename) else: - if not filename.startswith("/"): - deviceCwd = self.deviceCwd.text() - if deviceCwd: - filename = ( - deviceCwd + "/" + filename - if deviceCwd != "/" - else "/" + filename - ) + filename = ( + f"{devicePath}/{filename}" + if devicePath != "/" + else f"/{devicePath}" + ) dirname = filename.rsplit("/", 1)[0] self.__fileManager.makedirs(dirname) self.__fileManager.putData(filename, text.encode("utf-8")) aw.setFileName(FileSystemUtilities.deviceFileName(filename)) aw.setModified(False) - aw.resetOnlineChangeTraceInfo() + with contextlib.suppress(AttributeError): + aw.resetOnlineChangeTraceInfo() @pyqtSlot() def on_saveAsButton_clicked(self): @@ -846,7 +1093,8 @@ if hasSelection: name = self.localFileTreeWidget.selectedItems()[0].text(0) isDir = name.endswith("/") - isFile = not isDir + isLink = name.endswith("@") + isFile = not (isDir or isLink) else: isDir = False isFile = False @@ -865,20 +1113,31 @@ @type bool """ cwdWidget = self.deviceCwd if localDevice else self.localCwd + fileTreeWidget = ( + self.deviceFileTreeWidget if localDevice else self.localFileTreeWidget + ) + if fileTreeWidget.selectedItems(): + defaultPath = fileTreeWidget.selectedItems()[0].data( + 0, Qt.ItemDataRole.UserRole + ) + if not os.path.isdir(defaultPath): + defaultPath = os.path.dirname(defaultPath) + else: + defaultPath = cwdWidget.text() dirPath, ok = EricPathPickerDialog.getStrPath( self, self.tr("Change Directory"), self.tr("Select Directory"), EricPathPickerModes.DIRECTORY_SHOW_FILES_MODE, - strPath=cwdWidget.text(), - defaultDirectory=cwdWidget.text(), + strPath=defaultPath, + defaultDirectory=defaultPath, ) if ok and dirPath: if not os.path.isabs(dirPath): dirPath = os.path.join(cwdWidget.text(), dirPath) cwdWidget.setText(dirPath) - self.__listLocalFiles(dirPath, localDevice=localDevice) + self.__listLocalFiles(dirname=dirPath, localDevice=localDevice) @pyqtSlot() def __createLocalDirectory(self, localDevice=False): @@ -889,6 +1148,19 @@ @type bool """ cwdWidget = self.deviceCwd if localDevice else self.localCwd + fileTreeWidget = ( + self.deviceFileTreeWidget if localDevice else self.localFileTreeWidget + ) + + if fileTreeWidget.selectedItems(): + localItem = fileTreeWidget.selectedItems()[0] + defaultPath = localItem.data(0, Qt.ItemDataRole.UserRole) + if not os.path.isdir(defaultPath): + defaultPath = os.path.dirname(defaultPath) + localItem = localItem.parent() + else: + defaultPath = cwdWidget.text() + localItem = None dirPath, ok = QInputDialog.getText( self, @@ -897,10 +1169,15 @@ QLineEdit.EchoMode.Normal, ) if ok and dirPath: - dirPath = os.path.join(cwdWidget.text(), dirPath) + dirPath = os.path.join(defaultPath, dirPath) try: os.mkdir(dirPath) - self.__listLocalFiles(cwdWidget.text(), localDevice=localDevice) + if localItem: + self.__listLocalFiles(localDevice=localDevice, parentItem=localItem) + else: + self.__listLocalFiles( + dirname=cwdWidget.text(), localDevice=localDevice + ) except OSError as exc: EricMessageBox.critical( self, @@ -926,9 +1203,10 @@ cwdWidget = self.localCwd fileTreeWidget = self.localFileTreeWidget - if bool(len(fileTreeWidget.selectedItems())): - name = fileTreeWidget.selectedItems()[0].text(0) - dirname = os.path.join(cwdWidget.text(), name[:-1]) + if bool(fileTreeWidget.selectedItems()): + localItem = fileTreeWidget.selectedItems()[0] + parentItem = localItem.parent() + dirname = localItem.data(0, Qt.ItemDataRole.UserRole) dlg = DeleteFilesConfirmationDialog( self, self.tr("Delete Directory Tree"), @@ -938,7 +1216,14 @@ if dlg.exec() == QDialog.DialogCode.Accepted: try: shutil.rmtree(dirname) - self.__listLocalFiles(cwdWidget.text(), localDevice=localDevice) + if parentItem: + self.__listLocalFiles( + localDevice=localDevice, parentItem=parentItem + ) + else: + self.__listLocalFiles( + dirname=cwdWidget.text(), localDevice=localDevice + ) except Exception as exc: EricMessageBox.critical( self, @@ -965,8 +1250,9 @@ fileTreeWidget = self.localFileTreeWidget if bool(len(fileTreeWidget.selectedItems())): - name = fileTreeWidget.selectedItems()[0].text(0) - filename = os.path.join(cwdWidget.text(), name) + localItem = fileTreeWidget.selectedItems()[0] + parentItem = localItem.parent() + filename = localItem.data(0, Qt.ItemDataRole.UserRole) dlg = DeleteFilesConfirmationDialog( self, self.tr("Delete File"), @@ -976,7 +1262,14 @@ if dlg.exec() == QDialog.DialogCode.Accepted: try: os.remove(filename) - self.__listLocalFiles(cwdWidget.text(), localDevice=localDevice) + if parentItem: + self.__listLocalFiles( + localDevice=localDevice, parentItem=parentItem + ) + else: + self.__listLocalFiles( + dirname=cwdWidget.text(), localDevice=localDevice + ) except OSError as exc: EricMessageBox.critical( self, @@ -996,16 +1289,13 @@ (defaults to False) @type bool (optional) """ - if localDevice: - cwdWidget = self.deviceCwd - fileTreeWidget = self.deviceFileTreeWidget - else: - cwdWidget = self.localCwd - fileTreeWidget = self.localFileTreeWidget + fileTreeWidget = ( + self.deviceFileTreeWidget if localDevice else self.localFileTreeWidget + ) if bool(len(fileTreeWidget.selectedItems())): - name = fileTreeWidget.selectedItems()[0].text(0) - filename = os.path.join(cwdWidget.text(), name) + localItem = fileTreeWidget.selectedItems()[0] + filename = localItem.data(0, Qt.ItemDataRole.UserRole) newname, ok = QInputDialog.getText( self, self.tr("Rename File"), @@ -1043,6 +1333,14 @@ Preferences.setMicroPython("ShowHiddenLocal", checked) self.on_localReloadButton_clicked() + @pyqtSlot() + def __clearLocalSelection(self): + """ + Private slot to clear the local selection. + """ + for item in self.localFileTreeWidget.selectedItems()[:]: + item.setSelected(False) + ################################################################## ## Context menu methods for the device files below ################################################################## @@ -1082,12 +1380,22 @@ if self.__repl.deviceSupportsLocalFileAccess(): self.__changeLocalDirectory(True) else: + selectedItems = self.deviceFileTreeWidget.selectedItems() + if selectedItems: + item = selectedItems[0] + dirName = ( + item.data(0, Qt.ItemDataRole.UserRole) + if item.text(0).endswith("/") + else os.path.dirname(item.data(0, Qt.ItemDataRole.UserRole)) + ) + else: + dirName = self.deviceCwd.text() dirPath, ok = QInputDialog.getText( self, self.tr("Change Directory"), self.tr("Enter the directory path on the device:"), QLineEdit.EchoMode.Normal, - self.deviceCwd.text(), + dirName, ) if ok and dirPath: if not dirPath.startswith("/"): @@ -1102,11 +1410,22 @@ if self.__repl.deviceSupportsLocalFileAccess(): self.__createLocalDirectory(True) else: + selectedItems = self.deviceFileTreeWidget.selectedItems() + if selectedItems: + item = selectedItems[0] + defaultPath = ( + item.data(0, Qt.ItemDataRole.UserRole) + if item.text(0).endswith("/") + else os.path.dirname(item.data(0, Qt.ItemDataRole.UserRole)) + ) + else: + defaultPath = self.deviceCwd.text() dirPath, ok = QInputDialog.getText( self, self.tr("Create Directory"), self.tr("Enter directory name:"), QLineEdit.EchoMode.Normal, + defaultPath, ) if ok and dirPath: self.__fileManager.mkdir(dirPath) @@ -1119,16 +1438,10 @@ if self.__repl.deviceSupportsLocalFileAccess(): self.__deleteLocalDirectoryTree(True) else: - if bool(len(self.deviceFileTreeWidget.selectedItems())): - name = self.deviceFileTreeWidget.selectedItems()[0].text(0) - cwd = self.deviceCwd.text() - if cwd: - if cwd != "/": - dirname = cwd + "/" + name[:-1] - else: - dirname = "/" + name[:-1] - else: - dirname = name[:-1] + if bool(self.deviceFileTreeWidget.selectedItems()): + dirname = self.deviceFileTreeWidget.selectedItems()[0].data( + 0, Qt.ItemDataRole.UserRole + ) dlg = DeleteFilesConfirmationDialog( self, self.tr("Delete Directory"), @@ -1148,15 +1461,9 @@ self.__deleteLocalDirectoryTree(True) else: if bool(len(self.deviceFileTreeWidget.selectedItems())): - name = self.deviceFileTreeWidget.selectedItems()[0].text(0) - cwd = self.deviceCwd.text() - if cwd: - if cwd != "/": - dirname = cwd + "/" + name[:-1] - else: - dirname = "/" + name[:-1] - else: - dirname = name[:-1] + dirname = self.deviceFileTreeWidget.selectedItems()[0].data( + 0, Qt.ItemDataRole.UserRole + ) dlg = DeleteFilesConfirmationDialog( self, self.tr("Delete Directory Tree"), @@ -1174,16 +1481,10 @@ if self.__repl.deviceSupportsLocalFileAccess(): self.__deleteLocalFile(True) else: - if bool(len(self.deviceFileTreeWidget.selectedItems())): - name = self.deviceFileTreeWidget.selectedItems()[0].text(0) - dirname = self.deviceCwd.text() - if dirname: - if dirname != "/": - filename = dirname + "/" + name - else: - filename = "/" + name - else: - filename = name + if bool(self.deviceFileTreeWidget.selectedItems()): + filename = self.deviceFileTreeWidget.selectedItems()[0].data( + 0, Qt.ItemDataRole.UserRole + ) dlg = DeleteFilesConfirmationDialog( self, self.tr("Delete File"), @@ -1201,16 +1502,10 @@ if self.__repl.deviceSupportsLocalFileAccess(): self.__renameLocalFile(True) else: - if bool(len(self.deviceFileTreeWidget.selectedItems())): - name = self.deviceFileTreeWidget.selectedItems()[0].text(0) - dirname = self.deviceCwd.text() - if dirname: - if dirname != "/": - filename = dirname + "/" + name - else: - filename = "/" + name - else: - filename = name + if bool(self.deviceFileTreeWidget.selectedItems()): + filename = self.deviceFileTreeWidget.selectedItems()[0].data( + 0, Qt.ItemDataRole.UserRole + ) newname, ok = QInputDialog.getText( self, self.tr("Rename File"), @@ -1271,3 +1566,11 @@ "<p>No file systems or file system information available.</p>" ) EricMessageBox.information(self, self.tr("Filesystem Information"), msg) + + @pyqtSlot() + def __clearDeviceSelection(self): + """ + Private slot to clear the local selection. + """ + for item in self.deviceFileTreeWidget.selectedItems()[:]: + item.setSelected(False)