UI/Browser.py

changeset 0
de9c2efb9d02
child 12
1d8dd9706f46
equal deleted inserted replaced
-1:000000000000 0:de9c2efb9d02
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2002 - 2009 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a browser with class browsing capabilities.
8 """
9
10 import sys
11 import os
12 import mimetypes
13
14 from PyQt4.QtCore import *
15 from PyQt4.QtGui import *
16
17 from E4Gui.E4Application import e4App
18
19 from BrowserModel import BrowserModel, \
20 BrowserDirectoryItem, BrowserFileItem, BrowserClassItem, BrowserMethodItem, \
21 BrowserClassAttributeItem
22 from BrowserSortFilterProxyModel import BrowserSortFilterProxyModel
23
24 import UI.PixmapCache
25 import Preferences
26 import Utilities
27
28 class Browser(QTreeView):
29 """
30 Class used to display a file system tree.
31
32 Via the context menu that
33 is displayed by a right click the user can select various actions on
34 the selected file.
35
36 @signal sourceFile(string, int, string) emitted to open a Python file at a line
37 @signal designerFile(string) emitted to open a Qt-Designer file
38 @signal linguistFile(string) emitted to open a Qt-Linguist (*.ts) file
39 @signal trpreview(string list) emitted to preview a Qt-Linguist (*.qm) file
40 @signal projectFile(string) emitted to open an eric4 project file
41 @signal multiProjectFile(string) emitted to open an eric4 multi project file
42 @signal pixmapFile(string) emitted to open a pixmap file
43 @signal pixmapEditFile(string) emitted to edit a pixmap file
44 @signal svgFile(string) emitted to open a SVG file
45 @signal unittestOpen(string) emitted to open a Python file for a unittest
46 """
47 def __init__(self, parent = None):
48 """
49 Constructor
50
51 @param parent parent widget (QWidget)
52 """
53 QTreeView.__init__(self, parent)
54
55 self.setWindowTitle(QApplication.translate('Browser', 'File-Browser'))
56 self.setWindowIcon(UI.PixmapCache.getIcon("eric.png"))
57
58 self.__embeddedBrowser = Preferences.getUI("LayoutFileBrowserEmbedded")
59
60 self.__model = BrowserModel()
61 self.__sortModel = BrowserSortFilterProxyModel()
62 self.__sortModel.setSourceModel(self.__model)
63 self.setModel(self.__sortModel)
64
65 self.selectedItemsFilter = [BrowserFileItem]
66
67 self.setContextMenuPolicy(Qt.CustomContextMenu)
68 self.connect(self, SIGNAL("customContextMenuRequested(const QPoint &)"),
69 self._contextMenuRequested)
70 self.connect(self, SIGNAL("activated(const QModelIndex &)"), self._openItem)
71 self.connect(self, SIGNAL("expanded(const QModelIndex &)"), self._resizeColumns)
72 self.connect(self, SIGNAL("collapsed(const QModelIndex &)"), self._resizeColumns)
73
74 self.setWhatsThis(QApplication.translate('Browser',
75 """<b>The Browser Window</b>"""
76 """<p>This allows you to easily navigate the hierachy of directories and"""
77 """ files on your system, identify the Python programs and open them up in"""
78 """ a Source Viewer window. The window displays several separate"""
79 """ hierachies.</p>"""
80 """<p>The first hierachy is only shown if you have opened a program for"""
81 """ debugging and it's root is the directory containing that program."""
82 """ Usually all of the separate files that make up a Python application are"""
83 """ held in the same directory, so this hierachy gives you easy access to"""
84 """ most of what you will need.</p>"""
85 """<p>The next hierachy is used to easily navigate the directories that are"""
86 """ specified in the Python <tt>sys.path</tt> variable.</p>"""
87 """<p>The remaining hierachies allow you navigate your system as a whole."""
88 """ On a UNIX system there will be a hierachy with <tt>/</tt> at its"""
89 """ root and another with the user home directory."""
90 """ On a Windows system there will be a hierachy for each drive on the"""
91 """ system.</p>"""
92 """<p>Python programs (i.e. those with a <tt>.py</tt> file name suffix)"""
93 """ are identified in the hierachies with a Python icon."""
94 """ The right mouse button will popup a menu which lets you"""
95 """ open the file in a Source Viewer window,"""
96 """ open the file for debugging or use it for a unittest run.</p>"""
97 """<p>The context menu of a class, function or method allows you to open"""
98 """ the file defining this class, function or method and will ensure, that"""
99 """ the correct source line is visible.</p>"""
100 """<p>Qt-Designer files (i.e. those with a <tt>.ui</tt> file name suffix)"""
101 """ are shown with a Designer icon. The context menu of these files"""
102 """ allows you to start Qt-Designer with that file.</p>"""
103 """<p>Qt-Linguist files (i.e. those with a <tt>.ts</tt> file name suffix)"""
104 """ are shown with a Linguist icon. The context menu of these files"""
105 """ allows you to start Qt-Linguist with that file.</p>"""
106 ))
107
108 self.__createPopupMenus()
109
110 self._init() # perform common initialization tasks
111
112 def _init(self):
113 """
114 Protected method to perform initialization tasks common to this
115 base class and all derived classes.
116 """
117 self.setRootIsDecorated(True)
118 self.setAlternatingRowColors(True)
119
120 header = self.header()
121 header.setSortIndicator(0, Qt.AscendingOrder)
122 header.setSortIndicatorShown(True)
123 header.setClickable(True)
124
125 self.setSortingEnabled(True)
126
127 self.setSelectionMode(QAbstractItemView.ExtendedSelection)
128 self.setSelectionBehavior(QAbstractItemView.SelectRows)
129
130 self.header().setStretchLastSection(True)
131 self.headerSize0 = 0
132 self.layoutDisplay()
133
134 def layoutDisplay(self):
135 """
136 Public slot to perform a layout operation.
137 """
138 self.doItemsLayout()
139 self._resizeColumns(QModelIndex())
140 self._resort()
141
142 def _resizeColumns(self, index):
143 """
144 Protected slot to resize the view when items get expanded or collapsed.
145
146 @param index index of item (QModelIndex)
147 """
148 w = max(100, self.sizeHintForColumn(0))
149 if w != self.headerSize0:
150 self.header().resizeSection(0, w)
151 self.headerSize0 = w
152
153 def _resort(self):
154 """
155 Protected slot to resort the tree.
156 """
157 self.model().sort(self.header().sortIndicatorSection(),
158 self.header().sortIndicatorOrder())
159
160 def __createPopupMenus(self):
161 """
162 Private method to generate the various popup menus.
163 """
164 # create the popup menu for source files
165 self.sourceMenu = QMenu(self)
166 self.sourceMenu.addAction(QApplication.translate('Browser', 'Open'),
167 self._openItem)
168 self.unittestAct = self.sourceMenu.addAction(\
169 QApplication.translate('Browser', 'Run unittest...'), self.handleUnittest)
170 self.sourceMenu.addAction(
171 QApplication.translate('Browser', 'Copy Path to Clipboard'),
172 self._copyToClipboard)
173
174 # create the popup menu for general use
175 self.menu = QMenu(self)
176 self.menu.addAction(QApplication.translate('Browser', 'Open'), self._openItem)
177 self.editPixmapAct = \
178 self.menu.addAction(QApplication.translate('Browser', 'Open in Icon Editor'),
179 self._editPixmap)
180 self.menu.addAction(
181 QApplication.translate('Browser', 'Copy Path to Clipboard'),
182 self._copyToClipboard)
183 if self.__embeddedBrowser in [1, 2]:
184 self.menu.addSeparator()
185 self.menu.addAction(QApplication.translate('Browser', 'Configure...'),
186 self.__configure)
187
188 # create the menu for multiple selected files
189 self.multiMenu = QMenu(self)
190 self.multiMenu.addAction(QApplication.translate('Browser', 'Open'),
191 self._openItem)
192 if self.__embeddedBrowser in [1, 2]:
193 self.multiMenu.addSeparator()
194 self.multiMenu.addAction(QApplication.translate('Browser', 'Configure...'),
195 self.__configure)
196
197 # create the directory menu
198 self.dirMenu = QMenu(self)
199 self.dirMenu.addAction(QApplication.translate('Browser',
200 'New toplevel directory...'),
201 self.__newToplevelDir)
202 self.addAsTopLevelAct = self.dirMenu.addAction(\
203 QApplication.translate('Browser', 'Add as toplevel directory'),
204 self.__addAsToplevelDir)
205 self.removeFromToplevelAct = self.dirMenu.addAction(\
206 QApplication.translate('Browser', 'Remove from toplevel'),
207 self.__removeToplevel)
208 self.dirMenu.addSeparator()
209 self.dirMenu.addAction(QApplication.translate('Browser',
210 'Find in this directory'),
211 self.__findInDirectory)
212 self.dirMenu.addAction(QApplication.translate('Browser',
213 'Find&&Replace in this directory'),
214 self.__replaceInDirectory)
215 self.dirMenu.addAction(
216 QApplication.translate('Browser', 'Copy Path to Clipboard'),
217 self._copyToClipboard)
218 if self.__embeddedBrowser in [1, 2]:
219 self.dirMenu.addSeparator()
220 self.dirMenu.addAction(QApplication.translate('Browser', 'Configure...'),
221 self.__configure)
222
223 # create the background menu
224 self.backMenu = QMenu(self)
225 self.backMenu.addAction(QApplication.translate('Browser',
226 'New toplevel directory...'),
227 self.__newToplevelDir)
228 if self.__embeddedBrowser in [1, 2]:
229 self.backMenu.addSeparator()
230 self.backMenu.addAction(QApplication.translate('Browser', 'Configure...'),
231 self.__configure)
232
233 def mouseDoubleClickEvent(self, mouseEvent):
234 """
235 Protected method of QAbstractItemView.
236
237 Reimplemented to disable expanding/collapsing
238 of items when double-clicking. Instead the double-clicked entry is opened.
239
240 @param mouseEvent the mouse event (QMouseEvent)
241 """
242 index = self.indexAt(mouseEvent.pos())
243 if index.isValid():
244 self._openItem()
245
246 def _contextMenuRequested(self, coord):
247 """
248 Protected slot to show the context menu of the listview.
249
250 @param coord the position of the mouse pointer (QPoint)
251 """
252 categories = self.getSelectedItemsCountCategorized(\
253 [BrowserDirectoryItem, BrowserFileItem,
254 BrowserClassItem, BrowserMethodItem])
255 cnt = categories["sum"]
256 bfcnt = categories[unicode(BrowserFileItem)]
257 if cnt > 1 and cnt == bfcnt:
258 self.multiMenu.popup(self.mapToGlobal(coord))
259 else:
260 index = self.indexAt(coord)
261
262 if index.isValid():
263 self.setCurrentIndex(index)
264 flags = QItemSelectionModel.SelectionFlags(\
265 QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows)
266 self.selectionModel().select(index, flags)
267
268 itm = self.model().item(index)
269 coord = self.mapToGlobal(coord)
270 if isinstance(itm, BrowserFileItem):
271 if itm.isPythonFile():
272 if itm.fileName().endswith('.py'):
273 self.unittestAct.setEnabled(True)
274 else:
275 self.unittestAct.setEnabled(False)
276 self.sourceMenu.popup(coord)
277 else:
278 self.editPixmapAct.setVisible(itm.isPixmapFile())
279 self.menu.popup(coord)
280 elif isinstance(itm, BrowserClassItem) or \
281 isinstance(itm, BrowserMethodItem):
282 self.menu.popup(coord)
283 elif isinstance(itm, BrowserDirectoryItem):
284 if not index.parent().isValid():
285 self.removeFromToplevelAct.setEnabled(True)
286 self.addAsTopLevelAct.setEnabled(False)
287 else:
288 self.removeFromToplevelAct.setEnabled(False)
289 self.addAsTopLevelAct.setEnabled(True)
290 self.dirMenu.popup(coord)
291 else:
292 self.backMenu.popup(coord)
293 else:
294 self.backMenu.popup(self.mapToGlobal(coord))
295
296 def handlePreferencesChanged(self):
297 """
298 Public slot used to handle the preferencesChanged signal.
299 """
300 self.model().preferencesChanged()
301 self._resort()
302
303 def _openItem(self):
304 """
305 Protected slot to handle the open popup menu entry.
306 """
307 itmList = self.getSelectedItems(\
308 [BrowserFileItem, BrowserClassItem,
309 BrowserMethodItem, BrowserClassAttributeItem])
310
311 for itm in itmList:
312 if isinstance(itm, BrowserFileItem):
313 if itm.isPythonFile():
314 self.emit(SIGNAL('sourceFile'), itm.fileName(), 1, "Python")
315 elif itm.isRubyFile():
316 self.emit(SIGNAL('sourceFile'), itm.fileName(), 1, "Ruby")
317 elif itm.isDFile():
318 self.emit(SIGNAL('sourceFile'), itm.fileName(), 1, "D")
319 elif itm.isDesignerFile():
320 self.emit(SIGNAL('designerFile'), itm.fileName())
321 elif itm.isLinguistFile():
322 if itm.fileExt() == '.ts':
323 self.emit(SIGNAL('linguistFile'), itm.fileName())
324 else:
325 self.emit(SIGNAL('trpreview'), [itm.fileName()])
326 elif itm.isProjectFile():
327 self.emit(SIGNAL('projectFile'), itm.fileName())
328 elif itm.isMultiProjectFile():
329 self.emit(SIGNAL('multiProjectFile'), itm.fileName())
330 elif itm.isIdlFile():
331 self.emit(SIGNAL('sourceFile'), itm.fileName())
332 elif itm.isResourcesFile():
333 self.emit(SIGNAL('sourceFile'), itm.fileName())
334 elif itm.isPixmapFile():
335 self.emit(SIGNAL('pixmapFile'), itm.fileName())
336 elif itm.isSvgFile():
337 self.emit(SIGNAL('svgFile'), itm.fileName())
338 else:
339 type_ = mimetypes.guess_type(itm.fileName())[0]
340 if type_ is None or type_.split("/")[0] == "text":
341 self.emit(SIGNAL('sourceFile'), itm.fileName())
342 else:
343 QDesktopServices.openUrl(QUrl(itm.fileName()))
344 elif isinstance(itm, BrowserClassItem):
345 self.emit(SIGNAL('sourceFile'), itm.fileName(),
346 itm.classObject().lineno)
347 elif isinstance(itm, BrowserMethodItem):
348 self.emit(SIGNAL('sourceFile'), itm.fileName(),
349 itm.functionObject().lineno)
350 elif isinstance(itm, BrowserClassAttributeItem):
351 self.emit(SIGNAL('sourceFile'), itm.fileName(),
352 itm.attributeObject().lineno)
353
354 def _editPixmap(self):
355 """
356 Protected slot to handle the open in icon editor popup menu entry.
357 """
358 itmList = self.getSelectedItems([BrowserFileItem])
359
360 for itm in itmList:
361 if isinstance(itm, BrowserFileItem):
362 if itm.isPixmapFile():
363 self.emit(SIGNAL('pixmapEditFile'), itm.fileName())
364
365 def _copyToClipboard(self):
366 """
367 Protected method to copy the text shown for an entry to the clipboard.
368 """
369 itm = self.model().item(self.currentIndex())
370 try:
371 fn = itm.fileName()
372 except AttributeError:
373 try:
374 fn = itm.dirName()
375 except AttributeError:
376 fn = ""
377
378 if fn:
379 cb = QApplication.clipboard()
380 cb.setText(fn)
381
382 def handleUnittest(self):
383 """
384 Public slot to handle the unittest popup menu entry.
385 """
386 try:
387 index = self.currentIndex()
388 itm = self.model().item(index)
389 pyfn = itm.fileName()
390 except AttributeError:
391 pyfn = None
392
393 if pyfn is not None:
394 self.emit(SIGNAL('unittestOpen'), pyfn)
395
396 def __newToplevelDir(self):
397 """
398 Private slot to handle the New toplevel directory popup menu entry.
399 """
400 dname = QFileDialog.getExistingDirectory(\
401 None,
402 QApplication.translate('Browser', "New toplevel directory"),
403 "",
404 QFileDialog.Options(QFileDialog.ShowDirsOnly))
405 if dname:
406 dname = os.path.abspath(Utilities.toNativeSeparators(dname))
407 self.__model.addTopLevelDir(dname)
408
409 def __removeToplevel(self):
410 """
411 Private slot to handle the Remove from toplevel popup menu entry.
412 """
413 index = self.currentIndex()
414 sindex = self.model().mapToSource(index)
415 self.__model.removeToplevelDir(sindex)
416
417 def __addAsToplevelDir(self):
418 """
419 Private slot to handle the Add as toplevel directory popup menu entry.
420 """
421 index = self.currentIndex()
422 dname = self.model().item(index).dirName()
423 self.__model.addTopLevelDir(dname)
424
425 def __findInDirectory(self):
426 """
427 Private slot to handle the Find in directory popup menu entry.
428 """
429 index = self.currentIndex()
430 searchDir = self.model().item(index).dirName()
431
432 findFilesDialog = e4App().getObject("FindFilesDialog")
433 findFilesDialog.setSearchDirectory(searchDir)
434 findFilesDialog.show()
435 findFilesDialog.raise_()
436 findFilesDialog.activateWindow()
437
438 def __replaceInDirectory(self):
439 """
440 Private slot to handle the Find&Replace in directory popup menu entry.
441 """
442 index = self.currentIndex()
443 searchDir = self.model().item(index).dirName()
444
445 replaceFilesDialog = e4App().getObject("ReplaceFilesDialog")
446 replaceFilesDialog.setSearchDirectory(searchDir)
447 replaceFilesDialog.show()
448 replaceFilesDialog.raise_()
449 replaceFilesDialog.activateWindow()
450
451 def handleProgramChange(self,fn):
452 """
453 Public slot to handle the programChange signal.
454 """
455 self.__model.programChange(os.path.dirname(fn))
456
457 def wantedItem(self, itm, filter=None):
458 """
459 Public method to check type of an item.
460
461 @param itm the item to check (BrowserItem)
462 @param filter list of classes to check against
463 @return flag indicating item is a valid type (boolean)
464 """
465 if filter is None:
466 filter = self.selectedItemsFilter
467 for typ in filter:
468 if isinstance(itm, typ):
469 return True
470 return False
471
472 def getSelectedItems(self, filter=None):
473 """
474 Public method to get the selected items.
475
476 @param filter list of classes to check against
477 @return list of selected items (list of BroweserItem)
478 """
479 selectedItems = []
480 indexes = self.selectedIndexes()
481 for index in indexes:
482 if index.column() == 0:
483 itm = self.model().item(index)
484 if self.wantedItem(itm, filter):
485 selectedItems.append(itm)
486 return selectedItems
487
488 def getSelectedItemsCount(self, filter=None):
489 """
490 Public method to get the count of items selected.
491
492 @param filter list of classes to check against
493 @return count of items selected (integer)
494 """
495 count = 0
496 indexes = self.selectedIndexes()
497 for index in indexes:
498 if index.column() == 0:
499 itm = self.model().item(index)
500 if self.wantedItem(itm, filter):
501 count += 1
502 return count
503
504 def getSelectedItemsCountCategorized(self, filter=None):
505 """
506 Public method to get a categorized count of selected items.
507
508 @param filter list of classes to check against
509 @return a dictionary containing the counts of items belonging
510 to the individual filter classes. The keys of the dictionary
511 are the unicode representation of the classes given in the
512 filter (i.e. unicode(filterClass)). The dictionary contains
513 an additional entry with key "sum", that stores the sum of
514 all selected entries fulfilling the filter criteria.
515 """
516 if filter is None:
517 filter = self.selectedItemsFilter
518 categories = {}
519 categories["sum"] = 0
520 for typ in filter:
521 categories[unicode(typ)] = 0
522
523 indexes = self.selectedIndexes()
524 for index in indexes:
525 if index.column() == 0:
526 itm = self.model().item(index)
527 for typ in filter:
528 if isinstance(itm, typ):
529 categories["sum"] += 1
530 categories[unicode(typ)] += 1
531
532 return categories
533
534 def saveToplevelDirs(self):
535 """
536 Public slot to save the toplevel directories.
537 """
538 self.__model.saveToplevelDirs()
539
540 def __configure(self):
541 """
542 Private method to open the configuration dialog.
543 """
544 if self.__embeddedBrowser == 1:
545 e4App().getObject("UserInterface").showPreferences("debuggerGeneralPage")
546 elif self.__embeddedBrowser == 2:
547 e4App().getObject("UserInterface").showPreferences("projectBrowserPage")

eric ide

mercurial