Modules | Files | Inheritance Tree | Inheritance Graph | Name Index | Config
File: Synopsis/UI/Qt/browse.py
    1| # $Id: browse.py,v 1.11 2002/10/11 06:03:23 chalky Exp $
    2| #
    3| # This file is a part of Synopsis.
    4| # Copyright (C) 2000, 2001 Stefan Seefeld
    5| # Copyright (C) 2000, 2001 Stephen Davies
    6| #
    7| # Synopsis is free software; you can redistribute it and/or modify it
    8| # under the terms of the GNU General Public License as published by
    9| # the Free Software Foundation; either version 2 of the License, or
   10| # (at your option) any later version.
   11| #
   12| # This program is distributed in the hope that it will be useful,
   13| # but WITHOUT ANY WARRANTY; without even the implied warranty of
   14| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   15| # General Public License for more details.
   16| #
   17| # You should have received a copy of the GNU General Public License
   18| # along with this program; if not, write to the Free Software
   19| # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
   20| # 02111-1307, USA.
   21| #
   22| # $Log: browse.py,v $
   23| # Revision 1.11  2002/10/11 06:03:23  chalky
   24| # Use config from project
   25| #
   26| # Revision 1.10  2002/09/28 05:53:31  chalky
   27| # Refactored display into separate project and browser windows. Execute projects
   28| # in the background
   29| #
   30| # Revision 1.9  2002/08/23 04:35:56  chalky
   31| # Use png images w/ Dot. Only show classes in Classes list
   32| #
   33| # Revision 1.8  2002/04/26 01:21:14  chalky
   34| # Bugs and cleanups
   35| #
   36| # Revision 1.7  2002/01/13 09:45:52  chalky
   37| # Show formatted source code (only works with refmanual..)
   38| #
   39| # Revision 1.6  2002/01/09 11:43:41  chalky
   40| # Inheritance pics
   41| #
   42| # Revision 1.5  2002/01/09 10:16:35  chalky
   43| # Centralized navigation, clicking links in (html) docs works.
   44| #
   45| # Revision 1.4  2001/11/09 15:35:04  chalky
   46| # GUI shows HTML pages. just. Source window also scrolls to correct line.
   47| #
   48| # Revision 1.3  2001/11/09 08:06:59  chalky
   49| # More GUI fixes and stuff. Double click on Source Actions for a dialog.
   50| #
   51| # Revision 1.2  2001/11/07 05:58:21  chalky
   52| # Reorganised UI, opening a .syn file now builds a simple project to view it
   53| #
   54| # Revision 1.1  2001/11/05 06:52:11  chalky
   55| # Major backside ui changes
   56| #
   57| 
   58| 
   59| import sys, pickle, Synopsis, cStringIO, string, re
   60| from qt import *
   61| from Synopsis import Config
   62| from Synopsis.Core import AST, Util
   63| from Synopsis.Core.Action import *
   64| from Synopsis.Formatter.ASCII import ASCIIFormatter
   65| from Synopsis.Formatter.HTML import core, Page, ScopePages, FilePages
   66| from Synopsis.Formatter import Dot
   67| from Synopsis.Formatter import ClassTree
   68| from Synopsis.Formatter.HTML.core import FileTree
   69| from igraph import IGraphWindow
   70| 
   71| class BrowserWindow (QSplitter):
   72|     """The browser window displays an AST in the familiar (from JavaDoc)
   73|     three-pane view. In addition to JavaDoc, the right pane can display
   74|     documentation, source or a class hierarchy graph. The bottom-left pane can
   75|     also display classes or files."""
   76| 
   77|     class SelectionListener:
   78|         """Defines the interface for an object that listens to the browser
   79|         selection"""
   80|         def current_decl_changed(self, decl):
   81|             "Called when the current decl changes"
   82|         pass
   83|         
   84|         def current_package_changed(self, package):
   85|             "Called when the current package changes"
   86|         pass
   87| 
   88|         def current_ast_changed(self, ast):
   89|             """Called when the current AST changes. Browser's glob will be
   90|             updated first"""
   91|         pass
   92|     
   93|     def __init__(self, main_window, filename, project_window, config = None):
   94|         QSplitter.__init__(self, main_window.workspace)
   95|         self.main_window = main_window
   96|         self.project_window = project_window
   97|         self.filename = filename
   98|         self.setCaption('Output Window')
   99|         self.config = config
  100|         if not config:
  101|             self.config = Config.Base.HTML
  102| 
  103|         self.classTree = ClassTree.ClassTree()
  104|         self.fileTree = None #FileTree()
  105|         #self.tabs = QTabWidget(self)
  106|         #self.listView = QListView(self)
  107|         self._make_left()
  108|         self._make_right()
  109| 
  110|         #CanvasWindow(parent, main_window, self.project)
  111|         
  112|         self.setSizes([30,70])
  113|         #self.showMaximized()
  114|         #self.show()
  115| 
  116|         self.__ast = None     # The current AST
  117|         self.__package = None # The current package
  118|         self.__decl = None    # The current decl
  119|         self.__listeners = [] # Listeners for changes in current selection
  120| 
  121|         self.glob = AST.Scope('', -1, '', 'Global', ('global',))
  122| 
  123|         # Connect things up
  124|         self.connect(self.right_tab, SIGNAL('currentChanged(QWidget*)'), self.tabChanged)
  125| 
  126|         # Make the menu, to be inserted in the app menu upon window activation
  127|         self._file_menu = QPopupMenu(self.main_window.menuBar())
  128|         self._graph_id = self._file_menu.insertItem("&Graph class inheritance", self.openGraph, Qt.CTRL+Qt.Key_G)
  129| 
  130|         self.__activated = 0
  131|         self.connect(self.parent(), SIGNAL('windowActivated(QWidget*)'), self.windowActivated)
  132| 
  133|         #self.glob.accept(self.classTree)
  134| 
  135|         self.browsers = (
  136|             PackageBrowser(self),
  137|             ClassBrowser(self),
  138|             DocoBrowser(self),
  139|             SourceBrowser(self),
  140|             GraphBrowser(self)
  141|         )
  142| 
  143|         if filename:
  144|             self.load_file()
  145|             self.setCaption(filename+' : Output Window')
  146|         
  147|         main_window.add_window(self)
  148| 
  149| 
  150|     def _make_left(self):
  151|         self.left_split = QSplitter(Qt.Vertical, self)
  152|         self.package_list = QListView(self.left_split)
  153|         self.package_list.addColumn('Package')
  154|         self.left_tab = QTabWidget(self.left_split)
  155|         self.class_list = QListView(self.left_tab)
  156|         self.class_list.addColumn('Class')
  157|         self.file_list = QListView(self.left_tab)
  158|         self.file_list.addColumn('File')
  159|         self.left_tab.addTab(self.class_list, "Classes")
  160|         self.left_tab.addTab(self.file_list, "Files")
  161|         self.left_split.setSizes([30,70])
  162| 
  163|     def _make_right(self):
  164|         self.right_tab = QTabWidget(self)
  165|         self.doco_display = QTextBrowser(self.right_tab)
  166|         self.doco_display.setTextFormat(Qt.RichText)
  167|         self.doco_display.setText("<i>Select a package/namespace to view from the left.")
  168|         self.source_display = QTextBrowser(self.right_tab)
  169|         self.graph_display = IGraphWindow(self.right_tab, self.main_window, self.classTree)
  170|         self.right_tab.addTab(self.doco_display, "Documentation")
  171|         self.right_tab.addTab(self.source_display, "Source")
  172|         self.right_tab.addTab(self.graph_display, "Graph")
  173| 
  174|     def add_listener(self, listener):
  175|         """Adds a listener for changes. The listener must implement the
  176|         SelectionListener interface"""
  177|         if not isinstance(listener, BrowserWindow.SelectionListener):
  178|             raise TypeError, 'Not an implementation of SelectionListener'
  179|         self.__listeners.append(listener)
  180| 
  181|     def current_decl(self):
  182|         """Returns the current declaration being viewed by the project"""
  183|         return self.__decl
  184| 
  185|     def set_current_decl(self, decl):
  186|         """Sets the current declaration being viewed by the project. This will
  187|         also notify all displays"""
  188|         self.__decl = decl
  189| 
  190|         for listener in self.__listeners:
  191|             try: listener.current_decl_changed(decl)
  192|            except:
  193|                import traceback
  194|                print "Exception occurred dispatching to",listener
  195|                traceback.print_exc()
  196|     
  197|     def set_current_package(self, package):
  198|         """Sets the current package (a Scope declaration) being viewed by the
  199|         project. This will also notify all displays"""
  200|         self.__package = package
  201| 
  202|         for listener in self.__listeners:
  203|             listener.current_package_changed(package)
  204| 
  205|     def set_current_ast(self, ast):
  206|         self.__ast = ast
  207|         self.glob.declarations()[:] = ast.declarations()
  208|         self.glob.accept(self.classTree)
  209| 
  210|         for listener in self.__listeners:
  211|             listener.current_ast_changed(ast)
  212|     
  213|     def current_ast(self):
  214|         return self.__ast
  215| 
  216|     def windowActivated(self, widget):
  217|         if self.__activated:
  218|             if widget is not self: self.deactivate()
  219|         elif widget is self: self.activate()
  220|     
  221|     def activate(self):
  222|         self.__activated = 1
  223|         self._menu_id = self.main_window.menuBar().insertItem('AST', self._file_menu)
  224| 
  225|     def deactivate(self):
  226|         self.__activated = 0
  227|         self.main_window.menuBar().removeItem(self._menu_id)
  228| 
  229| 
  230|     def openGraph(self):
  231|         IGraphWindow(self.main_window.workspace, self.main_window,
  232|                     self.classTree).set_class(self.decl.name())
  233| 
  234|     def setGraphEnabled(self, enable):
  235|         self._file_menu.setItemEnabled(self._graph_id, enable)
  236|         if enable:
  237|             self.graph_display.set_class(self.decl.name())
  238|         #else:
  239|         #    self.window.graph_display.hide()
  240| 
  241|     def tabChanged(self, widget):
  242|         self.set_current_decl(self.current_decl())
  243| 
  244|     def load_file(self):
  245|         """Loads the AST from disk."""
  246|         try:
  247|             self.set_current_ast(AST.load(self.filename))
  248|         except Exception, e:
  249|            print e
  250| 
  251| class ListFiller( AST.Visitor ):
  252|     """A visitor that fills in a QListView from an AST"""
  253|     def __init__(self, main, listview, types = None, anti_types = None):
  254|         self.map = {}
  255|         self.main = main
  256|         self.listview = listview
  257|         self.stack = [self.listview]
  258|         self.types = types
  259|         self.anti_types = anti_types
  260|         self.auto_open = 1
  261| 
  262|     def clear(self):
  263|         self.listview.clear()
  264|         self.map = {}
  265|         self.stack = [self.listview]
  266| 
  267|     def fillFrom(self, decl):
  268|         self.addGroup(decl)
  269|         self.listview.setContentsPos(0,0)
  270| 
  271|     def visitDeclaration(self, decl):
  272|         if self.types and decl.type() not in self.types: return
  273|         if self.anti_types and decl.type() in self.anti_types: return
  274|         self.addDeclaration(decl)
  275| 
  276|     def addDeclaration(self, decl):
  277|         item = QListViewItem(self.stack[-1], decl.name()[-1], decl.type())
  278|         self.map[item] = decl
  279|         self.__last = item
  280| 
  281|     def visitGroup(self, group):
  282|         if self.types and group.type() not in self.types: return
  283|         if self.anti_types and group.type() in self.anti_types: return
  284|         self.addGroup(group)
  285| 
  286|     def addGroup(self, group):
  287|         self.addDeclaration(group)
  288|         item = self.__last
  289|         self.stack.append(item)
  290|         for decl in group.declarations(): decl.accept(self)
  291|         self.stack.pop()
  292|         if len(self.stack) <= self.auto_open: self.listview.setOpen(item, 1)
  293| 
  294|     def visitForward(self, fwd): pass
  295| 
  296|     def visitEnum(self, enum):
  297|         if self.types and enum.type() not in self.types: return
  298|         if self.anti_types and enum.type() in self.anti_types: return
  299|         self.addDeclaration(enum)
  300|         item = self.__last
  301|         self.stack.append(item)
  302|         for decl in enum.enumerators(): decl.accept(self)
  303|         self.stack.pop()
  304|         if len(self.stack) <= self.auto_open: self.listview.setOpen(item, 1)
  305| 
  306| class PackageBrowser (BrowserWindow.SelectionListener):
  307|     """Browser that manages the package view"""
  308|     def __init__(self, browser):
  309|         self.__browser = browser
  310|         browser.add_listener(self)
  311| 
  312|         # Create the filler. It only displays a few types
  313|         self.filler = ListFiller(self, browser.package_list, (
  314|             'package', 'module', 'namespace', 'global'))
  315|         self.filler.auto_open = 3
  316| 
  317|         browser.connect(browser.package_list, SIGNAL('selectionChanged(QListViewItem*)'), self.select_package_item)
  318| 
  319|     def select_package_item(self, item):
  320|         """Show a given package (by item)"""
  321|         decl = self.filler.map[item]
  322|         self.__browser.set_current_package(decl)
  323|         self.__browser.set_current_decl(decl)
  324| 
  325|     def DISABLED_current_package_changed(self, decl):
  326|         self.setGraphEnabled(0)
  327|         # Grab the comments and put them in the text view
  328|         os = cStringIO.StringIO()
  329|         for comment in decl.comments():
  330|             os.write(comment.text())
  331|             os.write('<hr>')
  332|         self.__browser.doco_display.setText(os.getvalue())
  333| 
  334|     def current_ast_changed(self, ast):
  335|         self.filler.fillFrom(self.__browser.glob)
  336| 
  337| class ClassBrowser (BrowserWindow.SelectionListener):
  338|     """Browser display that manages the class view"""
  339|     def __init__(self, browser):
  340|         self.__browser = browser
  341|         self.__glob = AST.Scope('', -1, '', 'Global Classes', ('global',))
  342|         browser.add_listener(self)
  343| 
  344|         # Create the filler
  345|         self.filler = ListFiller(self, browser.class_list, None, (
  346|             'Package', 'Module', 'Namespace', 'Global'))
  347| 
  348|         browser.connect(browser.class_list, SIGNAL('selectionChanged(QListViewItem*)'), self.select_decl_item)
  349|         browser.connect(browser.class_list, SIGNAL('expanded(QListViewItem*)'), self.selfish_expansion)
  350| 
  351|     def select_decl_item(self, item):
  352|         """Show a given declaration (by item)"""
  353|         decl = self.filler.map[item]
  354|         self.__browser.set_current_decl(decl)
  355| 
  356|     def current_package_changed(self, package):
  357|         "Refill the tree with the new package as root"
  358|         self.filler.clear()
  359|         self.filler.fillFrom(package)
  360| 
  361|     def selfish_expansion(self, item):
  362|         """Selfishly makes item the only expanded node"""
  363|         if not item.parent(): return
  364|         iter = item.parent().firstChild()
  365|         while iter:
  366|             if iter != item: self.__browser.class_list.setOpen(iter, 0)
  367|             iter = iter.nextSibling()
  368| 
  369|     def current_ast_changed(self, ast):
  370|         self.filler.clear()
  371|         glob_all = self.__browser.glob.declarations()
  372|         classes = lambda decl: decl.type() == 'class'
  373|         glob_cls = filter(classes, glob_all)
  374|         self.__glob.declarations()[:] = glob_cls
  375|         self.filler.fillFrom(self.__glob)
  376| 
  377| class DocoBrowser (BrowserWindow.SelectionListener):
  378|     """Browser that manages the documentation view"""
  379|     class BufferScopePages (ScopePages.ScopePages, Page.BufferPage):
  380|         def __init__(self, manager):
  381|             ScopePages.ScopePages.__init__(self, manager)
  382|             Page.BufferPage._take_control(self)
  383| 
  384|     def __init__(self, browser):
  385|         self.__browser = browser
  386|         browser.add_listener(self)
  387| 
  388|         self.mime_factory = SourceMimeFactory()
  389|         self.mime_factory.set_browser(self)
  390|         self.__browser.doco_display.setMimeSourceFactory(self.mime_factory)
  391| 
  392|         self.__generator = None
  393| 
  394|         self.__getting_mime = 0
  395|         
  396|     def generator(self):
  397|         if not self.__generator:
  398|             self.__generator = self.BufferScopePages(core.manager)
  399|         return self.__generator
  400| 
  401|     def current_decl_changed(self, decl):
  402|         if not self.__browser.doco_display.isVisible():
  403|             # Not visible, so ignore
  404|           return
  405|         if isinstance(decl, AST.Scope):
  406|             # These we can use the HTML scope formatter on
  407|             pages = self.generator()
  408|             pages.process_scope(decl)
  409|             self.__text = pages.get_buffer()
  410|             if self.__getting_mime: return
  411|             context = pages.filename()
  412|             self.__browser.doco_display.setText(self.__text, context)
  413|         elif decl:
  414|             # Need more work to use HTML on this.. use ASCIIFormatter for now
  415|             os = cStringIO.StringIO()
  416|             os.write('<pre>')
  417|             formatter = ASCIIFormatter(os)
  418|             formatter.set_scope(decl.name())
  419|             decl.accept(formatter)
  420|             self.__browser.doco_display.setText(os.getvalue())
  421|     
  422|     def current_ast_changed(self, ast):
  423|         core.configure_for_gui(ast, self.__browser.config)
  424| 
  425|         scope = AST.Scope('',-1,'','','')
  426|         scope.declarations()[:] = ast.declarations()
  427|         self.generator().register_filenames(scope)
  428| 
  429|     def get_mime_data(self, name):
  430|         if name[-16:] == '-inheritance.png':
  431|             # inheritance graph.
  432|             # Horrible Hack Time
  433|         try:
  434|                # Convert to .html name
  435|                html_name = name[:-16] + '.html'
  436|                page, scope = core.manager.filename_info(html_name) # may throw KeyError
  437| 
  438|                super = core.config.classTree.superclasses(scope.name())
  439|                sub = core.config.classTree.subclasses(scope.name())
  440|                if len(super) == 0 and len(sub) == 0:
  441|                    # Skip classes with a boring graph
  442|                return None
  443|                tmp = '/tmp/synopsis-inheritance.png'
  444|                dot_args = ['-o', tmp, '-f', 'png', '-s']
  445|                Dot.toc = core.config.toc
  446|                Dot.nodes = {}
  447|                ast = AST.AST([''], [scope], core.config.types)
  448|                Dot.format(dot_args, ast, None)
  449|                data = QImageDrag(QImage(tmp))
  450|                os.unlink(tmp)
  451|                return data
  452|             except KeyError:
  453|                print "inheritance doesnt have a page!"
  454|         pass
  455|         # Try for html page
  456|         try:
  457|             page, scope = core.manager.filename_info(name)
  458|             if page is self.generator():
  459|                self.__getting_mime = 1
  460|                self.__browser.set_current_decl(scope)
  461|                self.__getting_mime = 0
  462|                return QTextDrag(self.__text, self.__browser.doco_display, '')
  463|         except KeyError:
  464|         pass
  465|         return None
  466|         
  467| 
  468| class SourceMimeFactory (QMimeSourceFactory):
  469|     def set_browser(self, browser): self.__browser = browser
  470|     def data(self, name):
  471|         d = self.__browser.get_mime_data(str(name))
  472|         return d    
  473| 
  474| 
  475| re_tag = re.compile('<(?P<close>/?)(?P<tag>[a-z]+)( class="(?P<class>[^"]*?)")?(?P<href> href="[^"]*?")?( name="[^"]*?")?[^>]*?>')
  476| tags = {
  477|     'file-default' : ('', ''),
  478|     'file-indent' : ('<tt>', '</tt>'),
  479|     'file-linenum' : ('<font color=red><tt>', '</tt></font>'),
  480|     'file-comment' : ('<font color=purple>', '</font>'),
  481|     'file-keyword' : ('<b>', '</b>'),
  482|     'file-string' : ('<font color=#008000>', '</font>'),
  483|     'file-number' : ('<font color=#000080>', '</font>'),
  484| }
  485| def format_source(text):
  486|     """The source relies on stylesheets, and Qt doesn't have powerful enough
  487|     stylesheets. This function manually converts the html..."""
  488|     print '===\n%s\n==='%text
  489|     mo = re_tag.search(text)
  490|     stack = [] # stack of close tags
  491|     result = [] # list of strings for result text
  492|     pos = 0
  493|     while mo:
  494|         start, end, tag = mo.start(), mo.end(), mo.group('tag')
  495|         result.append(text[pos:start])
  496|         #if mo.group('close') != '/':
  497|             #print "OPEN::",tag,mo.group()
  498|         if mo.group('close') == '/':
  499|             # close tag
  500|             result.append(stack.pop())
  501|             #print "CLOSE:",tag,result[-1]
  502|             #print len(stack), stack
  503|         elif tag == 'span':
  504|             # open tag
  505|             span_class = mo.group('class')
  506|             if tags.has_key(span_class):
  507|                open, close = tags[span_class]
  508|                result.append(open)
  509|                stack.append(close)
  510|          else:
  511|                # unknown class
  512|                result.append(mo.group())
  513|                #print "UNKNOWN:",span_class
  514|                stack.append('</span>')
  515|         elif tag == 'a':
  516|             result.append(mo.group())
  517|             if mo.group('href'):
  518|                result.append('<font color=#602000>')
  519|                stack.append('</font></a>')
  520|          else:
  521|                stack.append('</a>')
  522|         elif tag in ('br', 'hr'):
  523|             result.append(mo.group())
  524|         else:
  525|             result.append(mo.group())
  526|             stack.append('</%s>'%tag)
  527|         mo = re_tag.search(text, end)
  528|         pos = end
  529|     result.append(text[pos:])
  530|     text = string.join(result, '')
  531|     #print '===\n%s\n==='%text
  532|     return text
  533| 
  534| 
  535| class SourceBrowser (BrowserWindow.SelectionListener):
  536|     """Browser that manages the source view"""
  537|     class BufferFilePages (FilePages.FilePages, Page.BufferPage):
  538|         def __init__(self, manager):
  539|             FilePages.FilePages.__init__(self, manager)
  540|             Page.BufferPage._take_control(self)
  541| 
  542|     def __init__(self, browser):
  543|         self.__browser = browser
  544|         browser.add_listener(self)
  545| 
  546|         self.mime_factory = SourceMimeFactory()
  547|         self.mime_factory.set_browser(self)
  548|         self.__browser.source_display.setMimeSourceFactory(self.mime_factory)
  549| 
  550|         self.__generator = None
  551| 
  552|         self.__current_file = None
  553| 
  554|         self.__getting_mime = 0
  555| 
  556|         self.__browser.connect(self.__browser.source_display,
  557|             SIGNAL('highlighted(const QString&)'), self.highlighted)
  558| 
  559|     def highlighted(self, text):
  560|         print text
  561| 
  562|     def generator(self):
  563|         if not self.__generator:
  564|             #fileconfig = core.config.obj.FilePages
  565|             #fileconfig.links_path = 'docs/RefManual/syn/%s-links'
  566|             self.__generator = self.BufferFilePages(core.manager)
  567|             self.__generator.register_filenames(None)
  568|         return self.__generator
  569| 
  570|     def current_decl_changed(self, decl):
  571|         if not self.__browser.source_display.isVisible():
  572|             # Not visible, so ignore
  573|           return
  574|         if decl is None:
  575|             self.__browser.source_display.setText('')
  576|             self.__current_file = ''
  577|           return
  578|         file, line = decl.file(), decl.line()
  579|         if self.__current_file != file:
  580|             # Check for empty file
  581|             if not file:
  582|                self.__browser.source_display.setText('')
  583|         return
  584|             print "looking for", file
  585|         
  586|             filepath = string.split(file, os.sep)
  587|             filename = core.config.files.nameOfScopedSpecial('page', filepath)
  588|             page, scope = core.manager.filename_info(filename)
  589|             pages = self.generator()
  590|             if page is pages:
  591|                pages.process_scope(scope)
  592|                self.__text = pages.get_buffer()
  593|                self.__text = format_source(self.__text)
  594|                if self.__getting_mime: return
  595|                context = pages.filename()
  596|                self.__browser.source_display.setText(self.__text, context)
  597|          else:
  598|                # Open the new file and give it line numbers
  599|                text = open(file).read()
  600|                # number the lines
  601|                lines = string.split(text, '\n')
  602|                for line in range(len(lines)): lines[line] = str(line+1)+'| '+lines[line]
  603|                text = string.join(lines, '\n')
  604|                # set text
  605|                self.__browser.source_display.setText(text)
  606|             self.__current_file = file
  607|         # scroll to line
  608|         if type(line) != type(''): # some lines are '' for some reason..
  609|             y = (self.__browser.source_display.fontMetrics().height()+1) * line
  610|             self.__browser.source_display.setContentsPos(0, y)
  611|         #print line, y
  612| 
  613|     def current_ast_changed(self, ast):
  614|         core.configure_for_gui(ast, self.__browser.config)
  615| 
  616|         scope = AST.Scope('',-1,'','','')
  617|         scope.declarations()[:] = ast.declarations()
  618|         self.generator().register_filenames(scope)
  619| 
  620| 
  621| 
  622| class GraphBrowser (BrowserWindow.SelectionListener):
  623|     """Browser that manages the graph view"""
  624|     def __init__(self, browser):
  625|         self.__browser = browser
  626|         browser.add_listener(self)
  627|     def current_decl_changed(self, decl):
  628|         if not self.__browser.graph_display.isVisible():
  629|             # Not visible, so ignore
  630|           return
  631|         if decl is None: return
  632|         self.__browser.graph_display.set_class(decl.name())
  633|