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|