2012-12-21 08:51:59 +01:00
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
# Copyright: Damien Elmes <anki@ichi2.net>
|
|
|
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
|
|
|
|
|
|
from aqt.qt import *
|
|
|
|
import re
|
|
|
|
from anki.consts import *
|
|
|
|
import aqt
|
|
|
|
from anki.sound import playFromText, clearAudioQueue
|
2012-12-22 01:17:10 +01:00
|
|
|
from aqt.utils import saveGeom, restoreGeom, getBase, mungeQA,\
|
|
|
|
showInfo, askUser, getOnlyText, \
|
2012-12-21 08:51:59 +01:00
|
|
|
showWarning, openHelp, openLink
|
|
|
|
from anki.utils import isMac, isWin, joinFields
|
|
|
|
from aqt.webview import AnkiWebView
|
|
|
|
import anki.js
|
|
|
|
|
|
|
|
class CardLayout(QDialog):
|
|
|
|
|
|
|
|
def __init__(self, mw, note, ord=0, parent=None, addMode=False):
|
|
|
|
QDialog.__init__(self, parent or mw, Qt.Window)
|
|
|
|
self.mw = aqt.mw
|
|
|
|
self.parent = parent or mw
|
|
|
|
self.note = note
|
|
|
|
self.ord = ord
|
|
|
|
self.col = self.mw.col
|
|
|
|
self.mm = self.mw.col.models
|
|
|
|
self.model = note.model()
|
|
|
|
self.mw.checkpoint(_("Card Types"))
|
|
|
|
self.addMode = addMode
|
|
|
|
if addMode:
|
|
|
|
# save it to DB temporarily
|
|
|
|
self.emptyFields = []
|
|
|
|
for name, val in note.items():
|
|
|
|
if val.strip():
|
|
|
|
continue
|
|
|
|
self.emptyFields.append(name)
|
|
|
|
note[name] = "(%s)" % name
|
|
|
|
note.flush()
|
|
|
|
self.setupTabs()
|
|
|
|
self.setupButtons()
|
|
|
|
self.setWindowTitle(_("Card Types for %s") % self.model['name'])
|
|
|
|
v1 = QVBoxLayout()
|
|
|
|
v1.addWidget(self.tabs)
|
|
|
|
v1.addLayout(self.buttons)
|
|
|
|
self.setLayout(v1)
|
|
|
|
self.redraw()
|
|
|
|
restoreGeom(self, "CardLayout")
|
|
|
|
self.exec_()
|
|
|
|
|
|
|
|
def redraw(self):
|
|
|
|
self.cards = self.col.previewCards(self.note, 2)
|
|
|
|
self.redrawing = True
|
|
|
|
self.updateTabs()
|
|
|
|
self.redrawing = False
|
2013-04-11 08:45:20 +02:00
|
|
|
idx = self.ord
|
|
|
|
if idx >= len(self.cards):
|
|
|
|
idx = len(self.cards) - 1
|
|
|
|
self.selectCard(idx)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
def setupTabs(self):
|
|
|
|
c = self.connect
|
|
|
|
cloze = self.model['type'] == MODEL_CLOZE
|
|
|
|
self.tabs = QTabWidget()
|
2013-04-11 07:02:01 +02:00
|
|
|
self.tabs.setTabsClosable(not cloze)
|
2012-12-21 08:51:59 +01:00
|
|
|
self.tabs.setUsesScrollButtons(True)
|
|
|
|
if not cloze:
|
|
|
|
add = QPushButton("+")
|
|
|
|
add.setFixedWidth(30)
|
|
|
|
add.setToolTip(_("Add new card"))
|
|
|
|
c(add, SIGNAL("clicked()"), self.onAddCard)
|
|
|
|
self.tabs.setCornerWidget(add)
|
2013-04-11 08:07:50 +02:00
|
|
|
c(self.tabs, SIGNAL("currentChanged(int)"), self.onCardSelected)
|
2013-04-11 07:02:01 +02:00
|
|
|
c(self.tabs, SIGNAL("tabCloseRequested(int)"), self.onRemoveTab)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
def updateTabs(self):
|
|
|
|
self.forms = []
|
|
|
|
self.tabs.clear()
|
|
|
|
for t in self.model['tmpls']:
|
|
|
|
self.addTab(t)
|
|
|
|
|
|
|
|
def addTab(self, t):
|
|
|
|
c = self.connect
|
|
|
|
w = QWidget()
|
|
|
|
l = QHBoxLayout()
|
|
|
|
l.setMargin(0)
|
|
|
|
l.setSpacing(3)
|
|
|
|
left = QWidget()
|
|
|
|
# template area
|
|
|
|
tform = aqt.forms.template.Ui_Form()
|
|
|
|
tform.setupUi(left)
|
|
|
|
tform.label1.setText(u" →")
|
|
|
|
tform.label2.setText(u" →")
|
|
|
|
tform.labelc1.setText(u" ↗")
|
|
|
|
tform.labelc2.setText(u" ↘")
|
|
|
|
if self.style().objectName() == "gtk+":
|
|
|
|
# gtk+ requires margins in inner layout
|
|
|
|
tform.tlayout1.setContentsMargins(0, 11, 0, 0)
|
|
|
|
tform.tlayout2.setContentsMargins(0, 11, 0, 0)
|
|
|
|
tform.tlayout3.setContentsMargins(0, 11, 0, 0)
|
|
|
|
if len(self.cards) > 1:
|
|
|
|
tform.groupBox_3.setTitle(_(
|
|
|
|
"Styling (shared between cards)"))
|
|
|
|
c(tform.front, SIGNAL("textChanged()"), self.saveCard)
|
|
|
|
c(tform.css, SIGNAL("textChanged()"), self.saveCard)
|
|
|
|
c(tform.back, SIGNAL("textChanged()"), self.saveCard)
|
|
|
|
l.addWidget(left, 5)
|
|
|
|
# preview area
|
|
|
|
right = QWidget()
|
|
|
|
pform = aqt.forms.preview.Ui_Form()
|
|
|
|
pform.setupUi(right)
|
|
|
|
if self.style().objectName() == "gtk+":
|
|
|
|
# gtk+ requires margins in inner layout
|
|
|
|
pform.frontPrevBox.setContentsMargins(0, 11, 0, 0)
|
|
|
|
pform.backPrevBox.setContentsMargins(0, 11, 0, 0)
|
|
|
|
# for cloze notes, show that it's one of n cards
|
|
|
|
if self.model['type'] == MODEL_CLOZE:
|
|
|
|
cnt = len(self.mm.availOrds(
|
|
|
|
self.model, joinFields(self.note.fields)))
|
|
|
|
for g in pform.groupBox, pform.groupBox_2:
|
|
|
|
g.setTitle(g.title() + _(" (1 of %d)") % max(cnt, 1))
|
|
|
|
pform.frontWeb = AnkiWebView()
|
|
|
|
pform.frontPrevBox.addWidget(pform.frontWeb)
|
|
|
|
pform.backWeb = AnkiWebView()
|
|
|
|
pform.backPrevBox.addWidget(pform.backWeb)
|
|
|
|
for wig in pform.frontWeb, pform.backWeb:
|
|
|
|
wig.page().setLinkDelegationPolicy(
|
|
|
|
QWebPage.DelegateExternalLinks)
|
|
|
|
l.addWidget(right, 5)
|
|
|
|
w.setLayout(l)
|
|
|
|
self.forms.append({'tform': tform, 'pform': pform})
|
|
|
|
self.tabs.addTab(w, t['name'])
|
|
|
|
|
|
|
|
def onRemoveTab(self, idx):
|
|
|
|
if len(self.model['tmpls']) < 2:
|
2013-04-11 07:02:01 +02:00
|
|
|
return showInfo(_("At least one card type is required."))
|
|
|
|
cards = self.mm.tmplUseCount(self.model, idx)
|
|
|
|
cards = ngettext("%d card", "%d cards", cards) % cards
|
2013-05-13 06:58:41 +02:00
|
|
|
msg = _("Delete the '%(a)s' card type, and its %(b)s?" %
|
|
|
|
dict(a=self.model['tmpls'][idx]['name'], b=cards))
|
2013-04-11 07:02:01 +02:00
|
|
|
if not askUser(msg):
|
2012-12-21 08:51:59 +01:00
|
|
|
return
|
|
|
|
if not self.mm.remTemplate(self.model, self.cards[idx].template()):
|
|
|
|
return showWarning(_("""\
|
|
|
|
Removing this card type would cause one or more notes to be deleted. \
|
|
|
|
Please create a new card type first."""))
|
|
|
|
self.redraw()
|
|
|
|
|
|
|
|
# Buttons
|
|
|
|
##########################################################################
|
|
|
|
|
|
|
|
def setupButtons(self):
|
|
|
|
c = self.connect
|
|
|
|
l = self.buttons = QHBoxLayout()
|
|
|
|
help = QPushButton(_("Help"))
|
|
|
|
help.setAutoDefault(False)
|
|
|
|
l.addWidget(help)
|
|
|
|
c(help, SIGNAL("clicked()"), self.onHelp)
|
|
|
|
l.addStretch()
|
|
|
|
addField = QPushButton(_("Add Field"))
|
|
|
|
addField.setAutoDefault(False)
|
|
|
|
l.addWidget(addField)
|
|
|
|
c(addField, SIGNAL("clicked()"), self.onAddField)
|
|
|
|
if self.model['type'] != MODEL_CLOZE:
|
|
|
|
flip = QPushButton(_("Flip"))
|
|
|
|
flip.setAutoDefault(False)
|
|
|
|
l.addWidget(flip)
|
|
|
|
c(flip, SIGNAL("clicked()"), self.onFlip)
|
|
|
|
more = QPushButton(_("More") + u" ▾")
|
|
|
|
more.setAutoDefault(False)
|
|
|
|
l.addWidget(more)
|
|
|
|
c(more, SIGNAL("clicked()"), lambda: self.onMore(more))
|
|
|
|
l.addStretch()
|
|
|
|
close = QPushButton(_("Close"))
|
|
|
|
close.setAutoDefault(False)
|
|
|
|
l.addWidget(close)
|
|
|
|
c(close, SIGNAL("clicked()"), self.accept)
|
|
|
|
|
|
|
|
# Cards
|
|
|
|
##########################################################################
|
|
|
|
|
|
|
|
def selectCard(self, idx):
|
2013-04-11 08:45:20 +02:00
|
|
|
if self.tabs.currentIndex() == idx:
|
|
|
|
# trigger a re-read
|
|
|
|
self.onCardSelected(idx)
|
|
|
|
else:
|
|
|
|
self.tabs.setCurrentIndex(idx)
|
2013-04-11 08:07:50 +02:00
|
|
|
|
|
|
|
def onCardSelected(self, idx):
|
2012-12-21 08:51:59 +01:00
|
|
|
if self.redrawing:
|
|
|
|
return
|
|
|
|
self.card = self.cards[idx]
|
|
|
|
self.tab = self.forms[idx]
|
|
|
|
self.tabs.setCurrentIndex(idx)
|
|
|
|
self.playedAudio = {}
|
|
|
|
self.readCard()
|
|
|
|
self.renderPreview()
|
|
|
|
|
|
|
|
def readCard(self):
|
|
|
|
t = self.card.template()
|
|
|
|
self.redrawing = True
|
|
|
|
self.tab['tform'].front.setPlainText(t['qfmt'])
|
|
|
|
self.tab['tform'].css.setPlainText(self.model['css'])
|
|
|
|
self.tab['tform'].back.setPlainText(t['afmt'])
|
|
|
|
self.redrawing = False
|
|
|
|
|
|
|
|
def saveCard(self):
|
|
|
|
if self.redrawing:
|
|
|
|
return
|
|
|
|
text = self.tab['tform'].front.toPlainText()
|
|
|
|
self.card.template()['qfmt'] = text
|
|
|
|
text = self.tab['tform'].css.toPlainText()
|
|
|
|
self.card.model()['css'] = text
|
|
|
|
text = self.tab['tform'].back.toPlainText()
|
|
|
|
self.card.template()['afmt'] = text
|
|
|
|
self.renderPreview()
|
|
|
|
|
|
|
|
# Preview
|
|
|
|
##########################################################################
|
|
|
|
|
|
|
|
def renderPreview(self):
|
|
|
|
c = self.card
|
|
|
|
ti = self.maybeTextInput
|
|
|
|
base = getBase(self.mw.col)
|
|
|
|
self.tab['pform'].frontWeb.stdHtml(
|
2013-06-10 08:28:34 +02:00
|
|
|
ti(mungeQA(self.mw.col, c.q(reload=True))), self.mw.reviewer._styles(),
|
2012-12-21 08:51:59 +01:00
|
|
|
bodyClass="card card%d" % (c.ord+1), head=base,
|
|
|
|
js=anki.js.browserSel)
|
|
|
|
self.tab['pform'].backWeb.stdHtml(
|
2013-06-10 08:28:34 +02:00
|
|
|
ti(mungeQA(self.mw.col, c.a()), type='a'), self.mw.reviewer._styles(),
|
2012-12-21 08:51:59 +01:00
|
|
|
bodyClass="card card%d" % (c.ord+1), head=base,
|
|
|
|
js=anki.js.browserSel)
|
|
|
|
clearAudioQueue()
|
|
|
|
if c.id not in self.playedAudio:
|
|
|
|
playFromText(c.q())
|
|
|
|
playFromText(c.a())
|
|
|
|
self.playedAudio[c.id] = True
|
|
|
|
|
|
|
|
def maybeTextInput(self, txt, type='q'):
|
2013-05-23 04:11:25 +02:00
|
|
|
if "[[type:" not in txt:
|
|
|
|
return txt
|
2013-05-20 11:21:14 +02:00
|
|
|
origLen = len(txt)
|
|
|
|
txt = txt.replace("<hr id=answer>", "")
|
|
|
|
hadHR = origLen != len(txt)
|
|
|
|
def answerRepl(match):
|
2013-06-07 02:51:26 +02:00
|
|
|
res = self.mw.reviewer.correct(u"exomple", u"an example")
|
2013-05-20 11:21:14 +02:00
|
|
|
if hadHR:
|
|
|
|
res = "<hr id=answer>" + res
|
|
|
|
return res
|
2012-12-21 08:51:59 +01:00
|
|
|
if type == 'q':
|
2013-05-20 11:21:14 +02:00
|
|
|
repl = "<input id='typeans' type=text value='exomple'>"
|
|
|
|
repl = "<center>%s</center>" % repl
|
2012-12-21 08:51:59 +01:00
|
|
|
else:
|
2013-05-20 11:21:14 +02:00
|
|
|
repl = answerRepl
|
2012-12-21 08:51:59 +01:00
|
|
|
return re.sub("\[\[type:.+?\]\]", repl, txt)
|
|
|
|
|
|
|
|
# Card operations
|
|
|
|
######################################################################
|
|
|
|
|
|
|
|
def onRename(self):
|
|
|
|
name = getOnlyText(_("New name:"),
|
|
|
|
default=self.card.template()['name'])
|
|
|
|
if not name:
|
|
|
|
return
|
|
|
|
if name in [c.template()['name'] for c in self.cards
|
|
|
|
if c.template()['ord'] != self.ord]:
|
|
|
|
return showWarning(_("That name is already used."))
|
|
|
|
self.card.template()['name'] = name
|
|
|
|
self.tabs.setTabText(self.tabs.currentIndex(), name)
|
|
|
|
|
|
|
|
def onReorder(self):
|
|
|
|
n = len(self.cards)
|
|
|
|
cur = self.card.template()['ord']+1
|
|
|
|
pos = getOnlyText(
|
|
|
|
_("Enter new card position (1...%s):") % n,
|
|
|
|
default=str(cur))
|
|
|
|
if not pos:
|
|
|
|
return
|
|
|
|
try:
|
|
|
|
pos = int(pos)
|
|
|
|
except ValueError:
|
|
|
|
return
|
|
|
|
if pos < 1 or pos > n:
|
|
|
|
return
|
|
|
|
if pos == cur:
|
|
|
|
return
|
|
|
|
pos -= 1
|
|
|
|
self.mm.moveTemplate(self.model, self.card.template(), pos)
|
|
|
|
self.ord = pos
|
|
|
|
self.redraw()
|
|
|
|
|
|
|
|
def _newCardName(self):
|
|
|
|
n = len(self.cards) + 1
|
|
|
|
while 1:
|
|
|
|
name = _("Card %d") % n
|
|
|
|
if name not in [c.template()['name'] for c in self.cards]:
|
|
|
|
break
|
|
|
|
n += 1
|
|
|
|
return name
|
|
|
|
|
|
|
|
def onAddCard(self):
|
|
|
|
name = self._newCardName()
|
|
|
|
t = self.mm.newTemplate(name)
|
|
|
|
old = self.card.template()
|
|
|
|
t['qfmt'] = "%s<br>\n%s" % (_("Edit to customize"), old['qfmt'])
|
|
|
|
t['afmt'] = old['afmt']
|
|
|
|
self.mm.addTemplate(self.model, t)
|
2013-04-11 08:45:20 +02:00
|
|
|
self.ord = len(self.cards)
|
2012-12-21 08:51:59 +01:00
|
|
|
self.redraw()
|
|
|
|
|
|
|
|
def onFlip(self):
|
|
|
|
old = self.card.template()
|
|
|
|
self._flipQA(old, old)
|
|
|
|
self.redraw()
|
|
|
|
|
|
|
|
def _flipQA(self, src, dst):
|
|
|
|
m = re.match("(?s)(.+)<hr id=answer>(.+)", src['afmt'])
|
|
|
|
if not m:
|
|
|
|
showInfo(_("""\
|
|
|
|
Anki couldn't find the line between the question and answer. Please \
|
|
|
|
adjust the template manually to switch the question and answer."""))
|
|
|
|
return
|
|
|
|
dst['afmt'] = "{{FrontSide}}\n\n<hr id=answer>\n\n%s" % src['qfmt']
|
|
|
|
dst['qfmt'] = m.group(2).strip()
|
|
|
|
return True
|
|
|
|
|
|
|
|
def onMore(self, button):
|
|
|
|
m = QMenu(self)
|
|
|
|
a = m.addAction(_("Rename"))
|
|
|
|
a.connect(a, SIGNAL("triggered()"),
|
|
|
|
self.onRename)
|
|
|
|
if self.model['type'] != MODEL_CLOZE:
|
|
|
|
a = m.addAction(_("Reposition"))
|
|
|
|
a.connect(a, SIGNAL("triggered()"),
|
|
|
|
self.onReorder)
|
|
|
|
t = self.card.template()
|
|
|
|
if t['did']:
|
|
|
|
s = _(" (on)")
|
|
|
|
else:
|
|
|
|
s = _(" (off)")
|
|
|
|
a = m.addAction(_("Deck Override") + s)
|
|
|
|
a.connect(a, SIGNAL("triggered()"),
|
|
|
|
self.onTargetDeck)
|
2012-12-22 02:10:52 +01:00
|
|
|
a = m.addAction(_("Browser Appearance"))
|
2012-12-21 08:51:59 +01:00
|
|
|
a.connect(a, SIGNAL("triggered()"),
|
|
|
|
self.onBrowserDisplay)
|
|
|
|
m.exec_(button.mapToGlobal(QPoint(0,0)))
|
|
|
|
|
|
|
|
def onBrowserDisplay(self):
|
|
|
|
d = QDialog()
|
|
|
|
f = aqt.forms.browserdisp.Ui_Dialog()
|
|
|
|
f.setupUi(d)
|
|
|
|
t = self.card.template()
|
|
|
|
f.qfmt.setText(t.get('bqfmt', ""))
|
|
|
|
f.afmt.setText(t.get('bafmt', ""))
|
2012-12-22 02:10:52 +01:00
|
|
|
f.font.setCurrentFont(QFont(t.get('bfont', "Arial")))
|
|
|
|
f.fontSize.setValue(t.get('bsize', 12))
|
2012-12-21 08:51:59 +01:00
|
|
|
d.connect(f.buttonBox, SIGNAL("accepted()"),
|
|
|
|
lambda: self.onBrowserDisplayOk(f))
|
|
|
|
d.exec_()
|
|
|
|
|
|
|
|
def onBrowserDisplayOk(self, f):
|
|
|
|
t = self.card.template()
|
|
|
|
t['bqfmt'] = f.qfmt.text().strip()
|
|
|
|
t['bafmt'] = f.afmt.text().strip()
|
2012-12-22 02:10:52 +01:00
|
|
|
t['bfont'] = f.font.currentFont().family()
|
|
|
|
t['bsize'] = f.fontSize.value()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
def onTargetDeck(self):
|
|
|
|
from aqt.tagedit import TagEdit
|
|
|
|
t = self.card.template()
|
|
|
|
d = QDialog(self)
|
|
|
|
d.setWindowTitle("Anki")
|
|
|
|
d.setMinimumWidth(400)
|
|
|
|
l = QVBoxLayout()
|
|
|
|
lab = QLabel(_("""\
|
|
|
|
Enter deck to place new %s cards in, or leave blank:""") %
|
|
|
|
self.card.template()['name'])
|
|
|
|
lab.setWordWrap(True)
|
|
|
|
l.addWidget(lab)
|
|
|
|
te = TagEdit(d, type=1)
|
|
|
|
te.setCol(self.col)
|
|
|
|
l.addWidget(te)
|
|
|
|
if t['did']:
|
|
|
|
te.setText(self.col.decks.get(t['did'])['name'])
|
|
|
|
te.selectAll()
|
|
|
|
bb = QDialogButtonBox(QDialogButtonBox.Close)
|
|
|
|
self.connect(bb, SIGNAL("rejected()"), d, SLOT("close()"))
|
|
|
|
l.addWidget(bb)
|
|
|
|
d.setLayout(l)
|
|
|
|
d.exec_()
|
|
|
|
if not te.text().strip():
|
|
|
|
t['did'] = None
|
|
|
|
else:
|
|
|
|
t['did'] = self.col.decks.id(te.text())
|
|
|
|
|
|
|
|
def onAddField(self):
|
|
|
|
diag = QDialog(self)
|
|
|
|
form = aqt.forms.addfield.Ui_Dialog()
|
|
|
|
form.setupUi(diag)
|
|
|
|
fields = [f['name'] for f in self.model['flds']]
|
|
|
|
form.fields.addItems(fields)
|
|
|
|
form.font.setCurrentFont(QFont("Arial"))
|
|
|
|
form.size.setValue(20)
|
|
|
|
diag.show()
|
|
|
|
# Work around a Qt bug,
|
|
|
|
# https://bugreports.qt-project.org/browse/QTBUG-1894
|
|
|
|
if isMac or isWin:
|
|
|
|
# No problems on Macs or Windows.
|
|
|
|
form.fields.showPopup()
|
|
|
|
else:
|
|
|
|
# Delay showing the pop-up.
|
|
|
|
self.mw.progress.timer(200, form.fields.showPopup, False)
|
|
|
|
if not diag.exec_():
|
|
|
|
return
|
|
|
|
if form.radioQ.isChecked():
|
|
|
|
obj = self.tab['tform'].front
|
|
|
|
else:
|
|
|
|
obj = self.tab['tform'].back
|
|
|
|
self._addField(obj,
|
|
|
|
fields[form.fields.currentIndex()],
|
|
|
|
form.font.currentFont().family(),
|
|
|
|
form.size.value())
|
|
|
|
|
|
|
|
def _addField(self, widg, field, font, size):
|
|
|
|
t = widg.toPlainText()
|
|
|
|
t +="\n<div style='font-family: %s; font-size: %spx;'>{{%s}}</div>\n" % (
|
|
|
|
font, size, field)
|
|
|
|
widg.setPlainText(t)
|
|
|
|
self.saveCard()
|
|
|
|
|
|
|
|
# Closing & Help
|
|
|
|
######################################################################
|
|
|
|
|
|
|
|
def accept(self):
|
|
|
|
self.reject()
|
|
|
|
|
|
|
|
def reject(self):
|
|
|
|
clearAudioQueue()
|
|
|
|
if self.addMode:
|
|
|
|
# remove the filler fields we added
|
|
|
|
for name in self.emptyFields:
|
|
|
|
self.note[name] = ""
|
|
|
|
self.mw.col.db.execute("delete from notes where id = ?",
|
|
|
|
self.note.id)
|
|
|
|
self.mm.save(self.model, templates=True)
|
|
|
|
self.mw.reset()
|
|
|
|
saveGeom(self, "CardLayout")
|
|
|
|
return QDialog.reject(self)
|
|
|
|
|
|
|
|
def onHelp(self):
|
|
|
|
openHelp("templates")
|