Modules | Files | Inheritance Tree | Inheritance Graph | Name Index | Config
File: Synopsis/UI/Qt/actionwiz.py
    1| # $Id: actionwiz.py,v 1.7 2002/11/11 14:28:41 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: actionwiz.py,v $
   23| # Revision 1.7  2002/11/11 14:28:41  chalky
   24| # New Source Edit dialog, with Insert Rule wizard and new s/path/rules with
   25| # extra features.
   26| #
   27| # Revision 1.6  2002/07/11 09:30:16  chalky
   28| # Added page for configuring the cacher action
   29| #
   30| # Revision 1.5  2002/07/04 06:44:41  chalky
   31| # Can now edit define list for C++ Parser.
   32| #
   33| # Revision 1.4  2002/06/22 07:03:27  chalky
   34| # Updates to GUI - better editing of Actions, and can now execute some.
   35| #
   36| # Revision 1.3  2002/04/26 01:21:14  chalky
   37| # Bugs and cleanups
   38| #
   39| # Revision 1.2  2001/11/09 08:06:59  chalky
   40| # More GUI fixes and stuff. Double click on Source Actions for a dialog.
   41| #
   42| # Revision 1.1  2001/11/05 06:52:11  chalky
   43| # Major backside ui changes
   44| #
   45| 
   46| 
   47| import sys, pickle, Synopsis, cStringIO, types, copy
   48| from qt import *
   49| from Synopsis import Config
   50| from Synopsis.Core import AST, Util
   51| from Synopsis.Core.Action import *
   52| from Synopsis.Formatter.ASCII import ASCIIFormatter
   53| from Synopsis.Formatter.ClassTree import ClassTree
   54| 
   55| from sourceoptionsdialog import SourceOptionsDialog
   56| from sourceinsertwizard import SourceInsertWizard
   57| from sourceeditexclude import SourceEditExclude
   58| from sourceeditsimple import SourceEditSimple
   59| from sourceeditglob import SourceEditGlob
   60| 
   61| class Struct:
   62|     "Dummy class. Initialise with keyword args."
   63|     def __init__(self, **keys):
   64|         for name, value in keys.items(): setattr(self, name, value)
   65| 
   66| def make_relative(base, file):
   67|     """Useful function to make a filename relative to the given base
   68|     filename"""
   69|     if file[:len(base)] == base:
   70|         # Simple case: base is a prefix of file
   71|         return file[len(base):]
   72|     elif file and file[0] == '/' and base[0] == '/':
   73|         # More complex case: must insert ../../'s
   74|         fs = string.split(file, os.sep)
   75|         bs = string.split(base[:-1], os.sep)
   76|         while len(fs) and len(bs) and fs[0] == bs[0]:
   77|             del fs[0]
   78|             del bs[0]
   79|         fs = ['..']*len(bs) + fs
   80|         return string.join(fs, os.sep)
   81|     # If either is not absolute, can't assume anything
   82|     return file
   83| 
   84| class SourceActionEditor:
   85|     """Presents a modal GUI for editing a Source Action"""
   86|     def __init__(self, parent, project):
   87|         self.parent = parent
   88|         self.project = project
   89|         self.action = None
   90|         self.dialog = None 
   91|         self.insert_wizard = None
   92|         self.rules = None
   93|         self.rule_list_items = []
   94|     def edit(self, action):
   95|         """Edits the given action"""
   96|         self.action = action
   97|         self.init_dialog()
   98|         result = self.dialog.exec_loop()
   99|         if result:
  100|             self.keep_changes()
  101|         print "result is",result
  102|     def keep_changes(self):
  103|         dialog, action = self.dialog, self.action
  104|         # If the name is changed, must use Project to rename
  105|         name = str(dialog.NameEdit.text())
  106|         if action.name() != name:
  107|             print "Setting name to ",name
  108|             self.project.actions().rename_action(action, name)
  109|             self.project.actions().action_changed(action)
  110|         # Copy the list of rules back into the action
  111|         self.action.rules()[:] = self.rules
  112| 
  113|     def init_dialog(self):
  114|         dialog, action = self.dialog, self.action
  115|         if not self.dialog:
  116|             dialog = self.dialog = SourceOptionsDialog(self.parent, "source", 1)
  117|             self.dialog.action = action
  118|             dialog.connect(dialog.RuleList, SIGNAL("selectionChanged()"), self.on_selection)
  119|             dialog.connect(dialog.InsertButton, SIGNAL("clicked()"), self.on_insert)
  120|             dialog.connect(dialog.DeleteButton, SIGNAL("clicked()"), self.on_delete)
  121|             dialog.connect(dialog.EditButton, SIGNAL("clicked()"), self.on_edit)
  122|             dialog.connect(dialog.DownButton, SIGNAL("clicked()"), self.on_down)
  123|             dialog.connect(dialog.UpButton, SIGNAL("clicked()"), self.on_up)
  124|             dialog.connect(dialog.TestButton, SIGNAL("clicked()"), self.on_test)
  125|         # Deep copy list of source rules
  126|         self.rules = map(lambda x: x.clone(), action.rules())
  127|         # Set fields from the Action
  128|         dialog.NameEdit.setText(action.name())
  129|         self.update_list()
  130| 
  131|     def on_insert(self):
  132|         # Run the wizard
  133|         self.insert_wiz = wiz = SourceInsertWizard(self.dialog)
  134|         # Add connections
  135|         wiz.connect(wiz, SIGNAL("selected(const QString&)"), self.on_insert_finished)
  136|         wiz.CurrentDirectoryLabel.setText('Current Directory: '+os.getcwd())
  137|         result = wiz.exec_loop()
  138|         print "Wizard result:",result
  139|         # Deal with result
  140|         if not result: return
  141|         base = None
  142|         if wiz.relative_option == 1:
  143|             base = os.getcwd()
  144|         elif wiz.relative_option == 2:
  145|             base = str(wiz.RelativeThisEdit.text())
  146|         if base and base[-1] != '/': base = base + '/'
  147|         print "Base is:",base
  148|         if wiz.rule_type == "simple":
  149|             files = []
  150|             item = wiz.FileList.firstChild()
  151|             while item:
  152|                file = str(item.text(0))
  153|                file = make_relative(base, file)
  154|                files.append(file)
  155|                item = item.nextSibling()
  156|             self.rules.append(SimpleSourceRule(files))
  157|         elif wiz.rule_type == "exclude":
  158|             glob = str(wiz.ExcludeCombo.currentText())
  159|             self.rules.append(ExcludeSourceRule(glob))
  160|         else:
  161|             dirs = []
  162|             item = wiz.DirList.firstChild()
  163|             while item:
  164|                dir = str(item.text(0))
  165|                dir = make_relative(base, dir)
  166|                dirs.append(dir)
  167|                item = item.nextSibling()
  168|             glob = str(wiz.FileGlobEdit.text())
  169|             if wiz.rule_type == 'glob':
  170|                self.rules.append(GlobSourceRule(dirs, glob, 0))
  171|          else:
  172|                self.rules.append(GlobSourceRule(dirs, glob, 1))
  173| 
  174|         # Update the list
  175|         self.update_list()
  176| 
  177|     def on_insert_finished(self, title):
  178|         if str(title) != "Finished":
  179|           return
  180|         # Figure out text to put in the "rule info" box
  181|         wiz = self.insert_wiz
  182|         base = None
  183|         if wiz.relative_option == 1:
  184|             base = os.getcwd()
  185|         elif wiz.relative_option == 2:
  186|             base = str(wiz.RelativeThisEdit.text())
  187|         if base and base[-1] != '/': base = base + '/'
  188|         print "Base is:",base
  189|         if wiz.rule_type == "simple":
  190|             files = []
  191|             item = wiz.FileList.firstChild()
  192|             while item:
  193|                file = str(item.text(0))
  194|                file = make_relative(base, file)
  195|                files.append(file)
  196|                item = item.nextSibling()
  197|             wiz.RuleInfo.setText("Type: Simple\n"+
  198|                "Files:\n "+string.join(files, "\n "))
  199|         elif wiz.rule_type == "exclude":
  200|             wiz.RuleInfo.setText("Type: Exclude\n"+
  201|                "Glob expression:\n "+str(wiz.ExcludeCombo.currentText()))
  202|         else:
  203|             # Glob or recursive glob
  204|             dirs = []
  205|             item = wiz.DirList.firstChild()
  206|             while item:
  207|                dir = str(item.text(0))
  208|                dir = make_relative(base, dir)
  209|                dirs.append(dir)
  210|                item = item.nextSibling()
  211|             wiz.RuleInfo.setText("Type: %s\n"%wiz.rule_type +
  212|                "Directories:\n "+string.join(dirs, "\n ")+
  213|                "\nFiles:\n "+str(wiz.FileGlobEdit.text()))
  214| 
  215|     def update_list(self):
  216|         dialog, action = self.dialog, self.action
  217|         dialog.RuleList.clear()
  218|         items = []
  219|         self.__rule_list_items = items
  220|         # NB: we must reverse the list before adding (and then unreverse the
  221|         # items list later). I don't know why QListView adds things at the top
  222|         # rather than the bottom, and I sure hope it doesn't change later on!
  223|         rules = list(self.rules)
  224|         rules.reverse()
  225|         for rule in rules:
  226|             if rule.type == 'Simple':
  227|                if len(rule.files) <= 1:
  228|                    dir, name = os.path.split(rule.files[0])
  229|                    items.append(QListViewItem(dialog.RuleList, 
  230|                       'Simple', dir, name, ''))
  231|         else:
  232|                    # Make into a tree
  233|                    items.append(QListViewItem(dialog.RuleList,
  234|                       'Simple', '', '(%d files)'%len(rule.files), ''))
  235|                    for file in rule.files:
  236|                       dir, name = os.path.split(file)
  237|                       QListViewItem(items[-1], '', dir, name, '')
  238|             elif rule.type == 'Glob':
  239|                recursive = ''
  240|                if rule.recursive: recursive = 'recursive'
  241|                if len(rule.dirs) <= 1:
  242|                    items.append(QListViewItem(dialog.RuleList, 
  243|                       'Glob', string.join(rule.dirs, '\n'), 
  244|                       rule.glob, recursive))
  245|         else:
  246|                    items.append(QListViewItem(dialog.RuleList, 
  247|                       'Glob', '(%d directories)'%len(rule.dirs), 
  248|                       rule.glob, recursive))
  249|                    for dir in rule.dirs:
  250|                       QListViewItem(items[-1], '', dir, '', '')
  251|             elif rule.type == 'Exclude':
  252|                items.append(QListViewItem(dialog.RuleList, 
  253|                    'Exclude', rule.glob, '', ''))
  254|         items.reverse()
  255| 
  256|     def get_selected_index(self):
  257|         """Returns the index of the selected item, or None"""
  258|         sel_item = self.dialog.RuleList.selectedItem()
  259|         if not sel_item: return None
  260|         index = self.__rule_list_items.index(sel_item)
  261|         if index == -1:
  262|             print "Warning: QListViewItem not found in list"
  263|             return None
  264|         return index
  265| 
  266|     def on_selection(self):
  267|         dialog = self.dialog
  268|         index = self.get_selected_index()
  269|         if index != None:
  270|             dialog.DeleteButton.setEnabled(1)
  271|             dialog.EditButton.setEnabled(1)
  272|             if index > 0:
  273|                dialog.UpButton.setEnabled(1)
  274|          else:
  275|                dialog.UpButton.setEnabled(0)
  276|             if index < len(self.rules) - 1:
  277|                dialog.DownButton.setEnabled(1)
  278|          else:
  279|                dialog.DownButton.setEnabled(0)
  280|         else:
  281|             dialog.DeleteButton.setEnabled(0)
  282|             dialog.EditButton.setEnabled(0)
  283|             dialog.UpButton.setEnabled(0)
  284|             dialog.DownButton.setEnabled(0)
  285|     
  286|     def on_delete(self):
  287|         index = self.get_selected_index()
  288|         if index is None: return
  289|         button = QMessageBox.warning(self.dialog, "Confirm delete", 
  290|             "Really delete this source rule?",
  291|             QMessageBox.Yes, QMessageBox.No)
  292|         if button == QMessageBox.Yes:
  293|             del self.rules[index]
  294|             self.update_list()
  295|             self.on_selection()
  296|     
  297|     def on_up(self):
  298|         index = self.get_selected_index()
  299|         if index is None or index == 0: return
  300|         # Swap with rule above
  301|         this_rule = self.rules[index]
  302|         self.rules[index] = self.rules[index-1]
  303|         self.rules[index-1] = this_rule
  304|         self.update_list()
  305|         self.dialog.RuleList.setSelected(self.__rule_list_items[index-1], 1)
  306|         
  307|     def on_down(self):
  308|         index = self.get_selected_index()
  309|         if index is None or index == len(self.rules)-1: return
  310|         # Swap with rule below
  311|         this_rule = self.rules[index]
  312|         self.rules[index] = self.rules[index+1]
  313|         self.rules[index+1] = this_rule
  314|         self.update_list()
  315|         self.dialog.RuleList.setSelected(self.__rule_list_items[index+1], 1)
  316| 
  317|     def on_edit(self):
  318|         index = self.get_selected_index()
  319|         if index is None: return
  320|         rule = self.rules[index]
  321|         if rule.type == 'Simple':
  322|             # Bring up the dialog for editing simple rules
  323|             dlg = SourceEditSimple(self.dialog)
  324|             dlg.make_relative = make_relative
  325|             # Copy files from rule into ListView
  326|             for file in rule.files:
  327|                QListViewItem(dlg.FileList, file)
  328|             result = dlg.exec_loop()   
  329|             if not result: return
  330|             # Copy ListView back into rule
  331|             rule.files = []
  332|             item = dlg.FileList.firstChild()
  333|             while item:
  334|                rule.files.append(str(item.text(0)))
  335|                item = item.nextSibling()
  336|         elif rule.type == 'Glob':
  337|             # Bring up the dialog for editing glob rules
  338|             dlg = SourceEditGlob(self.dialog)
  339|             dlg.make_relative = make_relative
  340|             # Copy dirs from rule into ListView
  341|             for dir in rule.dirs:
  342|                QListViewItem(dlg.DirList, dir)
  343|             dlg.FileGlobEdit.setText(rule.glob)
  344|             dlg.RecurseCheckBox.setChecked(rule.recursive)
  345|             result = dlg.exec_loop()
  346|             if not result: return
  347|             # Copy ListView, edit and checkbox back into rule
  348|             rule.dirs = []
  349|             item = dlg.DirList.firstChild()
  350|             while item:
  351|                rule.dirs.append(str(item.text(0)))
  352|                item = item.nextSibling()
  353|             rule.glob = str(dlg.FileGlobEdit.text())
  354|             rule.recursive = dlg.RecurseCheckBox.isChecked()
  355|         elif rule.type == 'Exclude':
  356|             # Bring up the dialog for editing exclude rules
  357|             dlg = SourceEditExclude(self.dialog)
  358|             # Copy glob into dlg
  359|             dlg.GlobEdit.setText(rule.glob)
  360|             result = dlg.exec_loop()
  361|             if not result: return
  362|             # Copy glob from dialog back into rule
  363|             rule.glob = str(dlg.GlobEdit.text())
  364|         # Update the display with new info
  365|         self.update_list()
  366| 
  367|     def on_test(self):
  368|         from Synopsis.Core.Executor import ExecutorCreator
  369|         project = self.project
  370|         source_exec = ExecutorCreator(project).create(self.action)
  371|         names = source_exec.get_output_names()
  372|         names = map(lambda x: x[0], names)
  373|         dialog = QDialog(self.dialog, 'test results', 1)
  374|         dialog.setCaption('Files matched by source action %s'%self.action.name())
  375|         dialog.setMinimumSize(600,400)
  376|         vbox = QVBoxLayout(dialog)
  377|         listbox = QListBox(dialog)
  378|         listbox.insertStrList(names)
  379|         button = QPushButton('&Close', dialog)
  380|         dialog.connect(button, SIGNAL('clicked()'), dialog.accept)
  381|         vbox.addWidget(listbox)
  382|         vbox.addWidget(button)
  383|         dialog.exec_loop()
  384| 
  385| class CacherPage (QVBox):
  386|     """The Page (portion of dialog) that displays options for a Cacher
  387|     Action"""
  388|     def __init__(self, parent, action_container):
  389|         QVBox.__init__(self, parent)
  390|         self.__ac = action_container
  391|         self._make_layout()
  392|     def title(self):
  393|         return "Cache"
  394|     def action(self):
  395|         return self.__ac.action
  396|     def set_action(self, action):
  397|         self.__ac.action = action
  398|     def _make_layout(self):
  399|         self.setSpacing(4)
  400|         label = QLabel("<p>Cacher actions can load a stored AST from disk, "+
  401|         "or cache generated AST's between other actions (eg: parsing and "+
  402|         "linking, or linking and formatting).", self)
  403| 
  404|         # Make action name field
  405|         hbox = QHBox(self)
  406|         label = QLabel("Action &name:", hbox)
  407|         self.action_name = QLineEdit(hbox)
  408|         QToolTip.add(self.action_name, "A unique name for this Action")
  409|         label.setBuddy(self.action_name)
  410|         self.connect(self.action_name, SIGNAL('textChanged(const QString&)'), self.onActionName)
  411|         hbox.setSpacing(4)
  412| 
  413|         # Make options
  414|         bgroup = QVButtonGroup("Type of Cacher:", self)
  415|         self.bfile = QRadioButton("Load a single AST file", bgroup)
  416|         self.bdir = QRadioButton("Cache inputs to many files below a directory", bgroup)
  417| 
  418|         label = QLabel("Select the file to load:", self)
  419|         hbox = QHBox(self)
  420|         self.line_file = QLineEdit('', hbox)
  421|         self.brfile = QPushButton('Browse', hbox)
  422|         label = QLabel("Select the directory to cache files in:", self)
  423|         hbox = QHBox(self)
  424|         self.line_dir = QLineEdit('', hbox)
  425|         self.brdir = QPushButton('Browse', hbox)
  426| 
  427|         self.connect(self.bfile, SIGNAL('clicked()'), self.onButtonFile)
  428|         self.connect(self.bdir, SIGNAL('clicked()'), self.onButtonDir)
  429|         self.connect(self.line_file, SIGNAL('textChanged(const QString&)'), self.onFileChanged)
  430|         self.connect(self.line_dir, SIGNAL('textChanged(const QString&)'), self.onDirChanged)
  431|         self.connect(self.brfile, SIGNAL('clicked()'), self.onBrowseFile)
  432|         self.connect(self.brdir, SIGNAL('clicked()'), self.onBrowseDir)
  433| 
  434|     def pre_show(self):
  435|         action = self.action()
  436|         self.action_name.setText(action.name())
  437|         self.line_file.setText(str(action.file or ''))
  438|         self.line_dir.setText(str(action.dir or ''))
  439| 
  440|         if not action.file:
  441|             self.line_file.setEnabled(0)
  442|             self.brfile.setEnabled(0)
  443|         elif not action.dir:
  444|             self.line_dir.setEnabled(0)
  445|             self.brdir.setEnabled(0)
  446|         if action.file:
  447|             self.bfile.setOn(1)
  448|         elif action.dir:
  449|             self.bdir.setOn(1)
  450| 
  451|     def showEvent(self, ev):
  452|         QVBox.showEvent(self, ev)
  453|         # Focus name field
  454|         self.action_name.setFocus()
  455|         self.action_name.selectAll()
  456| 
  457|     def onActionName(self, name):
  458|         self.action().set_name(str(name))
  459| 
  460|     def onButtonFile(self):
  461|         self.action().dir = ''
  462|         self.line_file.setEnabled(1)
  463|         self.brfile.setEnabled(1)
  464|         self.line_dir.setEnabled(0)
  465|         self.brdir.setEnabled(0)
  466| 
  467|     def onButtonDir(self):
  468|         self.action().file = ''
  469|         self.line_dir.setEnabled(1)
  470|         self.brdir.setEnabled(1)
  471|         self.line_file.setEnabled(0)
  472|         self.brfile.setEnabled(0)
  473| 
  474|     def onFileChanged(self, text):
  475|         text = str(text)
  476|         print "file =",text
  477|         self.action().file = text
  478| 
  479|     def onDirChanged(self, text):
  480|         text = str(text)
  481|         print "dir =",text
  482|         self.action().dir = text
  483| 
  484|     def onBrowseFile(self):
  485|         result = QFileDialog.getOpenFileName(str(self.action().file or ''), '*.syn',
  486|             self, 'open', 'Select an AST file to load from')
  487|         result = str(result)
  488|         if result:
  489|             self.line_file.setText(result)
  490| 
  491|     def onBrowseDir(self):
  492|         result = QFileDialog.getExistingDirectory(str(self.action().dir or ''),
  493|             self, 'open', 'Select an AST file to load from')
  494|         result = str(result)
  495|         if result:
  496|             self.line_dir.setText(result)
  497| 
  498| class ParserPage (QVBox):
  499|     """The Page (portion of dialog) that displays options for a Parser
  500|     Action"""
  501|     def __init__(self, parent, action_container):
  502|         QVBox.__init__(self, parent)
  503|         self.__ac = action_container
  504|         self._make_layout()
  505|     def title(self):
  506|         return "Module"
  507|     def action(self):
  508|         return self.__ac.action
  509|     def set_action(self, action):
  510|         self.__ac.action = action
  511|     def _make_layout(self):
  512|         self.setSpacing(4)
  513|         label = QLabel("<p>Parser actions take a list of source files from "+
  514|                "connected Source actions, and pass them through a parser "+
  515|                "by one.", self)
  516| 
  517|         # Make action name field
  518|         hbox = QHBox(self)
  519|         label = QLabel("Action &name:", hbox)
  520|         self.source_name = QLineEdit(hbox)
  521|         QToolTip.add(self.source_name, "A unique name for this Action")
  522|         label.setBuddy(self.source_name)
  523|         self.connect(self.source_name, SIGNAL('textChanged(const QString&)'), self.onActionName)
  524|         hbox.setSpacing(4)
  525| 
  526|         # Make module type area
  527|         label = QLabel("Select a &Parser module to use:", self)
  528|         self.module_list = QListBox(self)
  529|         label.setBuddy(self.module_list)
  530|         self.module_items = {}
  531|         for name in ['C++', 'Python', 'IDL']:
  532|             # Listbox item is automatically added to the list
  533|             item = QListBoxText(self.module_list, name)
  534|             self.module_items[name] = item
  535|         self.connect(self.module_list, SIGNAL('clicked(QListBoxItem*)'), self.onModuleSelected)
  536| 
  537|     def pre_show(self):
  538|         self.source_name.setText(self.action().name())
  539|         config = self.action().config()
  540|         if config and self.module_items.has_key(config.name):
  541|             self.module_list.setCurrentItem(self.module_items[config.name])
  542| 
  543|     def showEvent(self, ev):
  544|         QVBox.showEvent(self, ev)
  545|         # Focus name field
  546|         self.source_name.setFocus()
  547|         self.source_name.selectAll()
  548| 
  549|     def onActionName(self, name):
  550|         self.action().set_name(str(name))
  551| 
  552|     def onModuleSelected(self, item):
  553|         module = str(item.text()) #un-QString-ify it
  554|         print module,"selected"
  555|         action = self.action()
  556|         config = action.config()
  557|         if config is None:
  558|             # New action, create empty struct
  559|             config = Struct()
  560|             action.set_config(config)
  561|         else:
  562|             # Confirm with user
  563|             button = QMessageBox.information(self, "Confirm module change",
  564|                "Warning: Changing the module type may cause configuration\n"+\
  565|                "settings to be lost. Are you sure?", QMessageBox.Yes,
  566|                QMessageBox.No)
  567|             print button
  568|             if button == QMessageBox.No:
  569|                print "User aborted"
  570|         return
  571|         # Changing module for existing action - copy attributes from
  572|         # example config if they're not already set
  573|         config.name = module
  574|         copy = Config.Base.Parser.modules[module]
  575|         for attr, value in copy.__dict__.items():
  576|             if not hasattr(config, attr):
  577|                setattr(config, attr, value)
  578| 
  579| class CppParserPage (QVBox):
  580|     """The Page (portion of dialog) that displays options for a C++ Parser Action"""
  581|     def __init__(self, parent, action_container):
  582|         QVBox.__init__(self, parent)
  583|         self.__ac = action_container
  584|         self._make_layout()
  585|     def title(self):
  586|         return "C++ Options"
  587|     def action(self):
  588|         return self.__ac.action
  589|     def set_action(self, action):
  590|         self.__ac.action = action
  591|     def _make_layout(self):
  592|         self.setSpacing(4)
  593|         config = self.action().config()
  594| 
  595|         # Make options area
  596|         if not hasattr(config, 'main_file'): config.main_file = 0
  597|         self.main_only = QCheckBox("Include declarations from &main file only", self)
  598|         self.main_only.setChecked(config.main_file)
  599|         self.connect(self.main_only, SIGNAL('toggled(bool)'), self.onMainFile)
  600|         # Make include path area
  601|         label = QLabel("&Include paths:", self)
  602|         hbox = QHBox(self)
  603|         self.path_list = QListBox(hbox)
  604|         label.setBuddy(self.path_list)
  605|         self._update_path_list()
  606|         self.connect(self.path_list, SIGNAL('clicked(QListBoxItem*)'), self.onPathSelected)
  607| 
  608|         # Make buttons
  609|         bbox = QVBox(hbox)
  610|         bbox.setSpacing(4)
  611|         self.badd = QPushButton('&Add', bbox)
  612|         self.bremove = QPushButton('&Remove', bbox)
  613|         self.bgcc = QPushButton('Add &GCC paths', bbox)
  614|         QToolTip.add(self.badd, "Add a new path setting")
  615|         QToolTip.add(self.bremove, "Remove the selected path setting")
  616|         self.connect(self.badd, SIGNAL('clicked()'), self.onAddPath)
  617|         self.connect(self.bremove, SIGNAL('clicked()'), self.onRemovePath)
  618|         self.connect(self.bgcc, SIGNAL('clicked()'), self.onAddGCC)
  619| 
  620|         # Make Define area
  621|         label = QLabel("Preprocessor &defines:", self)
  622|         hbox = QHBox(self)
  623|         self.def_list = QListBox(hbox)
  624|         label.setBuddy(self.def_list)
  625|         self._update_def_list()
  626|         self.connect(self.def_list, SIGNAL('clicked(QListBoxItem*)'), self.onDefSelected)
  627| 
  628|         # Make buttons
  629|         bbox = QVBox(hbox)
  630|         bbox.setSpacing(4)
  631|         self.bdadd = QPushButton('&Add', bbox)
  632|         self.bdremove = QPushButton('&Remove', bbox)
  633|         QToolTip.add(self.bdadd, "Add a new define")
  634|         QToolTip.add(self.bdremove, "Remove the selected define")
  635|         self.connect(self.bdadd, SIGNAL('clicked()'), self.onAddDefine)
  636|         self.connect(self.bdremove, SIGNAL('clicked()'), self.onRemoveDefine)
  637| 
  638| 
  639|     def _update_path_list(self):
  640|         config = self.action().config()
  641|         self.path_list.clear()
  642|         self.path_items = {}
  643|         if not hasattr(config, 'include_path') or \
  644|                type(config.include_path) != type([]):
  645|             config.include_path = []
  646|         for path in config.include_path:
  647|             # Listbox item is automatically added to the list
  648|             print "path:",path
  649|             item = QListBoxText(self.path_list, path)
  650|             self.path_items[path] = item
  651| 
  652|     def _update_def_list(self):
  653|         config = self.action().config()
  654|         self.def_list.clear()
  655|         self.def_items = {}
  656|         if not hasattr(config, 'defines') or \
  657|                type(config.defines) != type([]):
  658|             config.defines = []
  659|         for define in config.defines:
  660|             # Listbox item is automatically added to the list
  661|             print "Define:", define
  662|             item = QListBoxText(self.def_list, define)
  663|             self.def_items[define] = item
  664| 
  665|     def pre_show(self):
  666|         pass
  667| 
  668|     def showEvent(self, ev):
  669|         QVBox.showEvent(self, ev)
  670|         # Focus name field
  671|         #self.source_name.setFocus()
  672|         #self.source_name.selectAll()
  673| 
  674|     def onPathSelected(self, item):
  675|         path = item.text()
  676| 
  677|     def onDefSelected(self, item):
  678|         pass
  679| 
  680|     def onAddPath(self):
  681|         dir = QFileDialog.getExistingDirectory()
  682|         dir = str(dir)
  683|         if dir:
  684|             self.action().config().include_path.append(dir)
  685|             self._update_path_list()
  686| 
  687|     def onRemovePath(self):
  688|         pass
  689| 
  690|     def onAddGCC(self):
  691|         config = self.action().config()
  692|         config.include_path.extend([
  693|             '/usr/include/g++-3/',
  694|             '/usr/lib/gcc-lib/i386-linux/2.95.4/include'
  695|         ])
  696|         print "config:",config.include_path
  697|         self._update_list()
  698| 
  699|     def onMainFile(self, on):
  700|         self.action().config().main_file = not not on
  701| 
  702|     def onAddDefine(self):
  703|         define, okay = QInputDialog.getText("Add define",
  704|             "Enter the new define. Eg: 'DEBUG' or 'MODE=1' without quotes.")
  705|         define = str(define)
  706|         if okay and define:
  707|             self.action().config().defines.append(define)
  708|             self._update_def_list()
  709|     
  710|     def onRemoveDefine(self):
  711|         curr = self.def_list.currentItem()
  712|         if curr < 0: return
  713|         del self.action().config().defines[curr]
  714| 
  715| class FormatterPage (QVBox):
  716|     """The Page (portion of dialog) that displays options for a Formatter
  717|     Action"""
  718|     def __init__(self, parent, action_container):
  719|         QVBox.__init__(self, parent)
  720|         self.__ac = action_container
  721|         self._make_layout()
  722|     def title(self):
  723|         return "Module"
  724|     def action(self):
  725|         return self.__ac.action
  726|     def set_action(self, action):
  727|         self.__ac.action = action
  728|     def _make_layout(self):
  729|         self.setSpacing(4)
  730| 
  731|         # Make action name field
  732|         hbox = QHBox(self)
  733|         label = QLabel("Formatter Action &name:", hbox)
  734|         self.formatter_name = QLineEdit(hbox)
  735|         QToolTip.add(self.formatter_name, "A unique name for this Formatter Action")
  736|         label.setBuddy(self.formatter_name)
  737|         self.connect(self.formatter_name, SIGNAL('textChanged(const QString&)'), self.onActionName)
  738|         hbox.setSpacing(4)
  739| 
  740|     def pre_show(self):
  741|         self.formatter_name.setText(self.action().name())
  742|     
  743|     def showEvent(self, ev):
  744|         QVBox.showEvent(self, ev)
  745|         # Focus name field
  746|         self.formatter_name.setFocus()
  747|         self.formatter_name.selectAll()
  748| 
  749|     def onActionName(self, name):
  750|         self.action().set_name(str(name))
  751| 
  752| 
  753| class DetailsPage (QVBox):
  754|     """The Page (portion of dialog) that displays config details"""
  755|     def __init__(self, parent, action_container):
  756|         QVBox.__init__(self, parent)
  757|         self.__ac = action_container
  758|         self._make_layout()
  759|     def title(self):
  760|         return "Details"
  761|     def action(self):
  762|         return self.__ac.action
  763|     def set_action(self, action):
  764|         self.__ac.action = action
  765|     def _make_layout(self):
  766|         self.setSpacing(4)
  767|         label = QLabel("<p>Configuration options are listed below. Most have"+
  768|             "comments which you can see by holding your mouse over them.", self)
  769|         # Make properties area
  770|         self.listview = QListView(self)
  771|         self.listview.addColumn('Property', 100)
  772|         self.listview.addColumn('Value')
  773|         self.listview.setRootIsDecorated(1)
  774| 
  775|     def pre_show(self):
  776|         # Fill properties table
  777|         self.listview.clear()
  778|         config = self.action().config()
  779|         self._fill_list(config, self.listview)
  780| 
  781|     def _fill_list(self, obj, parent):
  782|         attrs = obj.__dict__.keys()
  783|         attrs.sort()
  784|         for attr in attrs:
  785|             if attr in ('__name__', '__doc__', '__module__'):
  786|                continue
  787|             value = getattr(obj, attr)
  788|             if type(value) in (types.InstanceType, types.ClassType):
  789|                item = QListViewItem(parent, attr)
  790|                self._fill_list(value, item)
  791|          else:
  792|                item = QListViewItem(parent, attr, str(value))
  793| 
  794| 
  795| class ActionDialog (QTabDialog):
  796|     def __init__(self, parent, action, project):
  797|         QTabDialog.__init__(self, parent, 'Action Properties', 1, 
  798|             Qt.WStyle_Customize | Qt.WStyle_NormalBorder | Qt.WStyle_Title)
  799|         self.setCaption('Action Properties')
  800|         self.project = project
  801|         self.action = action
  802|         self.setMinimumSize(600,400)
  803|         self.page_classes = self._get_pages()
  804|         if not self.page_classes: return
  805|         self.pages = []
  806|         for page_class in self.page_classes:
  807|             page = page_class(self, self)
  808|             self.pages.append(page)
  809|             self.addTab(page, page.title())
  810|             page.pre_show()
  811|         self.setOkButton("Close")
  812|         self.exec_loop()
  813|         self.project.actions().action_changed(self.action)
  814| 
  815|     def _get_pages(self):
  816|         if isinstance(self.action, SourceAction):
  817|             return [SourcePage]
  818|         elif isinstance(self.action, ParserAction):
  819|             return [ParserPage, CppParserPage, DetailsPage]
  820|         elif isinstance(self.action, FormatAction):
  821|             return [FormatterPage, DetailsPage]
  822|         elif isinstance(self.action, CacherAction):
  823|             return [CacherPage]
  824|         return None
  825| 
  826| 
  827| 
  828| class AddWizard (QWizard):
  829|     def __init__(self, parent, action, project):
  830|         QWizard.__init__(self, parent, 'AddWizard', 1)
  831|         self.setMinimumSize(600,400)
  832|         self.action = action
  833|         self.project = project
  834| 
  835|         self.title_font = QFont('Helvetica', 20, QFont.Bold)
  836|         self.title_palette = QPalette(self.palette())
  837|         self.title_palette.setColor(QPalette.Normal, QColorGroup.Foreground, Qt.white)
  838|         self.title_palette.setColor(QPalette.Normal, QColorGroup.Background, Qt.darkBlue)
  839|         self.title_label = QLabel('', self)
  840|         self.title_label.setFrameStyle(QFrame.Panel | QFrame.Sunken)
  841|         self.title_label.setLineWidth(1)
  842|         self.title_label.setMidLineWidth(1)
  843|         self.title_label.setPalette(self.title_palette)
  844|         self.title_label.setFont(self.title_font)
  845| 
  846|         self._makeStartPage()
  847|         self._makeSourcePage()
  848|         self._makeParserPage()
  849|         self._makeCacherPage()
  850|         self._makeFormatterPage()
  851|         self._makeFormatPropPage()
  852|         # Finish page goes last
  853|         self._makeFinishPage()
  854| 
  855|     def _makeStartPage(self):
  856|         vbox = QVBox(self)
  857|         vbox.layout().setSpacing(4)
  858|         QLabel('What type of action would you like to create?', vbox)
  859|         group = QButtonGroup(1, Qt.Horizontal, "&Action type", vbox)
  860|         radio_source = QRadioButton('&Source - Specifies, monitors and loads source code files', group)
  861|         radio_parser = QRadioButton('&Parser - Reads source files and parses them into ASTs', group)
  862|         radio_linker = QRadioButton('&Linker - Operates on one or more ASTs, typically linking various things together', group)
  863|         radio_cacher = QRadioButton('File &Cache - Stores an AST to disk to speed reprocessing', group)
  864|         radio_format = QRadioButton('&Formatter - Exports an AST to some format, eg: HTML, PDF', group)
  865|         self.__action_type_group = group
  866|         self.__action_types = {
  867|             radio_source: 'source', radio_parser: 'parser', radio_linker: 'linker',
  868|             radio_format: 'format', radio_cacher: 'cacher' }
  869|         self.connect(group, SIGNAL('clicked(int)'), self.onActionType)
  870| 
  871|         self.addPage(vbox, "Select action type")
  872|         self.setNextEnabled(vbox, 0)
  873|         self.setHelpEnabled(vbox, 0)
  874|         self.start = vbox
  875|         self.old_action_type = None
  876| 
  877|     def onActionType(self, id):
  878|         t = self.__action_types[self.__action_type_group.find(id)]
  879|         old = self.action
  880|         x, y, name = old.x(), old.y(), old.name()
  881| 
  882|         if name[0:4] == 'New ' and name[-7:] == ' action':
  883|             name = 'New %s action' % t
  884|         
  885|         # Make old page not appropriate
  886|         if self.old_action_type and self.old_action_type != t:
  887|             o = self.old_action_type
  888|             if o == 'source': self.setAppropriate(self.source, 0)
  889|             elif o == 'parser': self.setAppropriate(self.parser, 0)
  890|             elif o == 'linker': pass
  891|             elif o == 'cacher': self.setAppropriate(self.cacher, 0)
  892|             elif o == 'format':
  893|                self.setAppropriate(self.formatter, 0)
  894|                self.setAppropriate(self.formatProp, 0)
  895|         self.old_action_type = t
  896| 
  897|         # Create Action depending on selection and set next page
  898|         if t == 'source':
  899|             self.action = SourceAction(x, y, name)
  900|             self.setAppropriate(self.source, 1)
  901|         elif t == 'parser':
  902|             self.action = ParserAction(x, y, name)
  903|             self.setAppropriate(self.parser, 1)
  904|         elif t == 'linker':
  905|             self.action = LinkerAction(x, y, name)
  906|             # Find the class in the config dictionary
  907|             config_class = Config.Base.Linker.modules['Linker']
  908|             config = Struct(name=config_class.name)
  909|             self.action.set_config( config )
  910|             # Copy attributes from example config
  911|             self._copyAttrs(config, config_class, 1)
  912|         elif t == 'cacher':
  913|             self.action = CacherAction(x, y, name)
  914|             self.setAppropriate(self.cacher, 1)
  915|         elif t == 'format':
  916|             self.action = FormatAction(x, y, name)
  917|             self.setAppropriate(self.formatter, 1)
  918| 
  919|         if self.action is not old:
  920|             # allow user to move to next screen if Action changed
  921|             self.setNextEnabled(self.start, 1)
  922| 
  923|     def _makeSourcePage(self):
  924|         page = SourcePage(self, self)
  925|         self.addPage(page, "Source action config")
  926|         self.setHelpEnabled(page, 0)
  927|         self.setAppropriate(page, 0)
  928|         self.source = page
  929| 
  930|     def _makeParserPage(self):
  931|         page = ParserPage(self, self)
  932|         self.addPage(page, "Parser action config")
  933|         self.setHelpEnabled(page, 0)
  934|         self.setAppropriate(page, 0)
  935|         self.parser = page
  936| 
  937|     def _makeCacherPage(self):
  938|         page = CacherPage(self, self)
  939|         self.addPage(page, "Cacher action config")
  940|         self.setHelpEnabled(page, 0)
  941|         self.setAppropriate(page, 0)
  942|         self.cacher = page
  943| 
  944|     def _makeFormatterPage(self):
  945|         vbox = QVBox(self)
  946|         vbox.layout().setSpacing(4)
  947|         QLabel('Which Formatter module do you want to use?', vbox)
  948|         group = QButtonGroup(1, Qt.Horizontal, "&Formatter module", vbox)
  949|         radio_html = QRadioButton('&HTML - Generates a static website', group)
  950|         radio_docbook = QRadioButton('&DocBook - Generates a DocBook book', group)
  951|         radio_texinfo = QRadioButton('&TexInfo - Generates a TexInfo manual', group)
  952|         radio_dia = QRadioButton('D&ia - Generates diagrams for Dia', group)
  953|         radio_dot = QRadioButton('&GraphViz - Generates diagrams with GraphViz', group)
  954|         radio_dump = QRadioButton('D&ump - Dumps the AST for debugging', group)
  955|         radio_other = QRadioButton('&Other - Choose any module (case matters)', group)
  956|         self.formatter_edit_other = QLineEdit('<module name>', vbox)
  957|         self.formatter_edit_other.setEnabled(0)
  958|         self.__format_module_group = group
  959|         self.__format_modules = {
  960|             radio_html: 'HTML', radio_docbook: 'DocBook', 
  961|             radio_texinfo: 'TexInfo', radio_dia: 'Dia', radio_dot: 'Dot',
  962|             radio_dump: 'DUMP', radio_other: 'other' }
  963|         self.connect(group, SIGNAL('clicked(int)'), self.onFormatModule)
  964| 
  965|         self.addPage(vbox, "Select Formatter Module")
  966|         self.setNextEnabled(vbox, 0)
  967|         self.setHelpEnabled(vbox, 0)
  968|         self.setAppropriate(vbox, 0)
  969|         self.formatter = vbox
  970| 
  971|     def onFormatModule(self, id):
  972|         # Find out which option was selected
  973|         t = self.__format_modules[self.__format_module_group.find(id)]
  974| 
  975|         # Decide which page to go to next..
  976|         if t == 'other':
  977|             self.formatter_edit_other.setEnabled(1)
  978|             # Create an empty config with the specified name
  979|             name = self.formatter_edit_other.text()
  980|             self.action.set_config(Struct(name=name))
  981|         else:
  982|             self.formatter_edit_other.setEnabled(0)
  983|             # Find the class in the config dictionary
  984|             config_class = Config.Base.Formatter.modules[t]
  985|             config = Struct(name=config_class.name)
  986|             self.action.set_config( config )
  987|             # Copy attributes from example config
  988|             self._copyAttrs(config, config_class, 1)
  989| 
  990| 
  991|         # Enable the Next button
  992|         self.setNextEnabled(self.formatter, 1)
  993|         self.setAppropriate(self.formatProp, 1)
  994| 
  995|     def _copyAttrs(self, dest, src, overwrite):
  996|         """Copies attributes from src to dest objects. src may be a class, and
  997|         recursive structs/classes are copied properly. Note that the copy
  998|         module doesn't copy classes, so we do that bit ourselves"""
  999|         for attr, value in src.__dict__.items():
 1000|             if type(value) in (types.InstanceType, types.ClassType):
 1001|                if not hasattr(dest, attr): setattr(dest, attr, Struct())
 1002|                self._copyAttrs(getattr(dest, attr), value, overwrite)
 1003|             elif type(value) in (types.FunctionType,):
 1004|                continue
 1005|             elif attr in ('__doc__', '__module__', '__name__'):
 1006|                continue
 1007|             elif overwrite or not hasattr(config, attr):
 1008|                setattr(dest, attr, copy.deepcopy(value))
 1009| 
 1010|     def _makeFormatPropPage(self):
 1011|         page = FormatterPage(self, self)
 1012|         self.addPage(page, "Formatter action properties")
 1013|         self.setHelpEnabled(page, 0)
 1014|         self.setAppropriate(page, 0)
 1015|         self.formatProp = page
 1016| 
 1017|     def onActionName(self, name):
 1018|         self.action.set_name(name)
 1019| 
 1020|     def onSourceAddPath(self, name):
 1021|         pass
 1022| 
 1023|     def _makeFinishPage(self):
 1024|         vbox = QVBox(self)
 1025|         QLabel('You have finished creating your new action.', vbox)
 1026|         self.addPage(vbox, "Finished")
 1027|         self.setFinish(vbox, 1)
 1028|         self.setFinishEnabled(vbox, 1)
 1029|         self.setHelpEnabled(vbox, 0)
 1030|         self.finish = vbox
 1031| 
 1032|     def showPage(self, page):
 1033|         if page in (self.source, self.formatProp):
 1034|             page.pre_show()
 1035|         QWizard.showPage(self, page)
 1036| 
 1037|         if page is self.finish:
 1038|             self.finishButton().setFocus()
 1039| 
 1040|     def layOutTitleRow(self, hbox, title):
 1041|         self.title_label.setText(title)
 1042|         hbox.add(self.title_label)
 1043|         
 1044|