src/eric7/UI/Browser.py

branch
eric7
changeset 9209
b99e7fd55fd3
parent 9066
a219ade50f7c
child 9221
bf71ee032bb4
equal deleted inserted replaced
9208:3fc8dfeb6ebe 9209:b99e7fd55fd3
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2002 - 2022 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a browser with class browsing capabilities.
8 """
9
10 import os
11 import shutil
12
13 from PyQt6.QtCore import (
14 pyqtSignal, pyqtSlot, Qt, QUrl, QCoreApplication, QItemSelectionModel,
15 QModelIndex, QElapsedTimer
16 )
17 from PyQt6.QtGui import QAction, QDesktopServices
18 from PyQt6.QtWidgets import (
19 QTreeView, QApplication, QMenu, QAbstractItemView, QInputDialog,
20 QLineEdit, QDialog
21 )
22
23 from EricWidgets.EricApplication import ericApp
24 from EricWidgets import EricFileDialog, EricMessageBox
25
26 from Project.ProjectBrowserModel import ProjectBrowserSimpleDirectoryItem
27 from .BrowserModel import (
28 BrowserModel, BrowserDirectoryItem, BrowserFileItem, BrowserClassItem,
29 BrowserMethodItem, BrowserClassAttributeItem, BrowserImportItem,
30 BrowserImportsItem, BrowserSysPathItem, BrowserGlobalsItem,
31 BrowserItemDirectory
32 )
33 from .BrowserSortFilterProxyModel import BrowserSortFilterProxyModel
34
35 import UI.PixmapCache
36 import Preferences
37 import Utilities
38 import Utilities.MimeTypes
39
40
41 class Browser(QTreeView):
42 """
43 Class used to display a file system tree.
44
45 Via the context menu that
46 is displayed by a right click the user can select various actions on
47 the selected file.
48
49 @signal sourceFile(filename) emitted to open a Python file at a line (str)
50 @signal sourceFile(filename, lineno) emitted to open a Python file at a
51 line (str, int)
52 @signal sourceFile(filename, lineno, type) emitted to open a Python file
53 at a line giving an explicit file type (str, int, str)
54 @signal sourceFile(filename, linenos) emitted to open a Python file giving
55 a list of lines(str, list)
56 @signal designerFile(filename) emitted to open a Qt-Designer file (str)
57 @signal linguistFile(filename) emitted to open a Qt-Linguist (*.ts)
58 file (str)
59 @signal trpreview(filenames) emitted to preview Qt-Linguist (*.qm)
60 files (list of str)
61 @signal trpreview(filenames, ignore) emitted to preview Qt-Linguist (*.qm)
62 files indicating whether non-existent files shall be ignored
63 (list of str, bool)
64 @signal projectFile(filename) emitted to open an eric project file (str)
65 @signal multiProjectFile(filename) emitted to open an eric multi project
66 file (str)
67 @signal pixmapFile(filename) emitted to open a pixmap file (str)
68 @signal pixmapEditFile(filename) emitted to edit a pixmap file (str)
69 @signal svgFile(filename) emitted to open a SVG file (str)
70 @signal umlFile(filename) emitted to open an eric UML file (str)
71 @signal binaryFile(filename) emitted to open a file as binary (str)
72 @signal testFile(filename) emitted to open a Python file for a
73 unit test (str)
74 """
75 sourceFile = pyqtSignal((str, ), (str, int), (str, list), (str, int, str))
76 designerFile = pyqtSignal(str)
77 linguistFile = pyqtSignal(str)
78 trpreview = pyqtSignal((list, ), (list, bool))
79 projectFile = pyqtSignal(str)
80 multiProjectFile = pyqtSignal(str)
81 pixmapFile = pyqtSignal(str)
82 pixmapEditFile = pyqtSignal(str)
83 svgFile = pyqtSignal(str)
84 umlFile = pyqtSignal(str)
85 binaryFile = pyqtSignal(str)
86 testFile = pyqtSignal(str)
87
88 def __init__(self, parent=None):
89 """
90 Constructor
91
92 @param parent parent widget (QWidget)
93 """
94 super().__init__(parent)
95
96 self.setWindowTitle(QCoreApplication.translate('Browser',
97 'File-Browser'))
98 self.setWindowIcon(UI.PixmapCache.getIcon("eric"))
99
100 self.__model = BrowserModel()
101 self.__sortModel = BrowserSortFilterProxyModel()
102 self.__sortModel.setSourceModel(self.__model)
103 self.setModel(self.__sortModel)
104
105 self.selectedItemsFilter = [BrowserFileItem]
106
107 self._activating = False
108
109 self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
110 self.customContextMenuRequested.connect(self._contextMenuRequested)
111 self.activated.connect(self._openItem)
112 self.expanded.connect(self._resizeColumns)
113 self.collapsed.connect(self._resizeColumns)
114
115 self.setWhatsThis(QCoreApplication.translate(
116 'Browser',
117 """<b>The Browser Window</b>"""
118 """<p>This allows you to easily navigate the hierarchy of"""
119 """ directories and files on your system, identify the Python"""
120 """ programs and open them up in a Source Viewer window. The"""
121 """ window displays several separate hierarchies.</p>"""
122 """<p>The first hierarchy is only shown if you have opened a"""
123 """ program for debugging and its root is the directory"""
124 """ containing that program. Usually all of the separate files"""
125 """ that make up a Python application are held in the same"""
126 """ directory, so this hierarchy gives you easy access to most"""
127 """ of what you will need.</p>"""
128 """<p>The next hierarchy is used to easily navigate the"""
129 """ directories that are specified in the Python"""
130 """ <tt>sys.path</tt> variable.</p>"""
131 """<p>The remaining hierarchies allow you navigate your system"""
132 """ as a whole. On a UNIX system there will be a hierarchy with"""
133 """ <tt>/</tt> at its root and another with the user home"""
134 """ directory. On a Windows system there will be a hierarchy for"""
135 """ each drive on the"""
136 """ system.</p>"""
137 """<p>Python programs (i.e. those with a <tt>.py</tt> file name"""
138 """ suffix) are identified in the hierarchies with a Python"""
139 """ icon. The right mouse button will popup a menu which lets"""
140 """ you open the file in a Source Viewer window, open the file"""
141 """ for debugging or use it for a test run.</p>"""
142 """<p>The context menu of a class, function or method allows you"""
143 """ to open the file defining this class, function or method and"""
144 """ will ensure, that the correct source line is visible.</p>"""
145 """<p>Qt-Designer files (i.e. those with a <tt>.ui</tt> file"""
146 """ name suffix) are shown with a Designer icon. The context"""
147 """ menu of these files allows you to start Qt-Designer with"""
148 """ that file.</p>"""
149 """<p>Qt-Linguist files (i.e. those with a <tt>.ts</tt> file"""
150 """ name suffix) are shown with a Linguist icon. The context"""
151 """ menu of these files allows you to start Qt-Linguist with"""
152 """ that file.</p>"""
153 ))
154
155 self.__createPopupMenus()
156
157 self._init() # perform common initialization tasks
158
159 self._keyboardSearchString = ""
160 self._keyboardSearchTimer = QElapsedTimer()
161 self._keyboardSearchTimer.invalidate()
162
163 def _init(self):
164 """
165 Protected method to perform initialization tasks common to this
166 base class and all derived classes.
167 """
168 self.setRootIsDecorated(True)
169 self.setAlternatingRowColors(True)
170
171 header = self.header()
172 header.setSortIndicator(0, Qt.SortOrder.AscendingOrder)
173 header.setSortIndicatorShown(True)
174 header.setSectionsClickable(True)
175
176 self.setSortingEnabled(True)
177
178 self.setSelectionMode(
179 QAbstractItemView.SelectionMode.ExtendedSelection)
180 self.setSelectionBehavior(
181 QAbstractItemView.SelectionBehavior.SelectRows)
182
183 self.header().setStretchLastSection(True)
184 self.headerSize0 = 0
185 self.layoutDisplay()
186
187 def layoutDisplay(self):
188 """
189 Public slot to perform a layout operation.
190 """
191 self._resizeColumns(QModelIndex())
192 self._resort()
193
194 def _resizeColumns(self, index):
195 """
196 Protected slot to resize the view when items get expanded or collapsed.
197
198 @param index index of item (QModelIndex)
199 """
200 w = max(100, self.sizeHintForColumn(0))
201 if w != self.headerSize0:
202 self.header().resizeSection(0, w)
203 self.headerSize0 = w
204
205 def _resort(self):
206 """
207 Protected slot to resort the tree.
208 """
209 self.model().sort(self.header().sortIndicatorSection(),
210 self.header().sortIndicatorOrder())
211
212 def __createPopupMenus(self):
213 """
214 Private method to generate the various popup menus.
215 """
216 self.showHiddenFilesAct = QAction(
217 QCoreApplication.translate('Browser', 'Show Hidden Files'))
218 self.showHiddenFilesAct.setCheckable(True)
219 self.showHiddenFilesAct.toggled.connect(self._showHidden)
220 self.showHiddenFilesAct.setChecked(
221 Preferences.getUI("BrowsersListHiddenFiles"))
222
223 self.__newMenu = QMenu(QCoreApplication.translate('Browser', "New"),
224 self)
225 self.__newMenu.addAction(
226 QCoreApplication.translate('Browser', 'Directory'),
227 self._newDirectory)
228 self.__newMenu.addAction(
229 QCoreApplication.translate('Browser', 'File'), self._newFile)
230
231 # create the popup menu for source files
232 self.sourceMenu = QMenu(self)
233 self.sourceMenu.addAction(
234 QCoreApplication.translate('Browser', 'Open'), self._openItem)
235 self.testingAct = self.sourceMenu.addAction(
236 QCoreApplication.translate('Browser', 'Run Test...'),
237 self.handleTesting)
238 self.sourceMenu.addSeparator()
239 self.mimeTypeAct = self.sourceMenu.addAction(
240 QCoreApplication.translate('Browser', 'Show Mime-Type'),
241 self.__showMimeType)
242 self.sourceMenu.addSeparator()
243 self.sourceMenu.addAction(
244 QCoreApplication.translate('Browser', 'Refresh Source File'),
245 self.__refreshSource)
246 self.sourceMenu.addSeparator()
247 self.sourceMenu.addAction(
248 QCoreApplication.translate('Browser', 'Copy Path to Clipboard'),
249 self._copyToClipboard)
250 self.sourceMenu.addSeparator()
251 self.sourceMenu.addAction(self.showHiddenFilesAct)
252 self.sourceMenu.addSeparator()
253 self.sourceMenu.addMenu(self.__newMenu)
254 self.sourceMenu.addAction(
255 QCoreApplication.translate('Browser', 'Delete'),
256 self._deleteFileOrDirectory)
257
258 # create the popup menu for general use
259 self.menu = QMenu(self)
260 self.menu.addAction(
261 QCoreApplication.translate('Browser', 'Open'), self._openItem)
262 self.menu.addAction(
263 QCoreApplication.translate('Browser', 'Open in Hex Editor'),
264 self._openHexEditor)
265 self.editPixmapAct = self.menu.addAction(
266 QCoreApplication.translate('Browser', 'Open in Icon Editor'),
267 self._editPixmap)
268 self.openInEditorAct = self.menu.addAction(
269 QCoreApplication.translate('Browser', 'Open in Editor'),
270 self._openFileInEditor)
271 self.menu.addSeparator()
272 self.mimeTypeAct = self.menu.addAction(
273 QCoreApplication.translate('Browser', 'Show Mime-Type'),
274 self.__showMimeType)
275 self.menu.addSeparator()
276 self.menu.addAction(
277 QCoreApplication.translate('Browser', 'Copy Path to Clipboard'),
278 self._copyToClipboard)
279 self.menu.addSeparator()
280 self.menu.addAction(self.showHiddenFilesAct)
281 self.menu.addSeparator()
282 self.menu.addMenu(self.__newMenu)
283 self.menu.addAction(
284 QCoreApplication.translate('Browser', 'Delete'),
285 self._deleteFileOrDirectory)
286
287 # create the menu for multiple selected files
288 self.multiMenu = QMenu(self)
289 self.multiMenu.addAction(
290 QCoreApplication.translate('Browser', 'Open'), self._openItem)
291 self.multiMenu.addSeparator()
292 self.multiMenu.addAction(self.showHiddenFilesAct)
293 self.multiMenu.addSeparator()
294 self.multiMenu.addAction(
295 QCoreApplication.translate('Browser', 'Delete'),
296 self.__deleteMultiple)
297
298 # create the directory menu
299 self.dirMenu = QMenu(self)
300 self.dirMenu.addAction(
301 QCoreApplication.translate('Browser', 'New toplevel directory...'),
302 self.__newToplevelDir)
303 self.addAsTopLevelAct = self.dirMenu.addAction(
304 QCoreApplication.translate('Browser', 'Add as toplevel directory'),
305 self.__addAsToplevelDir)
306 self.removeFromToplevelAct = self.dirMenu.addAction(
307 QCoreApplication.translate('Browser', 'Remove from toplevel'),
308 self.__removeToplevel)
309 self.dirMenu.addSeparator()
310 self.dirMenu.addAction(
311 QCoreApplication.translate('Browser', 'Refresh directory'),
312 self.__refreshDirectory)
313 self.dirMenu.addSeparator()
314 self.dirMenu.addAction(
315 QCoreApplication.translate('Browser', 'Find in this directory'),
316 self.__findInDirectory)
317 self.dirMenu.addAction(
318 QCoreApplication.translate(
319 'Browser', 'Find && Replace in this directory'),
320 self.__replaceInDirectory)
321 self.dirMenu.addAction(
322 QCoreApplication.translate('Browser', 'Copy Path to Clipboard'),
323 self._copyToClipboard)
324 self.dirMenu.addSeparator()
325 self.dirMenu.addAction(self.showHiddenFilesAct)
326 self.dirMenu.addSeparator()
327 self.dirMenu.addMenu(self.__newMenu)
328 self.dirMenu.addAction(
329 QCoreApplication.translate('Browser', 'Delete'),
330 self._deleteFileOrDirectory)
331
332 # create the attribute menu
333 self.gotoMenu = QMenu(QCoreApplication.translate('Browser', "Goto"),
334 self)
335 self.gotoMenu.aboutToShow.connect(self._showGotoMenu)
336 self.gotoMenu.triggered.connect(self._gotoAttribute)
337
338 self.attributeMenu = QMenu(self)
339 self.attributeMenu.addAction(
340 QCoreApplication.translate('Browser', 'New toplevel directory...'),
341 self.__newToplevelDir)
342 self.attributeMenu.addSeparator()
343 self.attributeMenu.addMenu(self.gotoMenu)
344
345 # create the background menu
346 self.backMenu = QMenu(self)
347 self.backMenu.addAction(
348 QCoreApplication.translate('Browser', 'New toplevel directory...'),
349 self.__newToplevelDir)
350 self.backMenu.addSeparator()
351 self.backMenu.addAction(self.showHiddenFilesAct)
352
353 def mouseDoubleClickEvent(self, mouseEvent):
354 """
355 Protected method of QAbstractItemView.
356
357 Reimplemented to disable expanding/collapsing of items when
358 double-clicking. Instead the double-clicked entry is opened.
359
360 @param mouseEvent the mouse event (QMouseEvent)
361 """
362 index = self.indexAt(mouseEvent.position().toPoint())
363 if index.isValid():
364 itm = self.model().item(index)
365 if isinstance(itm, (
366 BrowserDirectoryItem, BrowserImportsItem,
367 ProjectBrowserSimpleDirectoryItem, BrowserSysPathItem,
368 BrowserGlobalsItem)):
369 self.setExpanded(index, not self.isExpanded(index))
370 else:
371 self._openItem()
372
373 def _contextMenuRequested(self, coord):
374 """
375 Protected slot to show the context menu of the listview.
376
377 @param coord the position of the mouse pointer (QPoint)
378 """
379 categories = self.getSelectedItemsCountCategorized(
380 [BrowserDirectoryItem, BrowserFileItem,
381 BrowserClassItem, BrowserMethodItem])
382 cnt = categories["sum"]
383 bfcnt = categories[str(BrowserFileItem)]
384 if cnt > 1 and cnt == bfcnt:
385 self.multiMenu.popup(self.mapToGlobal(coord))
386 else:
387 index = self.indexAt(coord)
388
389 if index.isValid():
390 self.setCurrentIndex(index)
391 flags = (
392 QItemSelectionModel.SelectionFlag.ClearAndSelect |
393 QItemSelectionModel.SelectionFlag.Rows
394 )
395 self.selectionModel().select(index, flags)
396
397 itm = self.model().item(index)
398 coord = self.mapToGlobal(coord)
399 if isinstance(itm, BrowserFileItem):
400 if itm.isPython3File():
401 if itm.fileName().endswith('.py'):
402 self.testingAct.setEnabled(True)
403 else:
404 self.testingAct.setEnabled(False)
405 self.sourceMenu.popup(coord)
406 else:
407 self.editPixmapAct.setVisible(itm.isPixmapFile())
408 self.openInEditorAct.setVisible(itm.isSvgFile())
409 self.menu.popup(coord)
410 elif isinstance(
411 itm,
412 (BrowserClassItem, BrowserMethodItem, BrowserImportItem)
413 ):
414 self.editPixmapAct.setVisible(False)
415 self.menu.popup(coord)
416 elif isinstance(itm, BrowserClassAttributeItem):
417 self.attributeMenu.popup(coord)
418 elif isinstance(itm, BrowserDirectoryItem):
419 if not index.parent().isValid():
420 self.removeFromToplevelAct.setEnabled(True)
421 self.addAsTopLevelAct.setEnabled(False)
422 else:
423 self.removeFromToplevelAct.setEnabled(False)
424 self.addAsTopLevelAct.setEnabled(True)
425 self.dirMenu.popup(coord)
426 else:
427 self.backMenu.popup(coord)
428 else:
429 self.backMenu.popup(self.mapToGlobal(coord))
430
431 def _showGotoMenu(self):
432 """
433 Protected slot to prepare the goto submenu of the attribute menu.
434 """
435 self.gotoMenu.clear()
436
437 itm = self.model().item(self.currentIndex())
438 linenos = itm.linenos()
439 fileName = itm.fileName()
440
441 for lineno in sorted(linenos):
442 act = self.gotoMenu.addAction(
443 QCoreApplication.translate(
444 'Browser', "Line {0}").format(lineno))
445 act.setData([fileName, lineno])
446
447 def _gotoAttribute(self, act):
448 """
449 Protected slot to handle the selection of the goto menu.
450
451 @param act reference to the action (EricAction)
452 """
453 fileName, lineno = act.data()
454 self.sourceFile[str, int].emit(fileName, lineno)
455
456 def handlePreferencesChanged(self):
457 """
458 Public slot used to handle the preferencesChanged signal.
459 """
460 self.model().preferencesChanged()
461 self._resort()
462
463 def _openItem(self):
464 """
465 Protected slot to handle the open popup menu entry.
466 """
467 itmList = self.getSelectedItems(
468 [BrowserFileItem, BrowserClassItem,
469 BrowserMethodItem, BrowserClassAttributeItem,
470 BrowserImportItem])
471
472 if not self._activating:
473 self._activating = True
474 for itm in itmList:
475 if isinstance(itm, BrowserFileItem):
476 if (
477 itm.isPython3File() or
478 itm.isIdlFile() or
479 itm.isProtobufFile() or
480 itm.isResourcesFile()
481 ):
482 self.sourceFile[str].emit(itm.fileName())
483 elif itm.isRubyFile():
484 self.sourceFile[str, int, str].emit(
485 itm.fileName(), -1, "Ruby")
486 elif itm.isDFile():
487 self.sourceFile[str, int, str].emit(
488 itm.fileName(), -1, "D")
489 elif itm.isDesignerFile():
490 self.designerFile.emit(itm.fileName())
491 elif itm.isLinguistFile():
492 if itm.fileExt() == '.ts':
493 self.linguistFile.emit(itm.fileName())
494 else:
495 self.trpreview.emit([itm.fileName()])
496 elif itm.isProjectFile():
497 self.projectFile.emit(itm.fileName())
498 elif itm.isMultiProjectFile():
499 self.multiProjectFile.emit(itm.fileName())
500 elif itm.isSvgFile():
501 self.svgFile.emit(itm.fileName())
502 elif itm.isPixmapFile():
503 self.pixmapFile.emit(itm.fileName())
504 elif itm.isEricGraphicsFile():
505 self.umlFile.emit(itm.fileName())
506 else:
507 if Utilities.MimeTypes.isTextFile(itm.fileName()):
508 self.sourceFile[str].emit(itm.fileName())
509 else:
510 QDesktopServices.openUrl(QUrl(itm.fileName()))
511 elif isinstance(itm, BrowserClassItem):
512 self.sourceFile[str, int].emit(
513 itm.fileName(), itm.classObject().lineno)
514 elif isinstance(itm, BrowserMethodItem):
515 self.sourceFile[str, int].emit(
516 itm.fileName(), itm.functionObject().lineno)
517 elif isinstance(itm, BrowserClassAttributeItem):
518 self.sourceFile[str, int].emit(
519 itm.fileName(), itm.attributeObject().lineno)
520 elif isinstance(itm, BrowserImportItem):
521 self.sourceFile[str, list].emit(
522 itm.fileName(), itm.linenos())
523 self._activating = False
524
525 def __showMimeType(self):
526 """
527 Private slot to show the mime type of the selected entry.
528 """
529 itmList = self.getSelectedItems(
530 [BrowserFileItem, BrowserClassItem,
531 BrowserMethodItem, BrowserClassAttributeItem,
532 BrowserImportItem])
533 if itmList:
534 mimetype = Utilities.MimeTypes.mimeType(itmList[0].fileName())
535 if mimetype is None:
536 EricMessageBox.warning(
537 self,
538 QCoreApplication.translate('Browser', "Show Mime-Type"),
539 QCoreApplication.translate(
540 'Browser',
541 """The mime type of the file could not be"""
542 """ determined."""))
543 elif mimetype.split("/")[0] == "text":
544 EricMessageBox.information(
545 self,
546 QCoreApplication.translate('Browser', "Show Mime-Type"),
547 QCoreApplication.translate(
548 'Browser',
549 """The file has the mime type <b>{0}</b>.""")
550 .format(mimetype))
551 else:
552 textMimeTypesList = Preferences.getUI("TextMimeTypes")
553 if mimetype in textMimeTypesList:
554 EricMessageBox.information(
555 self,
556 QCoreApplication.translate(
557 'Browser', "Show Mime-Type"),
558 QCoreApplication.translate(
559 'Browser',
560 """The file has the mime type <b>{0}</b>.""")
561 .format(mimetype))
562 else:
563 ok = EricMessageBox.yesNo(
564 self,
565 QCoreApplication.translate(
566 'Browser', "Show Mime-Type"),
567 QCoreApplication.translate(
568 'Browser',
569 """The file has the mime type <b>{0}</b>."""
570 """<br/> Shall it be added to the list of"""
571 """ text mime types?""").format(mimetype))
572 if ok:
573 textMimeTypesList.append(mimetype)
574 Preferences.setUI("TextMimeTypes", textMimeTypesList)
575
576 def __refreshSource(self):
577 """
578 Private slot to refresh the structure of a source file.
579 """
580 itmList = self.getSelectedItems([BrowserFileItem])
581 if itmList:
582 self.__model.repopulateFileItem(itmList[0])
583
584 def _editPixmap(self):
585 """
586 Protected slot to handle the open in icon editor popup menu entry.
587 """
588 itmList = self.getSelectedItems([BrowserFileItem])
589
590 for itm in itmList:
591 if isinstance(itm, BrowserFileItem) and itm.isPixmapFile():
592 self.pixmapEditFile.emit(itm.fileName())
593
594 def _openHexEditor(self):
595 """
596 Protected slot to handle the open in hex editor popup menu entry.
597 """
598 itmList = self.getSelectedItems([BrowserFileItem])
599
600 for itm in itmList:
601 if isinstance(itm, BrowserFileItem):
602 self.binaryFile.emit(itm.fileName())
603
604 def _openFileInEditor(self):
605 """
606 Protected slot to handle the Open in Editor menu action.
607 """
608 itmList = self.getSelectedItems([BrowserFileItem])
609
610 for itm in itmList:
611 if Utilities.MimeTypes.isTextFile(itm.fileName()):
612 self.sourceFile.emit(itm.fileName())
613
614 def _copyToClipboard(self):
615 """
616 Protected method to copy the text shown for an entry to the clipboard.
617 """
618 itm = self.model().item(self.currentIndex())
619 try:
620 fn = itm.fileName()
621 except AttributeError:
622 try:
623 fn = itm.dirName()
624 except AttributeError:
625 fn = ""
626
627 if fn:
628 cb = QApplication.clipboard()
629 cb.setText(fn)
630
631 @pyqtSlot(bool)
632 def _showHidden(self, checked):
633 """
634 Protected slot to show or hide hidden files.
635
636 @param checked flag indicating the state of the action
637 @type bool
638 """
639 self.__sortModel.setShowHiddenFiles(checked)
640 # remember the current state
641 Preferences.setUI("BrowsersListHiddenFiles", checked)
642
643 def handleTesting(self):
644 """
645 Public slot to handle the testing popup menu entry.
646 """
647 try:
648 index = self.currentIndex()
649 itm = self.model().item(index)
650 pyfn = itm.fileName()
651 except AttributeError:
652 pyfn = None
653
654 if pyfn is not None:
655 self.testFile.emit(pyfn)
656
657 def __newToplevelDir(self):
658 """
659 Private slot to handle the New toplevel directory popup menu entry.
660 """
661 dname = EricFileDialog.getExistingDirectory(
662 None,
663 QCoreApplication.translate('Browser', "New toplevel directory"),
664 "",
665 EricFileDialog.ShowDirsOnly)
666 if dname:
667 dname = os.path.abspath(Utilities.toNativeSeparators(dname))
668 self.__model.addTopLevelDir(dname)
669
670 def __removeToplevel(self):
671 """
672 Private slot to handle the Remove from toplevel popup menu entry.
673 """
674 index = self.currentIndex()
675 sindex = self.model().mapToSource(index)
676 self.__model.removeToplevelDir(sindex)
677
678 def __addAsToplevelDir(self):
679 """
680 Private slot to handle the Add as toplevel directory popup menu entry.
681 """
682 index = self.currentIndex()
683 dname = self.model().item(index).dirName()
684 self.__model.addTopLevelDir(dname)
685
686 def __refreshDirectory(self):
687 """
688 Private slot to refresh a directory entry.
689 """
690 index = self.currentIndex()
691 refreshDir = self.model().item(index).dirName()
692 self.__model.directoryChanged(refreshDir)
693
694 def __findInDirectory(self):
695 """
696 Private slot to handle the Find in directory popup menu entry.
697 """
698 index = self.currentIndex()
699 searchDir = self.model().item(index).dirName()
700
701 ericApp().getObject("UserInterface").showFindFilesWidget(
702 searchDir=searchDir)
703
704 def __replaceInDirectory(self):
705 """
706 Private slot to handle the Find&Replace in directory popup menu entry.
707 """
708 index = self.currentIndex()
709 searchDir = self.model().item(index).dirName()
710
711 ericApp().getObject("UserInterface").showReplaceFilesWidget(
712 searchDir=searchDir)
713
714 def handleProgramChange(self, fn):
715 """
716 Public slot to handle the programChange signal.
717
718 @param fn file name (string)
719 """
720 self.__model.programChange(os.path.dirname(fn))
721
722 def handleInterpreterChanged(self, interpreter):
723 """
724 Public slot to handle a change of the debug client's interpreter.
725
726 @param interpreter interpreter of the debug client (string)
727 """
728 self.__model.interpreterChanged(interpreter)
729
730 def wantedItem(self, itm, filterList=None):
731 """
732 Public method to check type of an item.
733
734 @param itm the item to check (BrowserItem)
735 @param filterList list of classes to check against
736 @return flag indicating item is a valid type (boolean)
737 """
738 if filterList is None:
739 filterList = self.selectedItemsFilter
740
741 return any(isinstance(itm, typ) for typ in filterList)
742
743 def getSelectedItems(self, filterList=None):
744 """
745 Public method to get the selected items.
746
747 @param filterList list of classes to check against
748 @return list of selected items (list of BrowserItem)
749 """
750 selectedItems = []
751 indexes = self.selectedIndexes()
752 for index in indexes:
753 if index.column() == 0:
754 itm = self.model().item(index)
755 if self.wantedItem(itm, filterList):
756 selectedItems.append(itm)
757 return selectedItems
758
759 def getSelectedItemsCount(self, filterList=None):
760 """
761 Public method to get the count of items selected.
762
763 @param filterList list of classes to check against
764 @return count of items selected (integer)
765 """
766 count = 0
767 indexes = self.selectedIndexes()
768 for index in indexes:
769 if index.column() == 0:
770 itm = self.model().item(index)
771 if self.wantedItem(itm, filterList):
772 count += 1
773 return count
774
775 def getSelectedItemsCountCategorized(self, filterList=None):
776 """
777 Public method to get a categorized count of selected items.
778
779 @param filterList list of classes to check against
780 @return a dictionary containing the counts of items belonging
781 to the individual filter classes. The keys of the dictionary
782 are the string representation of the classes given in the
783 filter (i.e. str(filterClass)). The dictionary contains
784 an additional entry with key "sum", that stores the sum of
785 all selected entries fulfilling the filter criteria.
786 """
787 if filterList is None:
788 filterList = self.selectedItemsFilter
789 categories = {}
790 categories["sum"] = 0
791 for typ in filterList:
792 categories[str(typ)] = 0
793
794 indexes = self.selectedIndexes()
795 for index in indexes:
796 if index.column() == 0:
797 itm = self.model().item(index)
798 for typ in filterList:
799 if isinstance(itm, typ):
800 categories["sum"] += 1
801 categories[str(typ)] += 1
802
803 return categories
804
805 def saveToplevelDirs(self):
806 """
807 Public slot to save the toplevel directories.
808 """
809 self.__model.saveToplevelDirs()
810
811 def keyboardSearch(self, search):
812 """
813 Public function to search the tree via the keyboard.
814
815 @param search the character entered via the keyboard
816 @type str
817 """
818 if self.model().rowCount() == 0:
819 return
820
821 startIndex = (
822 self.currentIndex()
823 if self.currentIndex().isValid() else
824 self.model().index(0, 0)
825 )
826
827 keyboardSearchTimeWasValid = self._keyboardSearchTimer.isValid()
828 keyboardSearchTimeElapsed = self._keyboardSearchTimer.restart()
829 if (
830 not search or
831 not keyboardSearchTimeWasValid or
832 keyboardSearchTimeElapsed >
833 QApplication.keyboardInputInterval()
834 ):
835 self._keyboardSearchString = search.lower()
836 else:
837 self._keyboardSearchString += search.lower()
838
839 index = startIndex
840 found = False
841 while True:
842 name = self.model().data(index)
843 if (
844 name.lower().startswith(self._keyboardSearchString) and
845 self._keyboardSearchType(self.model().item(index))
846 ):
847 found = True
848 break
849
850 index = self.indexBelow(index)
851 if not index.isValid():
852 index = self.model().index(0, 0)
853 if index == startIndex:
854 break
855
856 if found:
857 self.setCurrentIndex(index)
858
859 def _keyboardSearchType(self, item):
860 """
861 Protected method to check, if the item is of the correct type.
862
863 @param item reference to the item
864 @type BrowserItem
865 @return flag indicating a correct type
866 @rtype bool
867 """
868 return isinstance(
869 item, (BrowserDirectoryItem, BrowserFileItem, BrowserSysPathItem))
870
871 @pyqtSlot()
872 def _newDirectory(self):
873 """
874 Protected slot to create a new directory.
875 """
876 index = self.currentIndex()
877 if index.isValid():
878 dname = self.model().item(index).dirName()
879 newName, ok = QInputDialog.getText(
880 self,
881 self.tr("New Directory"),
882 self.tr("Name for new directory:"),
883 QLineEdit.EchoMode.Normal)
884 if ok and bool(newName):
885 dirpath = os.path.join(dname, newName)
886 if os.path.exists(dirpath):
887 EricMessageBox.warning(
888 self,
889 self.tr("New Directory"),
890 self.tr("A file or directory named <b>{0}</b> exists"
891 " already. Aborting...")
892 .format(newName))
893 else:
894 try:
895 os.mkdir(dirpath, mode=0o751)
896 except OSError as err:
897 EricMessageBox.critical(
898 self,
899 self.tr("New Directory"),
900 self.tr("<p>The directory <b>{0}</b> could not be"
901 " created.</p><p>Reason: {1}</p>")
902 .format(newName, str(err)))
903
904 @pyqtSlot()
905 def _newFile(self):
906 """
907 Protected slot to create a new file.
908 """
909 index = self.currentIndex()
910 if index.isValid():
911 dname = self.model().item(index).dirName()
912 fname, ok = QInputDialog.getText(
913 self,
914 self.tr("New File"),
915 self.tr("Name for new file:"),
916 QLineEdit.EchoMode.Normal)
917 if ok and bool(fname):
918 filepath = os.path.join(dname, fname)
919 if os.path.exists(filepath):
920 EricMessageBox.warning(
921 self,
922 self.tr("New File"),
923 self.tr("A file or directory named <b>{0}</b> exists"
924 " already. Aborting...")
925 .format(fname))
926 else:
927 try:
928 with open(filepath, "w"):
929 pass
930 except OSError as err:
931 EricMessageBox.critical(
932 self,
933 self.tr("New File"),
934 self.tr("<p>The file <b>{0}</b> could not be"
935 " created.</p><p>Reason: {1}</p>")
936 .format(fname, str(err)))
937
938 @pyqtSlot()
939 def _deleteFileOrDirectory(self):
940 """
941 Protected slot to delete a directory or file.
942 """
943 index = self.currentIndex()
944 if index.isValid():
945 itm = self.model().item(index)
946 if itm.type() == BrowserItemDirectory:
947 self.__deleteDirectory(itm.dirName())
948 else:
949 self.__deleteFile(itm.fileName())
950
951 def __deleteFile(self, fn):
952 """
953 Private method to delete a file.
954
955 @param fn filename to be deleted
956 @type str
957 """
958 try:
959 from send2trash import send2trash as s2t
960 trashMsg = self.tr("Do you really want to move this file to the"
961 " trash?")
962 except ImportError:
963 s2t = os.remove
964 trashMsg = self.tr("Do you really want to delete this file?")
965
966 from UI.DeleteFilesConfirmationDialog import (
967 DeleteFilesConfirmationDialog
968 )
969 dlg = DeleteFilesConfirmationDialog(
970 self.parent(),
971 self.tr("Delete File"),
972 trashMsg,
973 [fn])
974 if dlg.exec() == QDialog.DialogCode.Accepted:
975 try:
976 s2t(fn)
977 except OSError as err:
978 EricMessageBox.critical(
979 self.ui,
980 self.tr("Delete File"),
981 self.tr(
982 "<p>The selected file <b>{0}</b> could not be"
983 " deleted.</p><p>Reason: {1}</p>")
984 .format(fn, str(err))
985 )
986
987 def __deleteDirectory(self, dn):
988 """
989 Private method to delete a directory.
990
991 @param dn directory name to be removed from the project
992 @type str
993 """
994 try:
995 from send2trash import send2trash
996 s2tAvailable = True
997 trashMsg = self.tr("Do you really want to move this directory to"
998 " the trash?")
999 except ImportError:
1000 s2tAvailable = False
1001 trashMsg = self.tr("Do you really want to delete this directory?")
1002
1003 from UI.DeleteFilesConfirmationDialog import (
1004 DeleteFilesConfirmationDialog
1005 )
1006 dlg = DeleteFilesConfirmationDialog(
1007 self.parent(),
1008 self.tr("Delete Directory"),
1009 trashMsg,
1010 [dn])
1011 if dlg.exec() == QDialog.DialogCode.Accepted:
1012 try:
1013 if s2tAvailable:
1014 send2trash(dn)
1015 else:
1016 shutil.rmtree(dn, True)
1017 except OSError as err:
1018 EricMessageBox.critical(
1019 self.ui,
1020 self.tr("Delete Directory"),
1021 self.tr(
1022 "<p>The selected directory <b>{0}</b> could not be"
1023 " deleted.</p><p>Reason: {1}</p>")
1024 .format(dn, str(err))
1025 )
1026
1027 @pyqtSlot()
1028 def __deleteMultiple(self):
1029 """
1030 Private slot to delete multiple directories and files.
1031
1032 Note: The context menu for multi selection is only shown for file
1033 items.
1034 """
1035 fileNames = []
1036 for itm in self.getSelectedItems():
1037 fileNames.append(itm.fileName())
1038
1039 try:
1040 from send2trash import send2trash as s2t
1041 trashMsg = self.tr("Do you really want to move these files to the"
1042 " trash?")
1043 except ImportError:
1044 s2t = os.remove
1045 trashMsg = self.tr("Do you really want to delete these files?")
1046
1047 from UI.DeleteFilesConfirmationDialog import (
1048 DeleteFilesConfirmationDialog
1049 )
1050 dlg = DeleteFilesConfirmationDialog(
1051 self.parent(),
1052 self.tr("Delete Files"),
1053 trashMsg,
1054 sorted(fileNames)
1055 )
1056 if dlg.exec() == QDialog.DialogCode.Accepted:
1057 for fn in fileNames:
1058 try:
1059 s2t(fn)
1060 except OSError as err:
1061 EricMessageBox.critical(
1062 self.ui,
1063 self.tr("Delete File"),
1064 self.tr(
1065 "<p>The selected file <b>{0}</b> could not be"
1066 " deleted.</p><p>Reason: {1}</p>")
1067 .format(fn, str(err))
1068 )

eric ide

mercurial