|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2019 - 2022 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing a file manager for MicroPython devices. |
|
8 """ |
|
9 |
|
10 import os |
|
11 import shutil |
|
12 |
|
13 from PyQt6.QtCore import pyqtSlot, Qt, QPoint |
|
14 from PyQt6.QtWidgets import ( |
|
15 QWidget, QTreeWidgetItem, QHeaderView, QMenu, QInputDialog, QLineEdit, |
|
16 QDialog |
|
17 ) |
|
18 |
|
19 from EricWidgets import EricMessageBox, EricPathPickerDialog |
|
20 from EricWidgets.EricPathPicker import EricPathPickerModes |
|
21 from EricWidgets.EricFileSaveConfirmDialog import confirmOverwrite |
|
22 from EricWidgets.EricApplication import ericApp |
|
23 |
|
24 from .Ui_MicroPythonFileManagerWidget import Ui_MicroPythonFileManagerWidget |
|
25 |
|
26 from .MicroPythonFileManager import MicroPythonFileManager |
|
27 from .MicroPythonFileSystemUtilities import ( |
|
28 mtime2string, mode2string, decoratedName, listdirStat |
|
29 ) |
|
30 |
|
31 from UI.DeleteFilesConfirmationDialog import DeleteFilesConfirmationDialog |
|
32 |
|
33 import UI.PixmapCache |
|
34 import Preferences |
|
35 import Utilities |
|
36 import Globals |
|
37 |
|
38 |
|
39 class MicroPythonFileManagerWidget(QWidget, Ui_MicroPythonFileManagerWidget): |
|
40 """ |
|
41 Class implementing a file manager for MicroPython devices. |
|
42 """ |
|
43 def __init__(self, commandsInterface, deviceWithLocalAccess, parent=None): |
|
44 """ |
|
45 Constructor |
|
46 |
|
47 @param commandsInterface reference to the commands interface object |
|
48 @type MicroPythonCommandsInterface |
|
49 @param deviceWithLocalAccess flag indicating the device supports file |
|
50 access via a local directory |
|
51 @type bool |
|
52 @param parent reference to the parent widget |
|
53 @type QWidget |
|
54 """ |
|
55 super().__init__(parent) |
|
56 self.setupUi(self) |
|
57 |
|
58 self.__repl = parent |
|
59 self.__deviceWithLocalAccess = deviceWithLocalAccess |
|
60 |
|
61 self.syncButton.setIcon(UI.PixmapCache.getIcon("2rightarrow")) |
|
62 self.putButton.setIcon(UI.PixmapCache.getIcon("1rightarrow")) |
|
63 self.putAsButton.setIcon(UI.PixmapCache.getIcon("putAs")) |
|
64 self.getButton.setIcon(UI.PixmapCache.getIcon("1leftarrow")) |
|
65 self.getAsButton.setIcon(UI.PixmapCache.getIcon("getAs")) |
|
66 self.localUpButton.setIcon(UI.PixmapCache.getIcon("1uparrow")) |
|
67 self.localHomeButton.setIcon(UI.PixmapCache.getIcon("home")) |
|
68 self.localReloadButton.setIcon(UI.PixmapCache.getIcon("reload")) |
|
69 self.deviceUpButton.setIcon(UI.PixmapCache.getIcon("1uparrow")) |
|
70 self.deviceHomeButton.setIcon(UI.PixmapCache.getIcon("home")) |
|
71 self.deviceReloadButton.setIcon(UI.PixmapCache.getIcon("reload")) |
|
72 |
|
73 self.deviceUpButton.setEnabled(not self.__repl.isMicrobit()) |
|
74 self.deviceHomeButton.setEnabled(not self.__repl.isMicrobit()) |
|
75 |
|
76 self.putButton.setEnabled(False) |
|
77 self.putAsButton.setEnabled(False) |
|
78 self.getButton.setEnabled(False) |
|
79 self.getAsButton.setEnabled(False) |
|
80 |
|
81 self.localFileTreeWidget.header().setSortIndicator( |
|
82 0, Qt.SortOrder.AscendingOrder) |
|
83 self.deviceFileTreeWidget.header().setSortIndicator( |
|
84 0, Qt.SortOrder.AscendingOrder) |
|
85 |
|
86 self.__progressInfoDialog = None |
|
87 self.__fileManager = MicroPythonFileManager(commandsInterface, self) |
|
88 |
|
89 self.__fileManager.longListFiles.connect(self.__handleLongListFiles) |
|
90 self.__fileManager.currentDir.connect(self.__handleCurrentDir) |
|
91 self.__fileManager.currentDirChanged.connect(self.__handleCurrentDir) |
|
92 self.__fileManager.putFileDone.connect(self.__newDeviceList) |
|
93 self.__fileManager.getFileDone.connect(self.__handleGetDone) |
|
94 self.__fileManager.rsyncDone.connect(self.__handleRsyncDone) |
|
95 self.__fileManager.rsyncProgressMessage.connect( |
|
96 self.__handleRsyncProgressMessage) |
|
97 self.__fileManager.removeDirectoryDone.connect(self.__newDeviceList) |
|
98 self.__fileManager.createDirectoryDone.connect(self.__newDeviceList) |
|
99 self.__fileManager.deleteFileDone.connect(self.__newDeviceList) |
|
100 self.__fileManager.fsinfoDone.connect(self.__fsInfoResultReceived) |
|
101 |
|
102 self.__fileManager.error.connect(self.__handleError) |
|
103 |
|
104 self.localFileTreeWidget.customContextMenuRequested.connect( |
|
105 self.__showLocalContextMenu) |
|
106 self.deviceFileTreeWidget.customContextMenuRequested.connect( |
|
107 self.__showDeviceContextMenu) |
|
108 |
|
109 self.__localMenu = QMenu(self) |
|
110 self.__localMenu.addAction(self.tr("Change Directory"), |
|
111 self.__changeLocalDirectory) |
|
112 self.__localMenu.addAction( |
|
113 self.tr("Create Directory"), self.__createLocalDirectory) |
|
114 self.__localDelDirTreeAct = self.__localMenu.addAction( |
|
115 self.tr("Delete Directory Tree"), self.__deleteLocalDirectoryTree) |
|
116 self.__localMenu.addSeparator() |
|
117 self.__localDelFileAct = self.__localMenu.addAction( |
|
118 self.tr("Delete File"), self.__deleteLocalFile) |
|
119 self.__localMenu.addSeparator() |
|
120 act = self.__localMenu.addAction(self.tr("Show Hidden Files")) |
|
121 act.setCheckable(True) |
|
122 act.setChecked(Preferences.getMicroPython("ShowHiddenLocal")) |
|
123 act.triggered[bool].connect(self.__localHiddenChanged) |
|
124 |
|
125 self.__deviceMenu = QMenu(self) |
|
126 if not self.__repl.isMicrobit(): |
|
127 self.__deviceMenu.addAction( |
|
128 self.tr("Change Directory"), self.__changeDeviceDirectory) |
|
129 self.__deviceMenu.addAction( |
|
130 self.tr("Create Directory"), self.__createDeviceDirectory) |
|
131 if not self.__deviceWithLocalAccess: |
|
132 self.__devDelDirAct = self.__deviceMenu.addAction( |
|
133 self.tr("Delete Directory"), self.__deleteDeviceDirectory) |
|
134 self.__devDelDirTreeAct = self.__deviceMenu.addAction( |
|
135 self.tr("Delete Directory Tree"), |
|
136 self.__deleteDeviceDirectoryTree) |
|
137 self.__deviceMenu.addSeparator() |
|
138 self.__devDelFileAct = self.__deviceMenu.addAction( |
|
139 self.tr("Delete File"), self.__deleteDeviceFile) |
|
140 self.__deviceMenu.addSeparator() |
|
141 act = self.__deviceMenu.addAction(self.tr("Show Hidden Files")) |
|
142 act.setCheckable(True) |
|
143 act.setChecked(Preferences.getMicroPython("ShowHiddenDevice")) |
|
144 act.triggered[bool].connect(self.__deviceHiddenChanged) |
|
145 if not parent.isMicrobit(): |
|
146 self.__deviceMenu.addSeparator() |
|
147 self.__deviceMenu.addAction( |
|
148 self.tr("Show Filesystem Info"), self.__showFileSystemInfo) |
|
149 |
|
150 def start(self): |
|
151 """ |
|
152 Public method to start the widget. |
|
153 """ |
|
154 dirname = "" |
|
155 vm = ericApp().getObject("ViewManager") |
|
156 aw = vm.activeWindow() |
|
157 if aw: |
|
158 dirname = os.path.dirname(aw.getFileName()) |
|
159 if not dirname: |
|
160 dirname = ( |
|
161 Preferences.getMicroPython("MpyWorkspace") or |
|
162 Preferences.getMultiProject("Workspace") or |
|
163 os.path.expanduser("~") |
|
164 ) |
|
165 self.__listLocalFiles(dirname) |
|
166 |
|
167 if self.__deviceWithLocalAccess: |
|
168 dirname = self.__repl.getDeviceWorkspace() |
|
169 if dirname: |
|
170 self.__listLocalFiles(dirname, True) |
|
171 return |
|
172 |
|
173 # list files via device script |
|
174 self.__fileManager.pwd() |
|
175 |
|
176 def stop(self): |
|
177 """ |
|
178 Public method to stop the widget. |
|
179 """ |
|
180 pass |
|
181 |
|
182 @pyqtSlot(str, str) |
|
183 def __handleError(self, method, error): |
|
184 """ |
|
185 Private slot to handle errors. |
|
186 |
|
187 @param method name of the method the error occured in |
|
188 @type str |
|
189 @param error error message |
|
190 @type str |
|
191 """ |
|
192 EricMessageBox.warning( |
|
193 self, |
|
194 self.tr("Error handling device"), |
|
195 self.tr("<p>There was an error communicating with the connected" |
|
196 " device.</p><p>Method: {0}</p><p>Message: {1}</p>") |
|
197 .format(method, error)) |
|
198 |
|
199 @pyqtSlot(str) |
|
200 def __handleCurrentDir(self, dirname): |
|
201 """ |
|
202 Private slot to handle a change of the current directory of the device. |
|
203 |
|
204 @param dirname name of the current directory |
|
205 @type str |
|
206 """ |
|
207 self.deviceCwd.setText(dirname) |
|
208 self.__newDeviceList() |
|
209 |
|
210 @pyqtSlot(tuple) |
|
211 def __handleLongListFiles(self, filesList): |
|
212 """ |
|
213 Private slot to receive a long directory listing. |
|
214 |
|
215 @param filesList tuple containing tuples with name, mode, size and time |
|
216 for each directory entry |
|
217 @type tuple of (str, str, str, str) |
|
218 """ |
|
219 self.deviceFileTreeWidget.clear() |
|
220 for name, mode, size, dateTime in filesList: |
|
221 itm = QTreeWidgetItem(self.deviceFileTreeWidget, |
|
222 [name, mode, size, dateTime]) |
|
223 itm.setTextAlignment(1, Qt.AlignmentFlag.AlignHCenter) |
|
224 itm.setTextAlignment(2, Qt.AlignmentFlag.AlignRight) |
|
225 self.deviceFileTreeWidget.header().resizeSections( |
|
226 QHeaderView.ResizeMode.ResizeToContents) |
|
227 |
|
228 def __listLocalFiles(self, dirname="", localDevice=False): |
|
229 """ |
|
230 Private method to populate the local files list. |
|
231 |
|
232 @param dirname name of the local directory to be listed |
|
233 @type str |
|
234 @param localDevice flag indicating device access via local file system |
|
235 @type bool |
|
236 """ |
|
237 if not dirname: |
|
238 dirname = os.getcwd() |
|
239 if dirname.endswith(os.sep): |
|
240 dirname = dirname[:-1] |
|
241 if localDevice: |
|
242 self.deviceCwd.setText(dirname) |
|
243 showHidden = Preferences.getMicroPython("ShowHiddenDevice") |
|
244 else: |
|
245 self.localCwd.setText(dirname) |
|
246 showHidden = Preferences.getMicroPython("ShowHiddenLocal") |
|
247 |
|
248 filesStatList = listdirStat(dirname, showHidden=showHidden) |
|
249 filesList = [( |
|
250 decoratedName(f, s[0], os.path.isdir(os.path.join(dirname, f))), |
|
251 mode2string(s[0]), |
|
252 str(s[6]), |
|
253 mtime2string(s[8])) for f, s in filesStatList] |
|
254 fileTreeWidget = ( |
|
255 self.deviceFileTreeWidget |
|
256 if localDevice else |
|
257 self.localFileTreeWidget |
|
258 ) |
|
259 fileTreeWidget.clear() |
|
260 for item in filesList: |
|
261 itm = QTreeWidgetItem(fileTreeWidget, item) |
|
262 itm.setTextAlignment(1, Qt.AlignmentFlag.AlignHCenter) |
|
263 itm.setTextAlignment(2, Qt.AlignmentFlag.AlignRight) |
|
264 fileTreeWidget.header().resizeSections( |
|
265 QHeaderView.ResizeMode.ResizeToContents) |
|
266 |
|
267 @pyqtSlot(QTreeWidgetItem, int) |
|
268 def on_localFileTreeWidget_itemActivated(self, item, column): |
|
269 """ |
|
270 Private slot to handle the activation of a local item. |
|
271 |
|
272 If the item is a directory, the list will be re-populated for this |
|
273 directory. |
|
274 |
|
275 @param item reference to the activated item |
|
276 @type QTreeWidgetItem |
|
277 @param column column of the activation |
|
278 @type int |
|
279 """ |
|
280 name = os.path.join(self.localCwd.text(), item.text(0)) |
|
281 if name.endswith("/"): |
|
282 # directory names end with a '/' |
|
283 self.__listLocalFiles(name[:-1]) |
|
284 elif Utilities.MimeTypes.isTextFile(name): |
|
285 ericApp().getObject("ViewManager").getEditor(name) |
|
286 |
|
287 @pyqtSlot() |
|
288 def on_localFileTreeWidget_itemSelectionChanged(self): |
|
289 """ |
|
290 Private slot handling a change of selection in the local pane. |
|
291 """ |
|
292 enable = bool(len(self.localFileTreeWidget.selectedItems())) |
|
293 if enable: |
|
294 enable &= not ( |
|
295 self.localFileTreeWidget.selectedItems()[0].text(0) |
|
296 .endswith("/")) |
|
297 self.putButton.setEnabled(enable) |
|
298 self.putAsButton.setEnabled(enable) |
|
299 |
|
300 @pyqtSlot() |
|
301 def on_localUpButton_clicked(self): |
|
302 """ |
|
303 Private slot to go up one directory level. |
|
304 """ |
|
305 cwd = self.localCwd.text() |
|
306 dirname = os.path.dirname(cwd) |
|
307 self.__listLocalFiles(dirname) |
|
308 |
|
309 @pyqtSlot() |
|
310 def on_localHomeButton_clicked(self): |
|
311 """ |
|
312 Private slot to change directory to the configured workspace. |
|
313 """ |
|
314 dirname = ( |
|
315 Preferences.getMicroPython("MpyWorkspace") or |
|
316 Preferences.getMultiProject("Workspace") or |
|
317 os.path.expanduser("~") |
|
318 ) |
|
319 self.__listLocalFiles(dirname) |
|
320 |
|
321 @pyqtSlot() |
|
322 def on_localReloadButton_clicked(self): |
|
323 """ |
|
324 Private slot to reload the local list. |
|
325 """ |
|
326 dirname = self.localCwd.text() |
|
327 self.__listLocalFiles(dirname) |
|
328 |
|
329 @pyqtSlot(QTreeWidgetItem, int) |
|
330 def on_deviceFileTreeWidget_itemActivated(self, item, column): |
|
331 """ |
|
332 Private slot to handle the activation of a device item. |
|
333 |
|
334 If the item is a directory, the current working directory is changed |
|
335 and the list will be re-populated for this directory. |
|
336 |
|
337 @param item reference to the activated item |
|
338 @type QTreeWidgetItem |
|
339 @param column column of the activation |
|
340 @type int |
|
341 """ |
|
342 if self.__deviceWithLocalAccess: |
|
343 name = os.path.join(self.deviceCwd.text(), item.text(0)) |
|
344 if name.endswith("/"): |
|
345 # directory names end with a '/' |
|
346 self.__listLocalFiles(name[:-1], True) |
|
347 elif Utilities.MimeTypes.isTextFile(name): |
|
348 ericApp().getObject("ViewManager").getEditor(name) |
|
349 else: |
|
350 cwd = self.deviceCwd.text() |
|
351 if cwd.endswith("/"): |
|
352 name = cwd + item.text(0) |
|
353 else: |
|
354 name = "{0}/{1}".format(cwd, item.text(0)) |
|
355 if name.endswith("/"): |
|
356 # directory names end with a '/' |
|
357 self.__fileManager.cd(name[:-1]) |
|
358 |
|
359 @pyqtSlot() |
|
360 def on_deviceFileTreeWidget_itemSelectionChanged(self): |
|
361 """ |
|
362 Private slot handling a change of selection in the local pane. |
|
363 """ |
|
364 enable = bool(len(self.deviceFileTreeWidget.selectedItems())) |
|
365 if enable: |
|
366 enable &= not ( |
|
367 self.deviceFileTreeWidget.selectedItems()[0].text(0) |
|
368 .endswith("/")) |
|
369 self.getButton.setEnabled(enable) |
|
370 self.getAsButton.setEnabled(enable) |
|
371 |
|
372 @pyqtSlot() |
|
373 def on_deviceUpButton_clicked(self): |
|
374 """ |
|
375 Private slot to go up one directory level on the device. |
|
376 """ |
|
377 cwd = self.deviceCwd.text() |
|
378 dirname = os.path.dirname(cwd) |
|
379 if self.__deviceWithLocalAccess: |
|
380 self.__listLocalFiles(dirname, True) |
|
381 else: |
|
382 self.__fileManager.cd(dirname) |
|
383 |
|
384 @pyqtSlot() |
|
385 def on_deviceHomeButton_clicked(self): |
|
386 """ |
|
387 Private slot to move to the device home directory. |
|
388 """ |
|
389 if self.__deviceWithLocalAccess: |
|
390 dirname = self.__repl.getDeviceWorkspace() |
|
391 if dirname: |
|
392 self.__listLocalFiles(dirname, True) |
|
393 return |
|
394 |
|
395 # list files via device script |
|
396 self.__fileManager.cd("/") |
|
397 |
|
398 @pyqtSlot() |
|
399 def on_deviceReloadButton_clicked(self): |
|
400 """ |
|
401 Private slot to reload the device list. |
|
402 """ |
|
403 dirname = self.deviceCwd.text() |
|
404 if self.__deviceWithLocalAccess: |
|
405 self.__listLocalFiles(dirname, True) |
|
406 else: |
|
407 if dirname: |
|
408 self.__newDeviceList() |
|
409 else: |
|
410 self.__fileManager.pwd() |
|
411 |
|
412 def __isFileInList(self, filename, treeWidget): |
|
413 """ |
|
414 Private method to check, if a file name is contained in a tree widget. |
|
415 |
|
416 @param filename name of the file to check |
|
417 @type str |
|
418 @param treeWidget reference to the tree widget to be checked against |
|
419 @return flag indicating that the file name is present |
|
420 @rtype bool |
|
421 """ |
|
422 itemCount = treeWidget.topLevelItemCount() |
|
423 return ( |
|
424 itemCount > 0 and |
|
425 any(treeWidget.topLevelItem(row).text(0) == filename |
|
426 for row in range(itemCount)) |
|
427 ) |
|
428 |
|
429 @pyqtSlot() |
|
430 def on_putButton_clicked(self, putAs=False): |
|
431 """ |
|
432 Private slot to copy the selected file to the connected device. |
|
433 |
|
434 @param putAs flag indicating to give it a new name |
|
435 @type bool |
|
436 """ |
|
437 selectedItems = self.localFileTreeWidget.selectedItems() |
|
438 if selectedItems: |
|
439 filename = selectedItems[0].text(0).strip() |
|
440 if not filename.endswith("/"): |
|
441 # it is really a file |
|
442 if putAs: |
|
443 deviceFilename, ok = QInputDialog.getText( |
|
444 self, |
|
445 self.tr("Put File As"), |
|
446 self.tr("Enter a new name for the file"), |
|
447 QLineEdit.EchoMode.Normal, |
|
448 filename) |
|
449 if not ok or not filename: |
|
450 return |
|
451 else: |
|
452 deviceFilename = filename |
|
453 |
|
454 if self.__isFileInList(deviceFilename, |
|
455 self.deviceFileTreeWidget): |
|
456 # ask for overwrite permission |
|
457 action, resultFilename = confirmOverwrite( |
|
458 deviceFilename, self.tr("Copy File to Device"), |
|
459 self.tr("The given file exists already" |
|
460 " (Enter file name only)."), |
|
461 False, self) |
|
462 if action == "cancel": |
|
463 return |
|
464 elif action == "rename": |
|
465 deviceFilename = os.path.basename(resultFilename) |
|
466 |
|
467 if self.__deviceWithLocalAccess: |
|
468 shutil.copy2( |
|
469 os.path.join(self.localCwd.text(), filename), |
|
470 os.path.join(self.deviceCwd.text(), deviceFilename) |
|
471 ) |
|
472 self.__listLocalFiles(self.deviceCwd.text(), |
|
473 localDevice=True) |
|
474 else: |
|
475 deviceCwd = self.deviceCwd.text() |
|
476 if deviceCwd: |
|
477 if deviceCwd != "/": |
|
478 deviceFilename = deviceCwd + "/" + deviceFilename |
|
479 else: |
|
480 deviceFilename = "/" + deviceFilename |
|
481 self.__fileManager.put( |
|
482 os.path.join(self.localCwd.text(), filename), |
|
483 deviceFilename |
|
484 ) |
|
485 |
|
486 @pyqtSlot() |
|
487 def on_putAsButton_clicked(self): |
|
488 """ |
|
489 Private slot to copy the selected file to the connected device |
|
490 with a different name. |
|
491 """ |
|
492 self.on_putButton_clicked(putAs=True) |
|
493 |
|
494 @pyqtSlot() |
|
495 def on_getButton_clicked(self, getAs=False): |
|
496 """ |
|
497 Private slot to copy the selected file from the connected device. |
|
498 |
|
499 @param getAs flag indicating to give it a new name |
|
500 @type bool |
|
501 """ |
|
502 selectedItems = self.deviceFileTreeWidget.selectedItems() |
|
503 if selectedItems: |
|
504 filename = selectedItems[0].text(0).strip() |
|
505 if not filename.endswith("/"): |
|
506 # it is really a file |
|
507 if getAs: |
|
508 localFilename, ok = QInputDialog.getText( |
|
509 self, |
|
510 self.tr("Get File As"), |
|
511 self.tr("Enter a new name for the file"), |
|
512 QLineEdit.EchoMode.Normal, |
|
513 filename) |
|
514 if not ok or not filename: |
|
515 return |
|
516 else: |
|
517 localFilename = filename |
|
518 |
|
519 if self.__isFileInList(localFilename, |
|
520 self.localFileTreeWidget): |
|
521 # ask for overwrite permission |
|
522 action, resultFilename = confirmOverwrite( |
|
523 localFilename, self.tr("Copy File from Device"), |
|
524 self.tr("The given file exists already."), |
|
525 True, self) |
|
526 if action == "cancel": |
|
527 return |
|
528 elif action == "rename": |
|
529 localFilename = resultFilename |
|
530 |
|
531 if self.__deviceWithLocalAccess: |
|
532 shutil.copy2( |
|
533 os.path.join(self.deviceCwd.text(), filename), |
|
534 os.path.join(self.localCwd.text(), localFilename) |
|
535 ) |
|
536 self.__listLocalFiles(self.localCwd.text()) |
|
537 else: |
|
538 deviceCwd = self.deviceCwd.text() |
|
539 if deviceCwd: |
|
540 filename = deviceCwd + "/" + filename |
|
541 self.__fileManager.get( |
|
542 filename, |
|
543 os.path.join(self.localCwd.text(), localFilename) |
|
544 ) |
|
545 |
|
546 @pyqtSlot() |
|
547 def on_getAsButton_clicked(self): |
|
548 """ |
|
549 Private slot to copy the selected file from the connected device |
|
550 with a different name. |
|
551 """ |
|
552 self.on_getButton_clicked(getAs=True) |
|
553 |
|
554 @pyqtSlot(str, str) |
|
555 def __handleGetDone(self, deviceFile, localFile): |
|
556 """ |
|
557 Private slot handling a successful copy of a file from the device. |
|
558 |
|
559 @param deviceFile name of the file on the device |
|
560 @type str |
|
561 @param localFile name of the local file |
|
562 @type str |
|
563 """ |
|
564 self.__listLocalFiles(self.localCwd.text()) |
|
565 |
|
566 @pyqtSlot() |
|
567 def on_syncButton_clicked(self): |
|
568 """ |
|
569 Private slot to synchronize the local directory to the device. |
|
570 """ |
|
571 self.__fileManager.rsync( |
|
572 self.localCwd.text(), |
|
573 self.deviceCwd.text(), |
|
574 mirror=True, |
|
575 localDevice=self.__deviceWithLocalAccess, |
|
576 ) |
|
577 |
|
578 @pyqtSlot(str, str) |
|
579 def __handleRsyncDone(self, localDir, deviceDir): |
|
580 """ |
|
581 Private method to handle the completion of the rsync operation. |
|
582 |
|
583 @param localDir name of the local directory |
|
584 @type str |
|
585 @param deviceDir name of the device directory |
|
586 @type str |
|
587 """ |
|
588 # simulate button presses to reload the two lists |
|
589 self.on_localReloadButton_clicked() |
|
590 self.on_deviceReloadButton_clicked() |
|
591 |
|
592 @pyqtSlot(str) |
|
593 def __handleRsyncProgressMessage(self, message): |
|
594 """ |
|
595 Private slot handling progress messages sent by the file manager. |
|
596 |
|
597 @param message message to be shown |
|
598 @type str |
|
599 """ |
|
600 if self.__progressInfoDialog is None: |
|
601 from .MicroPythonProgressInfoDialog import ( |
|
602 MicroPythonProgressInfoDialog |
|
603 ) |
|
604 self.__progressInfoDialog = MicroPythonProgressInfoDialog(self) |
|
605 self.__progressInfoDialog.finished.connect( |
|
606 self.__progressInfoDialogFinished) |
|
607 self.__progressInfoDialog.show() |
|
608 self.__progressInfoDialog.addMessage(message) |
|
609 |
|
610 @pyqtSlot() |
|
611 def __progressInfoDialogFinished(self): |
|
612 """ |
|
613 Private slot handling the closing of the progress info dialog. |
|
614 """ |
|
615 self.__progressInfoDialog.deleteLater() |
|
616 self.__progressInfoDialog = None |
|
617 |
|
618 @pyqtSlot() |
|
619 def __newDeviceList(self): |
|
620 """ |
|
621 Private slot to initiate a new long list of the device directory. |
|
622 """ |
|
623 self.__fileManager.lls( |
|
624 self.deviceCwd.text(), |
|
625 showHidden=Preferences.getMicroPython("ShowHiddenDevice") |
|
626 ) |
|
627 |
|
628 ################################################################## |
|
629 ## Context menu methods for the local files below |
|
630 ################################################################## |
|
631 |
|
632 @pyqtSlot(QPoint) |
|
633 def __showLocalContextMenu(self, pos): |
|
634 """ |
|
635 Private slot to show the REPL context menu. |
|
636 |
|
637 @param pos position to show the menu at |
|
638 @type QPoint |
|
639 """ |
|
640 hasSelection = bool(len(self.localFileTreeWidget.selectedItems())) |
|
641 if hasSelection: |
|
642 name = self.localFileTreeWidget.selectedItems()[0].text(0) |
|
643 isDir = name.endswith("/") |
|
644 isFile = not isDir |
|
645 else: |
|
646 isDir = False |
|
647 isFile = False |
|
648 self.__localDelDirTreeAct.setEnabled(isDir) |
|
649 self.__localDelFileAct.setEnabled(isFile) |
|
650 |
|
651 self.__localMenu.exec(self.localFileTreeWidget.mapToGlobal(pos)) |
|
652 |
|
653 @pyqtSlot() |
|
654 def __changeLocalDirectory(self, localDevice=False): |
|
655 """ |
|
656 Private slot to change the local directory. |
|
657 |
|
658 @param localDevice flag indicating device access via local file system |
|
659 @type bool |
|
660 """ |
|
661 cwdWidget = self.deviceCwd if localDevice else self.localCwd |
|
662 |
|
663 dirPath, ok = EricPathPickerDialog.getPath( |
|
664 self, |
|
665 self.tr("Change Directory"), |
|
666 self.tr("Select Directory"), |
|
667 EricPathPickerModes.DIRECTORY_SHOW_FILES_MODE, |
|
668 path=cwdWidget.text(), |
|
669 defaultDirectory=cwdWidget.text(), |
|
670 ) |
|
671 if ok and dirPath: |
|
672 if not os.path.isabs(dirPath): |
|
673 dirPath = os.path.join(cwdWidget.text(), dirPath) |
|
674 cwdWidget.setText(dirPath) |
|
675 self.__listLocalFiles(dirPath, localDevice=localDevice) |
|
676 |
|
677 @pyqtSlot() |
|
678 def __createLocalDirectory(self, localDevice=False): |
|
679 """ |
|
680 Private slot to create a local directory. |
|
681 |
|
682 @param localDevice flag indicating device access via local file system |
|
683 @type bool |
|
684 """ |
|
685 cwdWidget = self.deviceCwd if localDevice else self.localCwd |
|
686 |
|
687 dirPath, ok = QInputDialog.getText( |
|
688 self, |
|
689 self.tr("Create Directory"), |
|
690 self.tr("Enter directory name:"), |
|
691 QLineEdit.EchoMode.Normal) |
|
692 if ok and dirPath: |
|
693 dirPath = os.path.join(cwdWidget.text(), dirPath) |
|
694 try: |
|
695 os.mkdir(dirPath) |
|
696 self.__listLocalFiles(cwdWidget.text(), |
|
697 localDevice=localDevice) |
|
698 except OSError as exc: |
|
699 EricMessageBox.critical( |
|
700 self, |
|
701 self.tr("Create Directory"), |
|
702 self.tr("""<p>The directory <b>{0}</b> could not be""" |
|
703 """ created.</p><p>Reason: {1}</p>""").format( |
|
704 dirPath, str(exc)) |
|
705 ) |
|
706 |
|
707 @pyqtSlot() |
|
708 def __deleteLocalDirectoryTree(self, localDevice=False): |
|
709 """ |
|
710 Private slot to delete a local directory tree. |
|
711 |
|
712 @param localDevice flag indicating device access via local file system |
|
713 @type bool |
|
714 """ |
|
715 if localDevice: |
|
716 cwdWidget = self.deviceCwd |
|
717 fileTreeWidget = self.deviceFileTreeWidget |
|
718 else: |
|
719 cwdWidget = self.localCwd |
|
720 fileTreeWidget = self.localFileTreeWidget |
|
721 |
|
722 if bool(len(fileTreeWidget.selectedItems())): |
|
723 name = fileTreeWidget.selectedItems()[0].text(0) |
|
724 dirname = os.path.join(cwdWidget.text(), name[:-1]) |
|
725 dlg = DeleteFilesConfirmationDialog( |
|
726 self, |
|
727 self.tr("Delete Directory Tree"), |
|
728 self.tr( |
|
729 "Do you really want to delete this directory tree?"), |
|
730 [dirname]) |
|
731 if dlg.exec() == QDialog.DialogCode.Accepted: |
|
732 try: |
|
733 shutil.rmtree(dirname) |
|
734 self.__listLocalFiles(cwdWidget.text(), |
|
735 localDevice=localDevice) |
|
736 except Exception as exc: |
|
737 EricMessageBox.critical( |
|
738 self, |
|
739 self.tr("Delete Directory Tree"), |
|
740 self.tr("""<p>The directory <b>{0}</b> could not be""" |
|
741 """ deleted.</p><p>Reason: {1}</p>""").format( |
|
742 dirname, str(exc)) |
|
743 ) |
|
744 |
|
745 @pyqtSlot() |
|
746 def __deleteLocalFile(self, localDevice=False): |
|
747 """ |
|
748 Private slot to delete a local file. |
|
749 |
|
750 @param localDevice flag indicating device access via local file system |
|
751 @type bool |
|
752 """ |
|
753 if localDevice: |
|
754 cwdWidget = self.deviceCwd |
|
755 fileTreeWidget = self.deviceFileTreeWidget |
|
756 else: |
|
757 cwdWidget = self.localCwd |
|
758 fileTreeWidget = self.localFileTreeWidget |
|
759 |
|
760 if bool(len(fileTreeWidget.selectedItems())): |
|
761 name = fileTreeWidget.selectedItems()[0].text(0) |
|
762 filename = os.path.join(cwdWidget.text(), name) |
|
763 dlg = DeleteFilesConfirmationDialog( |
|
764 self, |
|
765 self.tr("Delete File"), |
|
766 self.tr( |
|
767 "Do you really want to delete this file?"), |
|
768 [filename]) |
|
769 if dlg.exec() == QDialog.DialogCode.Accepted: |
|
770 try: |
|
771 os.remove(filename) |
|
772 self.__listLocalFiles(cwdWidget.text(), |
|
773 localDevice=localDevice) |
|
774 except OSError as exc: |
|
775 EricMessageBox.critical( |
|
776 self, |
|
777 self.tr("Delete File"), |
|
778 self.tr("""<p>The file <b>{0}</b> could not be""" |
|
779 """ deleted.</p><p>Reason: {1}</p>""").format( |
|
780 filename, str(exc)) |
|
781 ) |
|
782 |
|
783 @pyqtSlot(bool) |
|
784 def __localHiddenChanged(self, checked): |
|
785 """ |
|
786 Private slot handling a change of the local show hidden menu entry. |
|
787 |
|
788 @param checked new check state of the action |
|
789 @type bool |
|
790 """ |
|
791 Preferences.setMicroPython("ShowHiddenLocal", checked) |
|
792 self.on_localReloadButton_clicked() |
|
793 |
|
794 ################################################################## |
|
795 ## Context menu methods for the device files below |
|
796 ################################################################## |
|
797 |
|
798 @pyqtSlot(QPoint) |
|
799 def __showDeviceContextMenu(self, pos): |
|
800 """ |
|
801 Private slot to show the REPL context menu. |
|
802 |
|
803 @param pos position to show the menu at |
|
804 @type QPoint |
|
805 """ |
|
806 hasSelection = bool(len(self.deviceFileTreeWidget.selectedItems())) |
|
807 if hasSelection: |
|
808 name = self.deviceFileTreeWidget.selectedItems()[0].text(0) |
|
809 isDir = name.endswith("/") |
|
810 isFile = not isDir |
|
811 else: |
|
812 isDir = False |
|
813 isFile = False |
|
814 if not self.__repl.isMicrobit(): |
|
815 if not self.__deviceWithLocalAccess: |
|
816 self.__devDelDirAct.setEnabled(isDir) |
|
817 self.__devDelDirTreeAct.setEnabled(isDir) |
|
818 self.__devDelFileAct.setEnabled(isFile) |
|
819 |
|
820 self.__deviceMenu.exec(self.deviceFileTreeWidget.mapToGlobal(pos)) |
|
821 |
|
822 @pyqtSlot() |
|
823 def __changeDeviceDirectory(self): |
|
824 """ |
|
825 Private slot to change the current directory of the device. |
|
826 |
|
827 Note: This triggers a re-population of the device list for the new |
|
828 current directory. |
|
829 """ |
|
830 if self.__deviceWithLocalAccess: |
|
831 self.__changeLocalDirectory(True) |
|
832 else: |
|
833 dirPath, ok = QInputDialog.getText( |
|
834 self, |
|
835 self.tr("Change Directory"), |
|
836 self.tr("Enter the directory path on the device:"), |
|
837 QLineEdit.EchoMode.Normal, |
|
838 self.deviceCwd.text()) |
|
839 if ok and dirPath: |
|
840 if not dirPath.startswith("/"): |
|
841 dirPath = self.deviceCwd.text() + "/" + dirPath |
|
842 self.__fileManager.cd(dirPath) |
|
843 |
|
844 @pyqtSlot() |
|
845 def __createDeviceDirectory(self): |
|
846 """ |
|
847 Private slot to create a directory on the device. |
|
848 """ |
|
849 if self.__deviceWithLocalAccess: |
|
850 self.__createLocalDirectory(True) |
|
851 else: |
|
852 dirPath, ok = QInputDialog.getText( |
|
853 self, |
|
854 self.tr("Create Directory"), |
|
855 self.tr("Enter directory name:"), |
|
856 QLineEdit.EchoMode.Normal) |
|
857 if ok and dirPath: |
|
858 self.__fileManager.mkdir(dirPath) |
|
859 |
|
860 @pyqtSlot() |
|
861 def __deleteDeviceDirectory(self): |
|
862 """ |
|
863 Private slot to delete an empty directory on the device. |
|
864 """ |
|
865 if self.__deviceWithLocalAccess: |
|
866 self.__deleteLocalDirectoryTree(True) |
|
867 else: |
|
868 if bool(len(self.deviceFileTreeWidget.selectedItems())): |
|
869 name = self.deviceFileTreeWidget.selectedItems()[0].text(0) |
|
870 cwd = self.deviceCwd.text() |
|
871 if cwd: |
|
872 if cwd != "/": |
|
873 dirname = cwd + "/" + name[:-1] |
|
874 else: |
|
875 dirname = "/" + name[:-1] |
|
876 else: |
|
877 dirname = name[:-1] |
|
878 dlg = DeleteFilesConfirmationDialog( |
|
879 self, |
|
880 self.tr("Delete Directory"), |
|
881 self.tr( |
|
882 "Do you really want to delete this directory?"), |
|
883 [dirname]) |
|
884 if dlg.exec() == QDialog.DialogCode.Accepted: |
|
885 self.__fileManager.rmdir(dirname) |
|
886 |
|
887 @pyqtSlot() |
|
888 def __deleteDeviceDirectoryTree(self): |
|
889 """ |
|
890 Private slot to delete a directory and all its subdirectories |
|
891 recursively. |
|
892 """ |
|
893 if self.__deviceWithLocalAccess: |
|
894 self.__deleteLocalDirectoryTree(True) |
|
895 else: |
|
896 if bool(len(self.deviceFileTreeWidget.selectedItems())): |
|
897 name = self.deviceFileTreeWidget.selectedItems()[0].text(0) |
|
898 cwd = self.deviceCwd.text() |
|
899 if cwd: |
|
900 if cwd != "/": |
|
901 dirname = cwd + "/" + name[:-1] |
|
902 else: |
|
903 dirname = "/" + name[:-1] |
|
904 else: |
|
905 dirname = name[:-1] |
|
906 dlg = DeleteFilesConfirmationDialog( |
|
907 self, |
|
908 self.tr("Delete Directory Tree"), |
|
909 self.tr( |
|
910 "Do you really want to delete this directory tree?"), |
|
911 [dirname]) |
|
912 if dlg.exec() == QDialog.DialogCode.Accepted: |
|
913 self.__fileManager.rmdir(dirname, recursive=True) |
|
914 |
|
915 @pyqtSlot() |
|
916 def __deleteDeviceFile(self): |
|
917 """ |
|
918 Private slot to delete a file. |
|
919 """ |
|
920 if self.__deviceWithLocalAccess: |
|
921 self.__deleteLocalFile(True) |
|
922 else: |
|
923 if bool(len(self.deviceFileTreeWidget.selectedItems())): |
|
924 name = self.deviceFileTreeWidget.selectedItems()[0].text(0) |
|
925 dirname = self.deviceCwd.text() |
|
926 if dirname: |
|
927 if dirname != "/": |
|
928 filename = dirname + "/" + name |
|
929 else: |
|
930 filename = "/" + name |
|
931 else: |
|
932 filename = name |
|
933 dlg = DeleteFilesConfirmationDialog( |
|
934 self, |
|
935 self.tr("Delete File"), |
|
936 self.tr( |
|
937 "Do you really want to delete this file?"), |
|
938 [filename]) |
|
939 if dlg.exec() == QDialog.DialogCode.Accepted: |
|
940 self.__fileManager.delete(filename) |
|
941 |
|
942 @pyqtSlot(bool) |
|
943 def __deviceHiddenChanged(self, checked): |
|
944 """ |
|
945 Private slot handling a change of the device show hidden menu entry. |
|
946 |
|
947 @param checked new check state of the action |
|
948 @type bool |
|
949 """ |
|
950 Preferences.setMicroPython("ShowHiddenDevice", checked) |
|
951 self.on_deviceReloadButton_clicked() |
|
952 |
|
953 @pyqtSlot() |
|
954 def __showFileSystemInfo(self): |
|
955 """ |
|
956 Private slot to show some file system information. |
|
957 """ |
|
958 self.__fileManager.fileSystemInfo() |
|
959 |
|
960 @pyqtSlot(tuple) |
|
961 def __fsInfoResultReceived(self, fsinfo): |
|
962 """ |
|
963 Private slot to show the file system information of the device. |
|
964 |
|
965 @param fsinfo tuple of tuples containing the file system name, the |
|
966 total size, the used size and the free size |
|
967 @type tuple of tuples of (str, int, int, int) |
|
968 """ |
|
969 msg = self.tr("<h3>Filesystem Information</h3>") |
|
970 for name, totalSize, usedSize, freeSize in fsinfo: |
|
971 msg += self.tr( |
|
972 "<h4>{0}</h4" |
|
973 "<table>" |
|
974 "<tr><td>Total Size: </td><td align='right'>{1}</td></tr>" |
|
975 "<tr><td>Used Size: </td><td align='right'>{2}</td></tr>" |
|
976 "<tr><td>Free Size: </td><td align='right'>{3}</td></tr>" |
|
977 "</table>" |
|
978 ).format(name, |
|
979 Globals.dataString(totalSize), |
|
980 Globals.dataString(usedSize), |
|
981 Globals.dataString(freeSize), |
|
982 ) |
|
983 EricMessageBox.information( |
|
984 self, |
|
985 self.tr("Filesystem Information"), |
|
986 msg) |