diff -r 000000000000 -r de9c2efb9d02 Templates/TemplateViewer.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Templates/TemplateViewer.py Mon Dec 28 16:03:33 2009 +0000 @@ -0,0 +1,905 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2005 - 2009 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a template viewer and associated classes. +""" + +import datetime +import os +import sys +import re +import cStringIO + +from PyQt4.QtCore import * +from PyQt4.QtGui import * + +from E4Gui.E4Application import e4App + +from TemplatePropertiesDialog import TemplatePropertiesDialog +from TemplateMultipleVariablesDialog import TemplateMultipleVariablesDialog +from TemplateSingleVariableDialog import TemplateSingleVariableDialog + +import Preferences + +from E4XML.XMLUtilities import make_parser +from E4XML.XMLErrorHandler import XMLErrorHandler, XMLFatalParseError +from E4XML.XMLEntityResolver import XMLEntityResolver +from E4XML.TemplatesHandler import TemplatesHandler +from E4XML.TemplatesWriter import TemplatesWriter + +import UI.PixmapCache +import Utilities + +from E4Gui.E4Application import e4App + +class TemplateGroup(QTreeWidgetItem): + """ + Class implementing a template group. + """ + def __init__(self, parent, name, language = "All"): + """ + Constructor + + @param parent parent widget of the template group (QWidget) + @param name name of the group (string) + @param language programming language for the group (string) + """ + self.name = name + self.language = language + self.entries = {} + + QTreeWidgetItem.__init__(self, parent, [name]) + + if Preferences.getTemplates("ShowTooltip"): + self.setToolTip(0, language) + + def setName(self, name): + """ + Public method to update the name of the group. + + @param name name of the group (string) + """ + self.name = name + self.setText(0, name) + + def getName(self): + """ + Public method to get the name of the group. + + @return name of the group (string) + """ + return self.name + + def setLanguage(self, language): + """ + Public method to update the name of the group. + + @param language programming language for the group (string) + """ + self.language = language + if Preferences.getTemplates("ShowTooltip"): + self.setToolTip(0, language) + + def getLanguage(self): + """ + Public method to get the name of the group. + + @return language of the group (string) + """ + return self.language + + def addEntry(self, name, description, template, quiet = False): + """ + Public method to add a template entry to this group. + + @param name name of the entry (string) + @param description description of the entry to add (string) + @param template template text of the entry (string) + @param quiet flag indicating quiet operation (boolean) + """ + if name in self.entries: + if not quiet: + QMessageBox.critical(None, + QApplication.translate("TemplateGroup", "Add Template"), + QApplication.translate("TemplateGroup", + """<p>The group <b>{0}</b> already contains a""" + """ template named <b>{1}</b>.</p>""")\ + .format(self.name, name)) + return + + self.entries[name] = TemplateEntry(self, name, description, template) + + if Preferences.getTemplates("AutoOpenGroups") and not self.isExpanded(): + self.setExpanded(True) + + def removeEntry(self, name): + """ + Public method to remove a template entry from this group. + + @param name name of the entry to be removed (string) + """ + if name in self.entries: + index = self.indexOfChild(self.entries[name]) + self.takeChild(index) + del self.entries[name] + + if len(self.entries) == 0: + if Preferences.getTemplates("AutoOpenGroups") and self.isExpanded(): + self.setExpanded(False) + + def removeAllEntries(self): + """ + Public method to remove all template entries of this group. + """ + for name in self.entries.keys()[:]: + self.removeEntry(name) + + def hasEntry(self, name): + """ + Public method to check, if the group has an entry with the given name. + + @param name name of the entry to check for (string) + @return flag indicating existence (boolean) + """ + return name in self.entries + + def getEntry(self, name): + """ + Public method to get an entry. + + @param name name of the entry to retrieve (string) + @return reference to the entry (TemplateEntry) + """ + try: + return self.entries[name] + except KeyError: + return None + + def getEntryNames(self, beginning): + """ + Public method to get the names of all entries, who's name starts with the + given string. + + @param beginning string denoting the beginning of the template name + (string) + @return list of entry names found (list of strings) + """ + names = [] + for name in self.entries: + if name.startswith(beginning): + names.append(name) + + return names + + def getAllEntries(self): + """ + Public method to retrieve all entries. + + @return list of all entries (list of TemplateEntry) + """ + return self.entries.values() + +class TemplateEntry(QTreeWidgetItem): + """ + Class immplementing a template entry. + """ + def __init__(self, parent, name, description, templateText): + """ + Constructor + + @param parent parent widget of the template entry (QWidget) + @param name name of the entry (string) + @param description descriptive text for the template (string) + @param templateText text of the template entry (string) + """ + self.name = name + self.description = description + self.template = templateText + self.__extractVariables() + + QTreeWidgetItem.__init__(self, parent, [self.__displayText()]) + if Preferences.getTemplates("ShowTooltip"): + self.setToolTip(0, self.template) + + def __displayText(self): + """ + Private method to generate the display text. + + @return display text (string) + """ + if self.description: + txt = "{0} - {1}".format(self.name, self.description) + else: + txt = self.name + return txt + + def setName(self, name): + """ + Public method to update the name of the entry. + + @param name name of the entry (string) + """ + self.name = name + self.setText(0, self.__displayText()) + + def getName(self): + """ + Public method to get the name of the entry. + + @return name of the entry (string) + """ + return self.name + + def setDescription(self, description): + """ + Public method to update the description of the entry. + + @param description description of the entry (string) + """ + self.description = description + self.setText(0, self.__displayText()) + + def getDescription(self): + """ + Public method to get the description of the entry. + + @return description of the entry (string) + """ + return self.description + + def getGroupName(self): + """ + Public method to get the name of the group this entry belongs to. + + @return name of the group containing this entry (string) + """ + return self.parent().getName() + + def setTemplateText(self, templateText): + """ + Public method to update the template text. + + @param templateText text of the template entry (string) + """ + self.template = templateText + self.__extractVariables() + if Preferences.getTemplates("ShowTooltip"): + self.setToolTip(0, self.template) + + def getTemplateText(self): + """ + Public method to get the template text. + + @return the template text (string) + """ + return self.template + + def getExpandedText(self, varDict, indent): + """ + Public method to get the template text with all variables expanded. + + @param varDict dictionary containing the texts of each variable + with the variable name as key. + @param indent indentation of the line receiving he expanded + template text (string) + @return a tuple of the expanded template text (string), the + number of lines (integer) and the length of the last line (integer) + """ + txt = self.template + for var, val in varDict.items(): + if var in self.formatedVariables: + txt = self.__expandFormattedVariable(var, val, txt) + else: + txt = txt.replace(var, val) + sepchar = Preferences.getTemplates("SeparatorChar") + txt = txt.replace("%s%s" % (sepchar, sepchar), sepchar) + prefix = "%s%s" % (os.linesep, indent) + trailingEol = txt.endswith(os.linesep) + lines = txt.splitlines() + lineCount = len(lines) + lineLen = len(lines[-1]) + txt = prefix.join(lines).lstrip() + if trailingEol: + txt = "%s%s" % (txt, os.linesep) + lineCount += 1 + lineLen = 0 + return txt, lineCount, lineLen + + def __expandFormattedVariable(self, var, val, txt): + """ + Private method to expand a template variable with special formatting. + + @param var template variable name (string) + @param val value of the template variable (string) + @param txt template text (string) + """ + t = "" + for line in txt.splitlines(): + ind = line.find(var) + if ind >= 0: + format = var[1:-1].split(':', 1)[1] + if format == 'rl': + prefix = line[:ind] + postfix = line [ind + len(var):] + for v in val.splitlines(): + t = "%s%s%s%s%s" % (t, os.linesep, prefix, v, postfix) + elif format == 'ml': + indent = line.replace(line.lstrip(), "") + prefix = line[:ind] + postfix = line[ind + len(var):] + count = 0 + for v in val.splitlines(): + if count: + t = "%s%s%s%s" % (t, os.linesep, indent, v) + else: + t = "%s%s%s%s" % (t, os.linesep, prefix, v) + count += 1 + t = "%s%s" % (t, postfix) + else: + t = "%s%s%s" % (t, os.linesep, line) + else: + t = "%s%s%s" % (t, os.linesep, line) + return "".join(t.splitlines(1)[1:]) + + def getVariables(self): + """ + Public method to get the list of variables. + + @return list of variables (list of strings) + """ + return self.variables + + def __extractVariables(self): + """ + Private method to retrieve the list of variables. + """ + sepchar = Preferences.getTemplates("SeparatorChar") + variablesPattern = \ + re.compile(r"""\%s[a-zA-Z][a-zA-Z0-9_]*(?::(?:ml|rl))?\%s""" % \ + (sepchar, sepchar)) + variables = variablesPattern.findall(self.template) + self.variables = [] + self.formatedVariables = [] + for var in variables: + if not var in self.variables: + self.variables.append(var) + if var.find(':') >= 0 and not var in self.formatedVariables: + self.formatedVariables.append(var) + +class TemplateViewer(QTreeWidget): + """ + Class implementing the template viewer. + """ + def __init__(self, parent, viewmanager): + """ + Constructor + + @param parent the parent (QWidget) + @param viewmanager reference to the viewmanager object + """ + QTreeWidget.__init__(self, parent) + + self.viewmanager = viewmanager + self.groups = {} + + self.setHeaderLabels(["Template"]) + self.header().hide() + self.header().setSortIndicator(0, Qt.AscendingOrder) + self.setRootIsDecorated(True) + self.setAlternatingRowColors(True) + + self.__menu = QMenu(self) + self.applyAct = \ + self.__menu.addAction(self.trUtf8("Apply"), self.__templateItemActivated) + self.__menu.addSeparator() + self.__menu.addAction(self.trUtf8("Add entry..."), self.__addEntry) + self.__menu.addAction(self.trUtf8("Add group..."), self.__addGroup) + self.__menu.addAction(self.trUtf8("Edit..."), self.__edit) + self.__menu.addAction(self.trUtf8("Remove"), self.__remove) + self.__menu.addSeparator() + self.__menu.addAction(self.trUtf8("Save"), self.__save) + self.__menu.addAction(self.trUtf8("Import..."), self.__import) + self.__menu.addAction(self.trUtf8("Export..."), self.__export) + self.__menu.addSeparator() + self.__menu.addAction(self.trUtf8("Help about Templates..."), self.__showHelp) + self.__menu.addSeparator() + self.__menu.addAction(self.trUtf8("Configure..."), self.__configure) + + self.__backMenu = QMenu(self) + self.__backMenu.addAction(self.trUtf8("Add group..."), self.__addGroup) + self.__backMenu.addSeparator() + self.__backMenu.addAction(self.trUtf8("Save"), self.__save) + self.__backMenu.addAction(self.trUtf8("Import..."), self.__import) + self.__backMenu.addAction(self.trUtf8("Export..."), self.__export) + self.__backMenu.addSeparator() + self.__backMenu.addAction(self.trUtf8("Help about Templates..."), self.__showHelp) + self.__backMenu.addSeparator() + self.__backMenu.addAction(self.trUtf8("Configure..."), self.__configure) + + self.setContextMenuPolicy(Qt.CustomContextMenu) + self.connect(self, SIGNAL("customContextMenuRequested(const QPoint &)"), + self.__showContextMenu) + self.connect(self, SIGNAL("itemActivated(QTreeWidgetItem *, int)"), + self.__templateItemActivated) + + self.setWindowIcon(UI.PixmapCache.getIcon("eric.png")) + + def __resort(self): + """ + Private method to resort the tree. + """ + self.sortItems(self.sortColumn(), self.header().sortIndicatorOrder()) + + def __templateItemActivated(self, itm = None, col = 0): + """ + Private slot to handle the activation of an item. + + @param itm reference to the activated item (QTreeWidgetItem) + @param col column the item was activated in (integer) + """ + itm = self.currentItem() + if isinstance(itm, TemplateEntry): + self.applyTemplate(itm) + + def __showContextMenu(self, coord): + """ + Private slot to show the context menu of the list. + + @param coord the position of the mouse pointer (QPoint) + """ + itm = self.itemAt(coord) + coord = self.mapToGlobal(coord) + if itm is None: + self.__backMenu.popup(coord) + else: + self.applyAct.setEnabled(self.viewmanager.activeWindow() is not None) + self.__menu.popup(coord) + + def __addEntry(self): + """ + Private slot to handle the Add Entry context menu action. + """ + itm = self.currentItem() + if isinstance(itm, TemplateGroup): + groupName = itm.getName() + else: + groupName = itm.getGroupName() + + dlg = TemplatePropertiesDialog(self) + dlg.setSelectedGroup(groupName) + if dlg.exec_() == QDialog.Accepted: + name, description, groupName, template = dlg.getData() + self.addEntry(groupName, name, description, template) + + def __addGroup(self): + """ + Private slot to handle the Add Group context menu action. + """ + dlg = TemplatePropertiesDialog(self, True) + if dlg.exec_() == QDialog.Accepted: + name, language = dlg.getData() + self.addGroup(name, language) + + def __edit(self): + """ + Private slot to handle the Edit context menu action. + """ + itm = self.currentItem() + if isinstance(itm, TemplateEntry): + editGroup = False + else: + editGroup = True + dlg = TemplatePropertiesDialog(self, editGroup, itm) + if dlg.exec_() == QDialog.Accepted: + if editGroup: + name, language = dlg.getData() + self.changeGroup(itm.getName(), name, language) + else: + name, description, groupName, template = dlg.getData() + self.changeEntry(itm, name, groupName, description, template) + + def __remove(self): + """ + Private slot to handle the Remove context menu action. + """ + itm = self.currentItem() + res = QMessageBox.question(self, + self.trUtf8("Remove Template"), + self.trUtf8("""<p>Do you really want to remove <b>{0}</b>?</p>""")\ + .format(itm.getName()), + QMessageBox.StandardButtons(\ + QMessageBox.No | \ + QMessageBox.Yes), + QMessageBox.No) + if res != QMessageBox.Yes: + return + + if isinstance(itm, TemplateGroup): + self.removeGroup(itm) + else: + self.removeEntry(itm) + + def __save(self): + """ + Private slot to handle the Save context menu action. + """ + self.writeTemplates() + + def __import(self): + """ + Private slot to handle the Import context menu action. + """ + fn = QFileDialog.getOpenFileName(\ + self, + self.trUtf8("Import Templates"), + "", + self.trUtf8("Templates Files (*.e3c *.e4c);; All Files (*)")) + + if fn: + self.readTemplates(fn) + + def __export(self): + """ + Private slot to handle the Export context menu action. + """ + fn, selectedFilter = QFileDialog.getSaveFileNameAndFilter(\ + self, + self.trUtf8("Export Templates"), + "", + self.trUtf8("Templates Files (*.e4c);; All Files (*)"), + "", + QFileDialog.Options(QFileDialog.DontConfirmOverwrite)) + + if fn: + ext = QFileInfo(fn).suffix() + if not ext: + ex = selectedFilter.split("(*")[1].split(")")[0] + if ex: + fn += ex + self.writeTemplates(fn) + + def __showHelp(self): + """ + Private method to show some help. + """ + QMessageBox.information(self, + self.trUtf8("Template Help"), + self.trUtf8("""<p><b>Template groups</b> are a means of grouping individual""" + """ templates. Groups have an attribute that specifies,""" + """ which programming language they apply for.""" + """ In order to add template entries, at least one group""" + """ has to be defined.</p>""" + """<p><b>Template entries</b> are the actual templates.""" + """ They are grouped by the template groups. Help about""" + """ how to define them is available in the template edit""" + """ dialog. There is an example template available in the""" + """ Examples subdirectory of the eric4 distribution.</p>""")) + + def __getPredefinedVars(self): + """ + Private method to return predefined variables. + + @return dictionary of predefined variables and their values + """ + project = e4App().getObject("Project") + editor = self.viewmanager.activeWindow() + today = datetime.datetime.now().date() + sepchar = Preferences.getTemplates("SeparatorChar") + if sepchar == '%': + sepchar = '%%' + keyfmt = sepchar + "%s" + sepchar + varValues = {keyfmt % 'date': today.isoformat(), + keyfmt % 'year': str(today.year)} + + if project.name: + varValues[keyfmt % 'project_name'] = project.name + + path_name = editor.getFileName() + if path_name: + dir_name, file_name = os.path.split(path_name) + base_name, ext = os.path.splitext(file_name) + if ext: + ext = ext[1:] + varValues.update({ + keyfmt % 'path_name': path_name, + keyfmt % 'dir_name': dir_name, + keyfmt % 'file_name': file_name, + keyfmt % 'base_name': base_name, + keyfmt % 'ext': ext + }) + return varValues + + def applyTemplate(self, itm): + """ + Public method to apply the template. + + @param itm reference to the template item to apply (TemplateEntry) + """ + editor = self.viewmanager.activeWindow() + if editor is None: + return + + ok = False + vars = itm.getVariables() + varValues = self.__getPredefinedVars() + + # Remove predefined variables from list so user doesn't have to fill + # these values out in the dialog. + for v in varValues.keys(): + if v in vars: + vars.remove(v) + + if vars: + if Preferences.getTemplates("SingleDialog"): + dlg = TemplateMultipleVariablesDialog(vars, self) + if dlg.exec_() == QDialog.Accepted: + varValues.update(dlg.getVariables()) + ok = True + else: + for var in vars: + dlg = TemplateSingleVariableDialog(var, self) + if dlg.exec_() == QDialog.Accepted: + varValues[var] = dlg.getVariable() + else: + return + del dlg + ok = True + else: + ok = True + + if ok: + line = editor.text(editor.getCursorPosition()[0])\ + .replace(os.linesep, "") + indent = line.replace(line.lstrip(), "") + txt, lines, count = itm.getExpandedText(varValues, indent) + # It should be done in this way to allow undo + editor.beginUndoAction() + if editor.hasSelectedText(): + editor.removeSelectedText() + line, index = editor.getCursorPosition() + editor.insert(txt) + editor.setCursorPosition(line + lines - 1, + count and index + count or 0) + editor.endUndoAction() + editor.setFocus() + + def applyNamedTemplate(self, templateName): + """ + Public method to apply a template given a template name. + + @param templateName name of the template item to apply (string) + """ + for group in self.groups.values(): + template = group.getEntry(templateName) + if template is not None: + self.applyTemplate(template) + break + + def addEntry(self, groupName, name, description, template, quiet = False): + """ + Public method to add a template entry. + + @param groupName name of the group to add to (string) + @param name name of the entry to add (string) + @param description description of the entry to add (string) + @param template template text of the entry (string) + @param quiet flag indicating quiet operation (boolean) + """ + self.groups[groupName].addEntry(name, description, template, quiet = quiet) + self.__resort() + + def addGroup(self, name, language = "All"): + """ + Public method to add a group. + + @param name name of the group to be added (string) + @param language programming language for the group (string) + """ + if name not in self.groups: + self.groups[name] = TemplateGroup(self, name, language) + self.__resort() + + def changeGroup(self, oldname, newname, language = "All"): + """ + Public method to rename a group. + + @param oldname old name of the group (string) + @param newname new name of the group (string) + @param language programming language for the group (string) + """ + if oldname != newname: + if newname in self.groups: + QMessageBox.warning(self, + self.trUtf8("Edit Template Group"), + self.trUtf8("""<p>A template group with the name""" + """ <b>{0}</b> already exists.</p>""")\ + .format(newname)) + return + + self.groups[newname] = self.groups[oldname] + del self.groups[oldname] + self.groups[newname].setName(newname) + + self.groups[newname].setLanguage(language) + self.__resort() + + def getAllGroups(self): + """ + Public method to get all groups. + + @return list of all groups (list of TemplateGroup) + """ + return self.groups.values() + + def getGroupNames(self): + """ + Public method to get all group names. + + @return list of all group names (list of strings) + """ + groups = sorted(self.groups.keys()[:]) + return groups + + def removeGroup(self, itm): + """ + Public method to remove a group. + + @param itm template group to be removed (TemplateGroup) + """ + name = itm.getName() + itm.removeAllEntries() + index = self.indexOfTopLevelItem(itm) + self.takeTopLevelItem(index) + del self.groups[name] + + def removeEntry(self, itm): + """ + Public method to remove a template entry. + + @param itm template entry to be removed (TemplateEntry) + """ + groupName = itm.getGroupName() + self.groups[groupName].removeEntry(itm.getName()) + + def changeEntry(self, itm, name, groupName, description, template): + """ + Public method to change a template entry. + + @param itm template entry to be changed (TemplateEntry) + @param name new name for the entry (string) + @param groupName name of the group the entry should belong to + (string) + @param description description of the entry (string) + @param template template text of the entry (string) + """ + if itm.getGroupName() != groupName: + # move entry to another group + self.groups[itm.getGroupName()].removeEntry(itm.getName()) + self.groups[groupName].addEntry(name, description, template) + return + + if itm.getName() != name: + # entry was renamed + self.groups[groupName].removeEntry(itm.getName()) + self.groups[groupName].addEntry(name, description, template) + return + + tmpl = self.groups[groupName].getEntry(name) + tmpl.setDescription(description) + tmpl.setTemplateText(template) + self.__resort() + + def writeTemplates(self, filename = None): + """ + Public method to write the templates data to an XML file (.e4c). + + @param filename name of a templates file to read (string) + """ + try: + if filename is None: + filename = os.path.join(Utilities.getConfigDir(), "eric4templates.e4c") + f = open(filename, "wb") + + TemplatesWriter(f, self).writeXML() + + f.close() + except IOError: + QMessageBox.critical(None, + self.trUtf8("Save templates"), + self.trUtf8("<p>The templates file <b>{0}</b> could not be written.</p>") + .format(filename)) + + def readTemplates(self, filename = None): + """ + Public method to read in the templates file (.e4c) + + @param filename name of a templates file to read (string) + """ + try: + if filename is None: + filename = os.path.join(Utilities.getConfigDir(), "eric4templates.e4c") + if not os.path.exists(filename): + return + f = open(filename, "rb") + line = f.readline() + dtdLine = f.readline() + f.close() + except IOError: + QMessageBox.critical(None, + self.trUtf8("Read templates"), + self.trUtf8("<p>The templates file <b>{0}</b> could not be read.</p>") + .format(filename)) + return + + # now read the file + if line.startswith('<?xml'): + parser = make_parser(dtdLine.startswith("<!DOCTYPE")) + handler = TemplatesHandler(templateViewer = self) + er = XMLEntityResolver() + eh = XMLErrorHandler() + + parser.setContentHandler(handler) + parser.setEntityResolver(er) + parser.setErrorHandler(eh) + + try: + f = open(filename, "rb") + try: + try: + parser.parse(f) + except UnicodeEncodeError: + f.seek(0) + buf = cStringIO.StringIO(f.read()) + parser.parse(buf) + finally: + f.close() + except IOError: + QMessageBox.critical(None, + self.trUtf8("Read templates"), + self.trUtf8("<p>The templates file <b>{0}</b> could not be read.</p>") + .format(filename)) + return + except XMLFatalParseError: + pass + + eh.showParseMessages() + else: + QMessageBox.critical(None, + self.trUtf8("Read templates"), + self.trUtf8("<p>The templates file <b>{0}</b> has an" + " unsupported format.</p>") + .format(filename)) + + def __configure(self): + """ + Private method to open the configuration dialog. + """ + e4App().getObject("UserInterface").showPreferences("templatesPage") + + def hasTemplate(self, entryName): + """ + Public method to check, if an entry of the given name exists. + + @param entryName name of the entry to check for (string) + @return flag indicating the existence (boolean) + """ + for group in self.groups.values(): + if group.hasEntry(entryName): + return True + + return False + + def getTemplateNames(self, start): + """ + Public method to get the names of templates starting with the given string. + + @param start start string of the name (string) + @return sorted list of matching template names (list of strings) + """ + names = [] + for group in self.groups.values(): + names.extend(group.getEntryNames(start)) + return sorted(names)