--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/UI/Browser.py Mon Dec 28 16:03:33 2009 +0000 @@ -0,0 +1,547 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2002 - 2009 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a browser with class browsing capabilities. +""" + +import sys +import os +import mimetypes + +from PyQt4.QtCore import * +from PyQt4.QtGui import * + +from E4Gui.E4Application import e4App + +from BrowserModel import BrowserModel, \ + BrowserDirectoryItem, BrowserFileItem, BrowserClassItem, BrowserMethodItem, \ + BrowserClassAttributeItem +from BrowserSortFilterProxyModel import BrowserSortFilterProxyModel + +import UI.PixmapCache +import Preferences +import Utilities + +class Browser(QTreeView): + """ + Class used to display a file system tree. + + Via the context menu that + is displayed by a right click the user can select various actions on + the selected file. + + @signal sourceFile(string, int, string) emitted to open a Python file at a line + @signal designerFile(string) emitted to open a Qt-Designer file + @signal linguistFile(string) emitted to open a Qt-Linguist (*.ts) file + @signal trpreview(string list) emitted to preview a Qt-Linguist (*.qm) file + @signal projectFile(string) emitted to open an eric4 project file + @signal multiProjectFile(string) emitted to open an eric4 multi project file + @signal pixmapFile(string) emitted to open a pixmap file + @signal pixmapEditFile(string) emitted to edit a pixmap file + @signal svgFile(string) emitted to open a SVG file + @signal unittestOpen(string) emitted to open a Python file for a unittest + """ + def __init__(self, parent = None): + """ + Constructor + + @param parent parent widget (QWidget) + """ + QTreeView.__init__(self, parent) + + self.setWindowTitle(QApplication.translate('Browser', 'File-Browser')) + self.setWindowIcon(UI.PixmapCache.getIcon("eric.png")) + + self.__embeddedBrowser = Preferences.getUI("LayoutFileBrowserEmbedded") + + self.__model = BrowserModel() + self.__sortModel = BrowserSortFilterProxyModel() + self.__sortModel.setSourceModel(self.__model) + self.setModel(self.__sortModel) + + self.selectedItemsFilter = [BrowserFileItem] + + self.setContextMenuPolicy(Qt.CustomContextMenu) + self.connect(self, SIGNAL("customContextMenuRequested(const QPoint &)"), + self._contextMenuRequested) + self.connect(self, SIGNAL("activated(const QModelIndex &)"), self._openItem) + self.connect(self, SIGNAL("expanded(const QModelIndex &)"), self._resizeColumns) + self.connect(self, SIGNAL("collapsed(const QModelIndex &)"), self._resizeColumns) + + self.setWhatsThis(QApplication.translate('Browser', + """<b>The Browser Window</b>""" + """<p>This allows you to easily navigate the hierachy of directories and""" + """ files on your system, identify the Python programs and open them up in""" + """ a Source Viewer window. The window displays several separate""" + """ hierachies.</p>""" + """<p>The first hierachy is only shown if you have opened a program for""" + """ debugging and it's root is the directory containing that program.""" + """ Usually all of the separate files that make up a Python application are""" + """ held in the same directory, so this hierachy gives you easy access to""" + """ most of what you will need.</p>""" + """<p>The next hierachy is used to easily navigate the directories that are""" + """ specified in the Python <tt>sys.path</tt> variable.</p>""" + """<p>The remaining hierachies allow you navigate your system as a whole.""" + """ On a UNIX system there will be a hierachy with <tt>/</tt> at its""" + """ root and another with the user home directory.""" + """ On a Windows system there will be a hierachy for each drive on the""" + """ system.</p>""" + """<p>Python programs (i.e. those with a <tt>.py</tt> file name suffix)""" + """ are identified in the hierachies with a Python icon.""" + """ The right mouse button will popup a menu which lets you""" + """ open the file in a Source Viewer window,""" + """ open the file for debugging or use it for a unittest run.</p>""" + """<p>The context menu of a class, function or method allows you to open""" + """ the file defining this class, function or method and will ensure, that""" + """ the correct source line is visible.</p>""" + """<p>Qt-Designer files (i.e. those with a <tt>.ui</tt> file name suffix)""" + """ are shown with a Designer icon. The context menu of these files""" + """ allows you to start Qt-Designer with that file.</p>""" + """<p>Qt-Linguist files (i.e. those with a <tt>.ts</tt> file name suffix)""" + """ are shown with a Linguist icon. The context menu of these files""" + """ allows you to start Qt-Linguist with that file.</p>""" + )) + + self.__createPopupMenus() + + self._init() # perform common initialization tasks + + def _init(self): + """ + Protected method to perform initialization tasks common to this + base class and all derived classes. + """ + self.setRootIsDecorated(True) + self.setAlternatingRowColors(True) + + header = self.header() + header.setSortIndicator(0, Qt.AscendingOrder) + header.setSortIndicatorShown(True) + header.setClickable(True) + + self.setSortingEnabled(True) + + self.setSelectionMode(QAbstractItemView.ExtendedSelection) + self.setSelectionBehavior(QAbstractItemView.SelectRows) + + self.header().setStretchLastSection(True) + self.headerSize0 = 0 + self.layoutDisplay() + + def layoutDisplay(self): + """ + Public slot to perform a layout operation. + """ + self.doItemsLayout() + self._resizeColumns(QModelIndex()) + self._resort() + + def _resizeColumns(self, index): + """ + Protected slot to resize the view when items get expanded or collapsed. + + @param index index of item (QModelIndex) + """ + w = max(100, self.sizeHintForColumn(0)) + if w != self.headerSize0: + self.header().resizeSection(0, w) + self.headerSize0 = w + + def _resort(self): + """ + Protected slot to resort the tree. + """ + self.model().sort(self.header().sortIndicatorSection(), + self.header().sortIndicatorOrder()) + + def __createPopupMenus(self): + """ + Private method to generate the various popup menus. + """ + # create the popup menu for source files + self.sourceMenu = QMenu(self) + self.sourceMenu.addAction(QApplication.translate('Browser', 'Open'), + self._openItem) + self.unittestAct = self.sourceMenu.addAction(\ + QApplication.translate('Browser', 'Run unittest...'), self.handleUnittest) + self.sourceMenu.addAction( + QApplication.translate('Browser', 'Copy Path to Clipboard'), + self._copyToClipboard) + + # create the popup menu for general use + self.menu = QMenu(self) + self.menu.addAction(QApplication.translate('Browser', 'Open'), self._openItem) + self.editPixmapAct = \ + self.menu.addAction(QApplication.translate('Browser', 'Open in Icon Editor'), + self._editPixmap) + self.menu.addAction( + QApplication.translate('Browser', 'Copy Path to Clipboard'), + self._copyToClipboard) + if self.__embeddedBrowser in [1, 2]: + self.menu.addSeparator() + self.menu.addAction(QApplication.translate('Browser', 'Configure...'), + self.__configure) + + # create the menu for multiple selected files + self.multiMenu = QMenu(self) + self.multiMenu.addAction(QApplication.translate('Browser', 'Open'), + self._openItem) + if self.__embeddedBrowser in [1, 2]: + self.multiMenu.addSeparator() + self.multiMenu.addAction(QApplication.translate('Browser', 'Configure...'), + self.__configure) + + # create the directory menu + self.dirMenu = QMenu(self) + self.dirMenu.addAction(QApplication.translate('Browser', + 'New toplevel directory...'), + self.__newToplevelDir) + self.addAsTopLevelAct = self.dirMenu.addAction(\ + QApplication.translate('Browser', 'Add as toplevel directory'), + self.__addAsToplevelDir) + self.removeFromToplevelAct = self.dirMenu.addAction(\ + QApplication.translate('Browser', 'Remove from toplevel'), + self.__removeToplevel) + self.dirMenu.addSeparator() + self.dirMenu.addAction(QApplication.translate('Browser', + 'Find in this directory'), + self.__findInDirectory) + self.dirMenu.addAction(QApplication.translate('Browser', + 'Find&&Replace in this directory'), + self.__replaceInDirectory) + self.dirMenu.addAction( + QApplication.translate('Browser', 'Copy Path to Clipboard'), + self._copyToClipboard) + if self.__embeddedBrowser in [1, 2]: + self.dirMenu.addSeparator() + self.dirMenu.addAction(QApplication.translate('Browser', 'Configure...'), + self.__configure) + + # create the background menu + self.backMenu = QMenu(self) + self.backMenu.addAction(QApplication.translate('Browser', + 'New toplevel directory...'), + self.__newToplevelDir) + if self.__embeddedBrowser in [1, 2]: + self.backMenu.addSeparator() + self.backMenu.addAction(QApplication.translate('Browser', 'Configure...'), + self.__configure) + + def mouseDoubleClickEvent(self, mouseEvent): + """ + Protected method of QAbstractItemView. + + Reimplemented to disable expanding/collapsing + of items when double-clicking. Instead the double-clicked entry is opened. + + @param mouseEvent the mouse event (QMouseEvent) + """ + index = self.indexAt(mouseEvent.pos()) + if index.isValid(): + self._openItem() + + def _contextMenuRequested(self, coord): + """ + Protected slot to show the context menu of the listview. + + @param coord the position of the mouse pointer (QPoint) + """ + categories = self.getSelectedItemsCountCategorized(\ + [BrowserDirectoryItem, BrowserFileItem, + BrowserClassItem, BrowserMethodItem]) + cnt = categories["sum"] + bfcnt = categories[unicode(BrowserFileItem)] + if cnt > 1 and cnt == bfcnt: + self.multiMenu.popup(self.mapToGlobal(coord)) + else: + index = self.indexAt(coord) + + if index.isValid(): + self.setCurrentIndex(index) + flags = QItemSelectionModel.SelectionFlags(\ + QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows) + self.selectionModel().select(index, flags) + + itm = self.model().item(index) + coord = self.mapToGlobal(coord) + if isinstance(itm, BrowserFileItem): + if itm.isPythonFile(): + if itm.fileName().endswith('.py'): + self.unittestAct.setEnabled(True) + else: + self.unittestAct.setEnabled(False) + self.sourceMenu.popup(coord) + else: + self.editPixmapAct.setVisible(itm.isPixmapFile()) + self.menu.popup(coord) + elif isinstance(itm, BrowserClassItem) or \ + isinstance(itm, BrowserMethodItem): + self.menu.popup(coord) + elif isinstance(itm, BrowserDirectoryItem): + if not index.parent().isValid(): + self.removeFromToplevelAct.setEnabled(True) + self.addAsTopLevelAct.setEnabled(False) + else: + self.removeFromToplevelAct.setEnabled(False) + self.addAsTopLevelAct.setEnabled(True) + self.dirMenu.popup(coord) + else: + self.backMenu.popup(coord) + else: + self.backMenu.popup(self.mapToGlobal(coord)) + + def handlePreferencesChanged(self): + """ + Public slot used to handle the preferencesChanged signal. + """ + self.model().preferencesChanged() + self._resort() + + def _openItem(self): + """ + Protected slot to handle the open popup menu entry. + """ + itmList = self.getSelectedItems(\ + [BrowserFileItem, BrowserClassItem, + BrowserMethodItem, BrowserClassAttributeItem]) + + for itm in itmList: + if isinstance(itm, BrowserFileItem): + if itm.isPythonFile(): + self.emit(SIGNAL('sourceFile'), itm.fileName(), 1, "Python") + elif itm.isRubyFile(): + self.emit(SIGNAL('sourceFile'), itm.fileName(), 1, "Ruby") + elif itm.isDFile(): + self.emit(SIGNAL('sourceFile'), itm.fileName(), 1, "D") + elif itm.isDesignerFile(): + self.emit(SIGNAL('designerFile'), itm.fileName()) + elif itm.isLinguistFile(): + if itm.fileExt() == '.ts': + self.emit(SIGNAL('linguistFile'), itm.fileName()) + else: + self.emit(SIGNAL('trpreview'), [itm.fileName()]) + elif itm.isProjectFile(): + self.emit(SIGNAL('projectFile'), itm.fileName()) + elif itm.isMultiProjectFile(): + self.emit(SIGNAL('multiProjectFile'), itm.fileName()) + elif itm.isIdlFile(): + self.emit(SIGNAL('sourceFile'), itm.fileName()) + elif itm.isResourcesFile(): + self.emit(SIGNAL('sourceFile'), itm.fileName()) + elif itm.isPixmapFile(): + self.emit(SIGNAL('pixmapFile'), itm.fileName()) + elif itm.isSvgFile(): + self.emit(SIGNAL('svgFile'), itm.fileName()) + else: + type_ = mimetypes.guess_type(itm.fileName())[0] + if type_ is None or type_.split("/")[0] == "text": + self.emit(SIGNAL('sourceFile'), itm.fileName()) + else: + QDesktopServices.openUrl(QUrl(itm.fileName())) + elif isinstance(itm, BrowserClassItem): + self.emit(SIGNAL('sourceFile'), itm.fileName(), + itm.classObject().lineno) + elif isinstance(itm, BrowserMethodItem): + self.emit(SIGNAL('sourceFile'), itm.fileName(), + itm.functionObject().lineno) + elif isinstance(itm, BrowserClassAttributeItem): + self.emit(SIGNAL('sourceFile'), itm.fileName(), + itm.attributeObject().lineno) + + def _editPixmap(self): + """ + Protected slot to handle the open in icon editor popup menu entry. + """ + itmList = self.getSelectedItems([BrowserFileItem]) + + for itm in itmList: + if isinstance(itm, BrowserFileItem): + if itm.isPixmapFile(): + self.emit(SIGNAL('pixmapEditFile'), itm.fileName()) + + def _copyToClipboard(self): + """ + Protected method to copy the text shown for an entry to the clipboard. + """ + itm = self.model().item(self.currentIndex()) + try: + fn = itm.fileName() + except AttributeError: + try: + fn = itm.dirName() + except AttributeError: + fn = "" + + if fn: + cb = QApplication.clipboard() + cb.setText(fn) + + def handleUnittest(self): + """ + Public slot to handle the unittest popup menu entry. + """ + try: + index = self.currentIndex() + itm = self.model().item(index) + pyfn = itm.fileName() + except AttributeError: + pyfn = None + + if pyfn is not None: + self.emit(SIGNAL('unittestOpen'), pyfn) + + def __newToplevelDir(self): + """ + Private slot to handle the New toplevel directory popup menu entry. + """ + dname = QFileDialog.getExistingDirectory(\ + None, + QApplication.translate('Browser', "New toplevel directory"), + "", + QFileDialog.Options(QFileDialog.ShowDirsOnly)) + if dname: + dname = os.path.abspath(Utilities.toNativeSeparators(dname)) + self.__model.addTopLevelDir(dname) + + def __removeToplevel(self): + """ + Private slot to handle the Remove from toplevel popup menu entry. + """ + index = self.currentIndex() + sindex = self.model().mapToSource(index) + self.__model.removeToplevelDir(sindex) + + def __addAsToplevelDir(self): + """ + Private slot to handle the Add as toplevel directory popup menu entry. + """ + index = self.currentIndex() + dname = self.model().item(index).dirName() + self.__model.addTopLevelDir(dname) + + def __findInDirectory(self): + """ + Private slot to handle the Find in directory popup menu entry. + """ + index = self.currentIndex() + searchDir = self.model().item(index).dirName() + + findFilesDialog = e4App().getObject("FindFilesDialog") + findFilesDialog.setSearchDirectory(searchDir) + findFilesDialog.show() + findFilesDialog.raise_() + findFilesDialog.activateWindow() + + def __replaceInDirectory(self): + """ + Private slot to handle the Find&Replace in directory popup menu entry. + """ + index = self.currentIndex() + searchDir = self.model().item(index).dirName() + + replaceFilesDialog = e4App().getObject("ReplaceFilesDialog") + replaceFilesDialog.setSearchDirectory(searchDir) + replaceFilesDialog.show() + replaceFilesDialog.raise_() + replaceFilesDialog.activateWindow() + + def handleProgramChange(self,fn): + """ + Public slot to handle the programChange signal. + """ + self.__model.programChange(os.path.dirname(fn)) + + def wantedItem(self, itm, filter=None): + """ + Public method to check type of an item. + + @param itm the item to check (BrowserItem) + @param filter list of classes to check against + @return flag indicating item is a valid type (boolean) + """ + if filter is None: + filter = self.selectedItemsFilter + for typ in filter: + if isinstance(itm, typ): + return True + return False + + def getSelectedItems(self, filter=None): + """ + Public method to get the selected items. + + @param filter list of classes to check against + @return list of selected items (list of BroweserItem) + """ + selectedItems = [] + indexes = self.selectedIndexes() + for index in indexes: + if index.column() == 0: + itm = self.model().item(index) + if self.wantedItem(itm, filter): + selectedItems.append(itm) + return selectedItems + + def getSelectedItemsCount(self, filter=None): + """ + Public method to get the count of items selected. + + @param filter list of classes to check against + @return count of items selected (integer) + """ + count = 0 + indexes = self.selectedIndexes() + for index in indexes: + if index.column() == 0: + itm = self.model().item(index) + if self.wantedItem(itm, filter): + count += 1 + return count + + def getSelectedItemsCountCategorized(self, filter=None): + """ + Public method to get a categorized count of selected items. + + @param filter list of classes to check against + @return a dictionary containing the counts of items belonging + to the individual filter classes. The keys of the dictionary + are the unicode representation of the classes given in the + filter (i.e. unicode(filterClass)). The dictionary contains + an additional entry with key "sum", that stores the sum of + all selected entries fulfilling the filter criteria. + """ + if filter is None: + filter = self.selectedItemsFilter + categories = {} + categories["sum"] = 0 + for typ in filter: + categories[unicode(typ)] = 0 + + indexes = self.selectedIndexes() + for index in indexes: + if index.column() == 0: + itm = self.model().item(index) + for typ in filter: + if isinstance(itm, typ): + categories["sum"] += 1 + categories[unicode(typ)] += 1 + + return categories + + def saveToplevelDirs(self): + """ + Public slot to save the toplevel directories. + """ + self.__model.saveToplevelDirs() + + def __configure(self): + """ + Private method to open the configuration dialog. + """ + if self.__embeddedBrowser == 1: + e4App().getObject("UserInterface").showPreferences("debuggerGeneralPage") + elif self.__embeddedBrowser == 2: + e4App().getObject("UserInterface").showPreferences("projectBrowserPage")