import src from 2.4.3

master
Can202 2021-09-01 22:02:37 -04:00
parent 15dac15b57
commit 3b8756a869
43 changed files with 19171 additions and 0 deletions

0
src/__init__.py Normal file
View File

106
src/autocompletion.py Normal file
View File

@ -0,0 +1,106 @@
import config
import mypickle
import screenplay
import util
# manages auto completion information for a single script.
class AutoCompletion:
def __init__(self):
# type configs, key = line type, value = Type
self.types = {}
# element types
t = Type(screenplay.SCENE)
self.types[t.ti.lt] = t
t = Type(screenplay.CHARACTER)
self.types[t.ti.lt] = t
t = Type(screenplay.TRANSITION)
t.items = [
"BACK TO:",
"CROSSFADE:",
"CUT TO:",
"DISSOLVE TO:",
"FADE IN:",
"FADE OUT",
"FADE TO BLACK",
"FLASHBACK TO:",
"JUMP CUT TO:",
"MATCH CUT TO:",
"SLOW FADE TO BLACK",
"SMASH CUT TO:",
"TIME CUT:"
]
self.types[t.ti.lt] = t
t = Type(screenplay.SHOT)
self.types[t.ti.lt] = t
self.refresh()
# load config from string 's'. does not throw any exceptions, silently
# ignores any errors, and always leaves config in an ok state.
def load(self, s):
vals = mypickle.Vars.makeVals(s)
for t in self.types.values():
t.load(vals, "AutoCompletion/")
self.refresh()
# save config into a string and return that.
def save(self):
s = ""
for t in self.types.values():
s += t.save("AutoCompletion/")
return s
# fix up invalid values and uppercase everything.
def refresh(self):
for t in self.types.values():
tmp = []
for v in t.items:
v = util.upper(util.toInputStr(v)).strip()
if len(v) > 0:
tmp.append(v)
t.items = tmp
# get type's Type, or None if it doesn't exist.
def getType(self, lt):
return self.types.get(lt)
# auto completion info for one element type
class Type:
cvars = None
def __init__(self, lt):
# pointer to TypeInfo
self.ti = config.lt2ti(lt)
if not self.__class__.cvars:
v = self.__class__.cvars = mypickle.Vars()
v.addBool("enabled", True, "Enabled")
v.addList("items", [], "Items",
mypickle.StrLatin1Var("", "", ""))
v.makeDicts()
self.__class__.cvars.setDefaults(self)
def save(self, prefix):
prefix += "%s/" % self.ti.name
return self.cvars.save(prefix, self)
def load(self, vals, prefix):
prefix += "%s/" % self.ti.name
self.cvars.load(vals, prefix, self)

98
src/autocompletiondlg.py Normal file
View File

@ -0,0 +1,98 @@
import gutil
import misc
import util
import wx
class AutoCompletionDlg(wx.Dialog):
def __init__(self, parent, autoCompletion):
wx.Dialog.__init__(self, parent, -1, "Auto-completion",
style = wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
self.autoCompletion = autoCompletion
vsizer = wx.BoxSizer(wx.VERTICAL)
hsizer = wx.BoxSizer(wx.HORIZONTAL)
hsizer.Add(wx.StaticText(self, -1, "Element:"), 0,
wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 10)
self.elementsCombo = wx.ComboBox(self, -1, style = wx.CB_READONLY)
for t in autoCompletion.types.values():
self.elementsCombo.Append(t.ti.name, t.ti.lt)
self.Bind(wx.EVT_COMBOBOX, self.OnElementCombo, id=self.elementsCombo.GetId())
hsizer.Add(self.elementsCombo, 0)
vsizer.Add(hsizer, 0, wx.EXPAND)
vsizer.Add(wx.StaticLine(self, -1), 0, wx.EXPAND | wx.TOP | wx.BOTTOM, 10)
self.enabledCb = wx.CheckBox(self, -1, "Auto-completion enabled")
self.Bind(wx.EVT_CHECKBOX, self.OnMisc, id=self.enabledCb.GetId())
vsizer.Add(self.enabledCb, 0, wx.BOTTOM, 10)
vsizer.Add(wx.StaticText(self, -1, "Default items:"))
self.itemsEntry = wx.TextCtrl(self, -1, style = wx.TE_MULTILINE |
wx.TE_DONTWRAP, size = (400, 200))
self.Bind(wx.EVT_TEXT, self.OnMisc, id=self.itemsEntry.GetId())
vsizer.Add(self.itemsEntry, 1, wx.EXPAND)
hsizer = wx.BoxSizer(wx.HORIZONTAL)
hsizer.Add((1, 1), 1)
cancelBtn = gutil.createStockButton(self, "Cancel")
hsizer.Add(cancelBtn, 0, wx.LEFT, 10)
okBtn = gutil.createStockButton(self, "OK")
hsizer.Add(okBtn, 0, wx.LEFT, 10)
vsizer.Add(hsizer, 0, wx.EXPAND | wx.TOP, 10)
util.finishWindow(self, vsizer)
self.elementsCombo.SetSelection(0)
self.OnElementCombo()
self.Bind(wx.EVT_BUTTON, self.OnCancel, id=cancelBtn.GetId())
self.Bind(wx.EVT_BUTTON, self.OnOK, id=okBtn.GetId())
def OnOK(self, event):
self.autoCompletion.refresh()
self.EndModal(wx.ID_OK)
def OnCancel(self, event):
self.EndModal(wx.ID_CANCEL)
def OnElementCombo(self, event = None):
self.lt = self.elementsCombo.GetClientData(self.elementsCombo.
GetSelection())
t = self.autoCompletion.getType(self.lt)
self.enabledCb.SetValue(t.enabled)
self.itemsEntry.Enable(t.enabled)
self.itemsEntry.SetValue("\n".join(t.items))
def OnMisc(self, event = None):
t = self.autoCompletion.getType(self.lt)
t.enabled = bool(self.enabledCb.IsChecked())
self.itemsEntry.Enable(t.enabled)
# this is cut&pasted from autocompletion.AutoCompletion.refresh,
# but I don't want to call that since it does all types, this does
# just the changed one.
tmp = []
for v in misc.fromGUI(self.itemsEntry.GetValue()).split("\n"):
v = util.toInputStr(v).strip()
if len(v) > 0:
tmp.append(v)
t.items = tmp

1545
src/cfgdlg.py Normal file

File diff suppressed because it is too large Load Diff

148
src/characterreport.py Normal file
View File

@ -0,0 +1,148 @@
import misc
import pdf
import pml
import screenplay
import util
from functools import reduce
import functools
class CharacterReport:
def __init__(self, sp):
self.sp = sp
ls = sp.lines
# key = character name, value = CharInfo
chars = {}
name = None
scene = "(NO SCENE NAME)"
# how many lines processed for current speech
curSpeechLines = 0
for i in range(len(ls)):
line = ls[i]
if (line.lt == screenplay.SCENE) and\
(line.lb == screenplay.LB_LAST):
scene = util.upper(line.text)
elif (line.lt == screenplay.CHARACTER) and\
(line.lb == screenplay.LB_LAST):
name = util.upper(line.text)
curSpeechLines = 0
elif line.lt in (screenplay.DIALOGUE, screenplay.PAREN) and name:
ci = chars.get(name)
if not ci:
ci = CharInfo(name, sp)
chars[name] = ci
if scene:
ci.scenes[scene] = ci.scenes.get(scene, 0) + 1
if curSpeechLines == 0:
ci.speechCnt += 1
curSpeechLines += 1
# PAREN lines don't count as spoken words
if line.lt == screenplay.DIALOGUE:
ci.lineCnt += 1
words = util.splitToWords(line.text)
ci.wordCnt += len(words)
ci.wordCharCnt += reduce(lambda x, y: x + len(y), words,
0)
ci.pages.addPage(sp.line2page(i))
else:
name = None
curSpeechLines = 0
# list of CharInfo objects
self.cinfo = []
for v in list(chars.values()):
self.cinfo.append(v)
self.cinfo = sorted(self.cinfo, key=functools.cmp_to_key(cmpLines))
self.totalSpeechCnt = self.sum("speechCnt")
self.totalLineCnt = self.sum("lineCnt")
self.totalWordCnt = self.sum("wordCnt")
self.totalWordCharCnt = self.sum("wordCharCnt")
# information types and what to include
self.INF_BASIC, self.INF_PAGES, self.INF_LOCATIONS = list(range(3))
self.inf = []
for s in ["Basic information", "Page list", "Location list"]:
self.inf.append(misc.CheckBoxItem(s))
# calculate total sum of self.cinfo.{name} and return it.
def sum(self, name):
return reduce(lambda tot, ci: tot + getattr(ci, name), self.cinfo, 0)
def generate(self):
tf = pml.TextFormatter(self.sp.cfg.paperWidth,
self.sp.cfg.paperHeight, 20.0, 12)
for ci in self.cinfo:
if not ci.include:
continue
tf.addText(ci.name, fs = 14,
style = pml.BOLD | pml.UNDERLINED)
if self.inf[self.INF_BASIC].selected:
tf.addText("Speeches: %d, Lines: %d (%.2f%%),"
" per speech: %.2f" % (ci.speechCnt, ci.lineCnt,
util.pctf(ci.lineCnt, self.totalLineCnt),
util.safeDiv(ci.lineCnt, ci.speechCnt)))
tf.addText("Words: %d, per speech: %.2f,"
" characters per: %.2f" % (ci.wordCnt,
util.safeDiv(ci.wordCnt, ci.speechCnt),
util.safeDiv(ci.wordCharCnt, ci.wordCnt)))
if self.inf[self.INF_PAGES].selected:
tf.addWrappedText("Pages: %d, list: %s" % (len(ci.pages),
ci.pages), " ")
if self.inf[self.INF_LOCATIONS].selected:
tf.addSpace(2.5)
for it in util.sortDict(ci.scenes):
tf.addText("%3d %s" % (it[1], it[0]),
x = tf.margin * 2.0, fs = 10)
tf.addSpace(5.0)
return pdf.generate(tf.doc)
# information about one character
class CharInfo:
def __init__(self, name, sp):
self.name = name
self.speechCnt = 0
self.lineCnt = 0
self.wordCnt = 0
self.wordCharCnt = 0
self.scenes = {}
self.include = True
self.pages = screenplay.PageList(sp.getPageNumbers())
def cmpfunc(a, b):
return (a > b) - (a < b)
def cmpLines(c1, c2):
ret = cmpfunc(c2.lineCnt, c1.lineCnt)
if ret != 0:
return ret
else:
return cmpfunc(c1.name, c2.name)

164
src/charmapdlg.py Normal file
View File

@ -0,0 +1,164 @@
import gutil
import util
import wx
class CharMapDlg(wx.Dialog):
def __init__(self, parent, ctrl):
wx.Dialog.__init__(self, parent, -1, "Character map")
self.ctrl = ctrl
hsizer = wx.BoxSizer(wx.HORIZONTAL)
self.charMap = MyCharMap(self)
hsizer.Add(self.charMap)
self.insertButton = wx.Button(self, -1, " Insert character ")
hsizer.Add(self.insertButton, 0, wx.ALL, 10)
self.Bind(wx.EVT_BUTTON, self.OnInsert, id=self.insertButton.GetId())
gutil.btnDblClick(self.insertButton, self.OnInsert)
util.finishWindow(self, hsizer, 0)
def OnInsert(self, event):
if self.charMap.selected:
self.ctrl.OnKeyChar(util.MyKeyEvent(ord(self.charMap.selected)))
class MyCharMap(wx.Window):
def __init__(self, parent):
wx.Window.__init__(self, parent, -1)
self.selected = None
# all valid characters
self.chars = ""
for i in range(256):
if util.isValidInputChar(i):
self.chars += chr(i)
self.cols = 16
self.rows = len(self.chars) // self.cols
if len(self.chars) % 16:
self.rows += 1
# offset of grid
self.offset = 5
# size of a single character cell
self.cellSize = 32
# size of the zoomed-in character boxes
self.boxSize = 60
self.smallFont = util.createPixelFont(18,
wx.FONTFAMILY_SWISS, wx.NORMAL, wx.NORMAL)
self.normalFont = util.createPixelFont(self.cellSize - 2,
wx.FONTFAMILY_MODERN, wx.NORMAL, wx.BOLD)
self.bigFont = util.createPixelFont(self.boxSize - 2,
wx.FONTFAMILY_MODERN, wx.NORMAL, wx.BOLD)
self.Bind(wx.EVT_PAINT, self.OnPaint)
self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
self.Bind(wx.EVT_MOTION, self.OnMotion)
self.Bind(wx.EVT_SIZE, self.OnSize)
util.setWH(self, self.cols * self.cellSize + 2 * self.offset, 460)
def OnSize(self, event):
size = self.GetClientSize()
self.screenBuf = wx.Bitmap(size.width, size.height)
def OnLeftDown(self, event):
pos = event.GetPosition()
x = (pos.x - self.offset) // self.cellSize
y = (pos.y - self.offset) // self.cellSize
self.selected = None
if (x >= 0) and (x < self.cols) and (y >= 0) and (y <= self.rows):
i = y * self.cols + x
if i < len(self.chars):
self.selected = self.chars[i]
self.Refresh(False)
def OnMotion(self, event):
if event.LeftIsDown():
self.OnLeftDown(event)
def OnPaint(self, event):
dc = wx.BufferedPaintDC(self, self.screenBuf)
size = self.GetClientSize()
dc.SetBrush(wx.WHITE_BRUSH)
dc.SetPen(wx.WHITE_PEN)
dc.DrawRectangle(0, 0, size.width, size.height)
dc.SetPen(wx.BLACK_PEN)
dc.SetTextForeground(wx.BLACK)
for y in range(self.rows + 1):
util.drawLine(dc, self.offset, self.offset + y * self.cellSize,
self.cols * self.cellSize + 1, 0)
for x in range(self.cols + 1):
util.drawLine(dc, self.offset + x * self.cellSize,
self.offset, 0, self.rows * self.cellSize)
dc.SetFont(self.normalFont)
for y in range(self.rows):
for x in range(self.cols):
i = y * self.cols + x
if i < len(self.chars):
util.drawText(dc, self.chars[i],
x * self.cellSize + self.offset + self.cellSize // 2 + 1,
y * self.cellSize + self.offset + self.cellSize // 2 + 1,
util.ALIGN_CENTER, util.VALIGN_CENTER)
y = self.offset + self.rows * self.cellSize
pad = 5
if self.selected:
code = ord(self.selected)
self.drawCharBox(dc, "Selected:", self.selected, self.offset,
y + pad, 75)
c = util.upper(self.selected)
if c == self.selected:
c = util.lower(self.selected)
if c == self.selected:
c = None
if c:
self.drawCharBox(dc, "Opposite case:", c, self.offset + 150,
y + pad, 110)
dc.SetFont(self.smallFont)
dc.DrawText("Character code: %d" % code, 360, y + pad)
if code == 32:
dc.DrawText("Normal space", 360, y + pad + 30)
elif code == 160:
dc.DrawText("Non-breaking space", 360, y + pad + 30)
else:
dc.SetFont(self.smallFont)
dc.DrawText("Click on a character to select it.", self.offset,
y + pad)
def drawCharBox(self, dc, text, char, x, y, xinc):
dc.SetFont(self.smallFont)
dc.DrawText(text, x, y)
boxX = x + xinc
dc.DrawRectangle(boxX, y, self.boxSize, self.boxSize)
dc.SetFont(self.bigFont)
util.drawText(dc, char, boxX + self.boxSize // 2 + 1,
y + self.boxSize // 2 + 1, util.ALIGN_CENTER, util.VALIGN_CENTER)

89
src/commandsdlg.py Normal file
View File

@ -0,0 +1,89 @@
import util
import xml.sax.saxutils as xss
import wx
import wx.html
class CommandsDlg(wx.Frame):
def __init__(self, cfgGl):
wx.Frame.__init__(self, None, -1, "Commands",
size = (650, 600), style = wx.DEFAULT_FRAME_STYLE)
self.Center()
vsizer = wx.BoxSizer(wx.VERTICAL)
self.SetSizer(vsizer)
s = '<table border="1"><tr><td><b>Key(s)</b></td>'\
'<td><b>Command</b></td></tr>'
for cmd in cfgGl.commands:
s += '<tr><td bgcolor="#dddddd" valign="top">'
if cmd.keys:
for key in cmd.keys:
k = util.Key.fromInt(key)
s += "%s<br>" % xss.escape(k.toStr())
else:
s += "No key defined<br>"
s += '</td><td valign="top">'
s += "%s" % xss.escape(cmd.desc)
s += "</td></tr>"
s += "</table>"
self.html = """
<html><head></head><body>
%s
<pre>
<b>Mouse:</b>
Left click Position cursor
Left click + drag Select text
Right click Unselect
<b>Keyboard shortcuts in Find/Replace dialog:</b>
F Find
R Replace
</pre>
</body></html>
""" % s
htmlWin = wx.html.HtmlWindow(self)
rep = htmlWin.GetInternalRepresentation()
rep.SetIndent(0, wx.html.HTML_INDENT_BOTTOM)
htmlWin.SetPage(self.html)
htmlWin.SetFocus()
vsizer.Add(htmlWin, 1, wx.EXPAND)
id = wx.NewId()
menu = wx.Menu()
menu.Append(id, "&Save as...")
mb = wx.MenuBar()
mb.Append(menu, "&File")
self.SetMenuBar(mb)
self.Bind(wx.EVT_MENU, self.OnSave, id=id)
self.Layout()
self.Bind(wx.EVT_CLOSE, self.OnCloseWindow)
def OnCloseWindow(self, event):
self.Destroy()
def OnSave(self, event):
dlg = wx.FileDialog(self, "Filename to save as",
wildcard = "HTML files (*.html)|*.html|All files|*",
style = wx.SAVE | wx.OVERWRITE_PROMPT)
if dlg.ShowModal() == wx.ID_OK:
util.writeToFile(dlg.GetPath(), self.html, self)
dlg.Destroy()

1443
src/config.py Normal file

File diff suppressed because it is too large Load Diff

405
src/dialoguechart.py Normal file
View File

@ -0,0 +1,405 @@
import gutil
import misc
import pdf
import pml
import screenplay
import util
import functools
import wx
def genDialogueChart(mainFrame, sp):
# TODO: would be nice if this behaved like the other reports, i.e. the
# junk below would be inside the class, not outside. this would allow
# testcases to be written. only complication is the minLines thing
# which would need some thinking.
inf = []
for it in [ ("Characters with < 10 lines", None),
("Sorted by: First appearance", cmpFirst),
("Sorted by: Last appearance", cmpLast),
("Sorted by: Number of lines spoken", cmpCount),
("Sorted by: Name", cmpName)
]:
inf.append(misc.CheckBoxItem(it[0], cdata = it[1]))
dlg = misc.CheckBoxDlg(mainFrame, "Report type", inf,
"Information to include:", False)
if dlg.ShowModal() != wx.ID_OK:
dlg.Destroy()
return
dlg.Destroy()
minLines = 1
if not inf[0].selected:
minLines = 10
chart = DialogueChart(sp, minLines)
if not chart.cinfo:
wx.MessageBox("No characters speaking found.", "Error", wx.OK,
mainFrame)
return
del inf[0]
if len(misc.CheckBoxItem.getClientData(inf)) == 0:
wx.MessageBox("Can't disable all output.", "Error", wx.OK,
mainFrame)
return
data = chart.generate(inf)
gutil.showTempPDF(data, sp.cfgGl, mainFrame)
class DialogueChart:
def __init__(self, sp, minLines):
self.sp = sp
ls = sp.lines
# PageInfo's for each page, 0-indexed.
self.pages = []
for i in range(len(sp.pages) - 1):
self.pages.append(PageInfo())
# map of CharInfo objects. key = name, value = CharInfo.
tmpCinfo = {}
name = "UNKNOWN"
for i in range(len(ls)):
pgNr = sp.line2page(i) -1
pi = self.pages[pgNr]
line = ls[i]
pi.addLine(line.lt)
if (line.lt == screenplay.CHARACTER) and\
(line.lb == screenplay.LB_LAST):
name = util.upper(line.text)
elif line.lt == screenplay.DIALOGUE:
pi.addLineToSpeaker(name)
ci = tmpCinfo.get(name)
if ci:
ci.addLine(pgNr)
else:
tmpCinfo[name] = CharInfo(name, pgNr)
elif line.lt != screenplay.PAREN:
name = "UNKNOWN"
# CharInfo's.
self.cinfo = []
for v in list(tmpCinfo.values()):
if v.lineCnt >= minLines:
self.cinfo.append(v)
# start Y of page markers
self.pageY = 20.0
# where dialogue density bars start and how tall they are
self.barY = 30.0
self.barHeight = 15.0
# chart Y pos
self.chartY = 50.0
# how much to leave empty on each side (mm)
self.margin = 10.0
# try point sizes 10,9,8,7,6 until all characters fit on the page
# (if 6 is still too big, too bad)
size = 10
while 1:
# character font size in points
self.charFs = size
# how many mm in Y direction for each character
self.charY = util.getTextHeight(self.charFs)
# height of chart
self.chartHeight = len(self.cinfo) * self.charY
if size <= 6:
break
if (self.chartY + self.chartHeight) <= \
(sp.cfg.paperWidth - self.margin):
break
size -= 1
# calculate maximum length of character name, and start position
# of chart from that
maxLen = 0
for ci in self.cinfo:
maxLen = max(maxLen, len(ci.name))
maxLen = max(10, maxLen)
charX = util.getTextWidth(" ", pml.COURIER, self.charFs)
# chart X pos
self.chartX = self.margin + maxLen * charX + 3
# width of chart
self.chartWidth = sp.cfg.paperHeight - self.chartX - self.margin
# page contents bar legends' size and position
self.legendWidth = 23.0
self.legendHeight = 23.0
self.legendX = self.margin + 2.0
self.legendY = self.barY + self.barHeight - self.legendHeight
# margin from legend border to first item
self.legendMargin = 2.0
# spacing from one legend item to next
self.legendSpacing = 5.0
# spacing from one legend item to next
self.legendSize = 4.0
def generate(self, cbil):
doc = pml.Document(self.sp.cfg.paperHeight,
self.sp.cfg.paperWidth)
for it in cbil:
if it.selected:
self.cinfo = sorted(self.cinfo, key=functools.cmp_to_key(it.cdata))
doc.add(self.generatePage(it.text, doc))
return pdf.generate(doc)
def generatePage(self, title, doc):
pg = pml.Page(doc)
pg.add(pml.TextOp(title, doc.w / 2.0, self.margin, 18,
pml.BOLD | pml.ITALIC | pml.UNDERLINED, util.ALIGN_CENTER))
pageCnt = len(self.pages)
mmPerPage = max(0.1, self.chartWidth / pageCnt)
pg.add(pml.TextOp("Page:", self.chartX - 1.0, self.pageY - 5.0, 10))
# draw backround for every other row. this needs to be done before
# drawing the grid.
for i in range(len(self.cinfo)):
y = self.chartY + i * self.charY
if (i % 2) == 1:
pg.add(pml.PDFOp("0.93 g"))
pg.add(pml.RectOp(self.chartX, y, self.chartWidth,
self.charY))
pg.add(pml.PDFOp("0.0 g"))
# line width to use
lw = 0.25
pg.add(pml.PDFOp("0.5 G"))
# dashed pattern
pg.add(pml.PDFOp("[2 2] 0 d"))
# draw grid and page markers
for i in range(pageCnt):
if (i == 0) or ((i + 1) % 10) == 0:
x = self.chartX + i * mmPerPage
pg.add(pml.TextOp("%d" % (i + 1), x, self.pageY,
10, align = util.ALIGN_CENTER))
if i != 0:
pg.add(pml.genLine(x, self.chartY, 0, self.chartHeight,
lw))
pg.add(pml.RectOp(self.chartX, self.chartY, self.chartWidth,
self.chartHeight, pml.NO_FILL, lw))
pg.add(pml.PDFOp("0.0 G"))
# restore normal line pattern
pg.add(pml.PDFOp("[] 0 d"))
# legend for page content bars
pg.add(pml.RectOp(self.legendX, self.legendY,
self.legendWidth, self.legendHeight, pml.NO_FILL, lw))
self.drawLegend(pg, 0, 1.0, "Other", lw)
self.drawLegend(pg, 1, 0.7, "Character", lw)
self.drawLegend(pg, 2, 0.5, "Dialogue", lw)
self.drawLegend(pg, 3, 0.3, "Action", lw)
# page content bars
for i in range(pageCnt):
x = self.chartX + i * mmPerPage
y = self.barY + self.barHeight
pi = self.pages[i]
tlc = pi.getTotalLineCount()
pg.add(pml.PDFOp("0.3 g"))
pct = util.safeDivInt(pi.getLineCount(screenplay.ACTION), tlc)
barH = self.barHeight * pct
pg.add(pml.RectOp(x, y - barH, mmPerPage, barH))
y -= barH
pg.add(pml.PDFOp("0.5 g"))
pct = util.safeDivInt(pi.getLineCount(screenplay.DIALOGUE), tlc)
barH = self.barHeight * pct
pg.add(pml.RectOp(x, y - barH, mmPerPage, barH))
y -= barH
pg.add(pml.PDFOp("0.7 g"))
pct = util.safeDivInt(pi.getLineCount(screenplay.CHARACTER), tlc)
barH = self.barHeight * pct
pg.add(pml.RectOp(x, y - barH, mmPerPage, barH))
y -= barH
pg.add(pml.PDFOp("0.0 g"))
# rectangle around page content bars
pg.add(pml.RectOp(self.chartX, self.barY, self.chartWidth,
self.barHeight, pml.NO_FILL, lw))
for i in range(len(self.cinfo)):
y = self.chartY + i * self.charY
ci = self.cinfo[i]
pg.add(pml.TextOp(ci.name, self.margin, y + self.charY / 2.0,
self.charFs, valign = util.VALIGN_CENTER))
for i in range(pageCnt):
pi = self.pages[i]
cnt = pi.getSpeakerLineCount(ci.name)
if cnt > 0:
h = self.charY * (float(cnt) / self.sp.cfg.linesOnPage)
pg.add(pml.RectOp(self.chartX + i * mmPerPage,
y + (self.charY - h) / 2.0, mmPerPage, h))
return pg
# draw a single legend for page content bars
def drawLegend(self, pg, pos, color, name, lw):
x = self.legendX + self.legendMargin
y = self.legendY + self.legendMargin + pos * self.legendSpacing
pg.add(pml.PDFOp("%f g" % color))
pg.add(pml.RectOp(x, y, self.legendSize, self.legendSize,
pml.STROKE_FILL, lw))
pg.add(pml.PDFOp("0.0 g"))
pg.add(pml.TextOp(name, x + self.legendSize + 2.0, y, 6))
# keeps track of information for one page
class PageInfo:
def __init__(self):
# how many lines of each type this page contains. key = line type,
# value = int. note that if value would be 0, this doesn't have
# the key at all, so use the helper functions below.
self.lineCounts = {}
# total line count
self.totalLineCount = -1
# how many lines each character speaks on this page. key =
# character name, value = int. note that if someone doesn't speak
# they have no entry.
self.speakers = {}
# add one line of given type.
def addLine(self, lt):
self.lineCounts[lt] = self.getLineCount(lt) + 1
# get total number of lines.
def getTotalLineCount(self):
if self.totalLineCount == -1:
self.totalLineCount = sum(iter(self.lineCounts.values()), 0)
return self.totalLineCount
# get number of lines of given type.
def getLineCount(self, lt):
return self.lineCounts.get(lt, 0)
# add one dialogue line for given speaker.
def addLineToSpeaker(self, name):
self.speakers[name] = self.getSpeakerLineCount(name) + 1
# get number of lines of dialogue for given character.
def getSpeakerLineCount(self, name):
return self.speakers.get(name, 0)
# keeps track of each character's dialogue lines.
class CharInfo:
def __init__(self, name, firstPage):
self.name = name
self.firstPage = firstPage
self.lastPage = firstPage
self.lineCnt = 1
# add a line at given page.
def addLine(self, page):
self.lastPage = page
self.lineCnt += 1
def cmpfunc(a, b):
return (a > b) - (a < b)
def cmpCount(c1, c2):
ret = cmpfunc(c2.lineCnt, c1.lineCnt)
if ret != 0:
return ret
else:
return cmpFirst(c1, c2)
def cmpCountThenName(c1, c2):
ret = cmpfunc(c2.lineCnt, c1.lineCnt)
if ret != 0:
return ret
else:
return cmpName(c1, c2)
def cmpFirst(c1, c2):
ret = cmpfunc(c1.firstPage, c2.firstPage)
if ret != 0:
return ret
else:
return cmpLastRev(c1, c2)
def cmpLast(c1, c2):
ret = cmpfunc(c1.lastPage, c2.lastPage)
if ret != 0:
return ret
else:
return cmpName(c1, c2)
def cmpLastRev(c1, c2):
ret = cmpfunc(c2.lastPage, c1.lastPage)
if ret != 0:
return ret
else:
return cmpCountThenName(c1, c2)
def cmpName(c1, c2):
return cmpfunc(c1.name, c2.name)

17
src/error.py Normal file
View File

@ -0,0 +1,17 @@
# exception classes
class TrelbyError(Exception):
def __init__(self, msg):
Exception.__init__(self, msg)
self.msg = msg
def __str__(self):
return str(self.msg)
class ConfigError(TrelbyError):
def __init__(self, msg):
TrelbyError.__init__(self, msg)
class MiscError(TrelbyError):
def __init__(self, msg):
TrelbyError.__init__(self, msg)

437
src/finddlg.py Normal file
View File

@ -0,0 +1,437 @@
import config
import gutil
import misc
import undo
import util
import wx
class FindDlg(wx.Dialog):
def __init__(self, parent, ctrl):
wx.Dialog.__init__(self, parent, -1, "Find & Replace",
style = wx.DEFAULT_DIALOG_STYLE | wx.WANTS_CHARS)
self.ctrl = ctrl
self.searchLine = -1
self.searchColumn = -1
self.searchWidth = -1
hsizer = wx.BoxSizer(wx.HORIZONTAL)
vsizer = wx.BoxSizer(wx.VERTICAL)
gsizer = wx.FlexGridSizer(2, 2, 5, 20)
gsizer.AddGrowableCol(1)
gsizer.Add(wx.StaticText(self, -1, "Find what:"), 0,
wx.ALIGN_CENTER_VERTICAL)
self.findEntry = wx.TextCtrl(self, -1, style = wx.TE_PROCESS_ENTER)
gsizer.Add(self.findEntry, 0, wx.EXPAND)
gsizer.Add(wx.StaticText(self, -1, "Replace with:"), 0,
wx.ALIGN_CENTER_VERTICAL)
self.replaceEntry = wx.TextCtrl(self, -1, style = wx.TE_PROCESS_ENTER)
gsizer.Add(self.replaceEntry, 0, wx.EXPAND)
vsizer.Add(gsizer, 0, wx.EXPAND | wx.BOTTOM, 10)
hsizer2 = wx.BoxSizer(wx.HORIZONTAL)
vsizer2 = wx.BoxSizer(wx.VERTICAL)
# wxGTK adds way more space by default than wxMSW between the
# items, have to adjust for that
pad = 0
if misc.isWindows:
pad = 5
self.matchWholeCb = wx.CheckBox(self, -1, "Match whole word only")
vsizer2.Add(self.matchWholeCb, 0, wx.TOP, pad)
self.matchCaseCb = wx.CheckBox(self, -1, "Match case")
vsizer2.Add(self.matchCaseCb, 0, wx.TOP, pad)
hsizer2.Add(vsizer2, 0, wx.EXPAND | wx.RIGHT, 10)
self.direction = wx.RadioBox(self, -1, "Direction",
choices = ["Up", "Down"])
self.direction.SetSelection(1)
hsizer2.Add(self.direction, 1, 0)
vsizer.Add(hsizer2, 0, wx.EXPAND | wx.BOTTOM, 10)
self.extraLabel = wx.StaticText(self, -1, "Search in:")
vsizer.Add(self.extraLabel)
self.elements = wx.CheckListBox(self, -1)
# sucky wxMSW doesn't support client data for checklistbox items,
# so we have to store it ourselves
self.elementTypes = []
for t in config.getTIs():
self.elements.Append(t.name)
self.elementTypes.append(t.lt)
vsizer.Add(self.elements, 1, wx.EXPAND)
hsizer.Add(vsizer, 1, wx.EXPAND)
vsizer = wx.BoxSizer(wx.VERTICAL)
find = wx.Button(self, -1, "&Find next")
vsizer.Add(find, 0, wx.EXPAND | wx.BOTTOM, 5)
replace = wx.Button(self, -1, "&Replace")
vsizer.Add(replace, 0, wx.EXPAND | wx.BOTTOM, 5)
replaceAll = wx.Button(self, -1, " Replace all ")
vsizer.Add(replaceAll, 0, wx.EXPAND | wx.BOTTOM, 5)
self.moreButton = wx.Button(self, -1, "")
vsizer.Add(self.moreButton, 0, wx.EXPAND | wx.BOTTOM, 5)
hsizer.Add(vsizer, 0, wx.EXPAND | wx.LEFT, 30)
self.Bind(wx.EVT_BUTTON, self.OnFind, id=find.GetId())
self.Bind(wx.EVT_BUTTON, self.OnReplace, id=replace.GetId())
self.Bind(wx.EVT_BUTTON, self.OnReplaceAll, id=replaceAll.GetId())
self.Bind(wx.EVT_BUTTON, self.OnMore, id=self.moreButton.GetId())
gutil.btnDblClick(find, self.OnFind)
gutil.btnDblClick(replace, self.OnReplace)
self.Bind(wx.EVT_TEXT, self.OnText, id=self.findEntry.GetId())
self.Bind(wx.EVT_TEXT_ENTER, self.OnFind, id=self.findEntry.GetId())
self.Bind(wx.EVT_TEXT_ENTER, self.OnFind, id=self.replaceEntry.GetId())
self.Bind(wx.EVT_CHAR, self.OnCharMisc)
self.findEntry.Bind(wx.EVT_CHAR, self.OnCharEntry)
self.replaceEntry.Bind(wx.EVT_CHAR, self.OnCharEntry)
find.Bind(wx.EVT_CHAR, self.OnCharButton)
replace.Bind(wx.EVT_CHAR, self.OnCharButton)
replaceAll.Bind(wx.EVT_CHAR, self.OnCharButton)
self.moreButton.Bind(wx.EVT_CHAR, self.OnCharButton)
self.matchWholeCb.Bind(wx.EVT_CHAR, self.OnCharMisc)
self.matchCaseCb.Bind(wx.EVT_CHAR, self.OnCharMisc)
self.direction.Bind(wx.EVT_CHAR, self.OnCharMisc)
self.elements.Bind(wx.EVT_CHAR, self.OnCharMisc)
util.finishWindow(self, hsizer, center = False)
self.loadState()
self.findEntry.SetFocus()
def loadState(self):
self.findEntry.SetValue(self.ctrl.findDlgFindText)
self.findEntry.SetSelection(-1, -1)
self.replaceEntry.SetValue(self.ctrl.findDlgReplaceText)
self.matchWholeCb.SetValue(self.ctrl.findDlgMatchWholeWord)
self.matchCaseCb.SetValue(self.ctrl.findDlgMatchCase)
self.direction.SetSelection(int(not self.ctrl.findDlgDirUp))
count = self.elements.GetCount()
tmp = self.ctrl.findDlgElements
if (tmp == None) or (len(tmp) != count):
tmp = [True] * self.elements.GetCount()
for i in range(count):
self.elements.Check(i, tmp[i])
self.showExtra(self.ctrl.findDlgUseExtra)
self.Center()
def saveState(self):
self.getParams()
self.ctrl.findDlgFindText = misc.fromGUI(self.findEntry.GetValue())
self.ctrl.findDlgReplaceText = misc.fromGUI(
self.replaceEntry.GetValue())
self.ctrl.findDlgMatchWholeWord = self.matchWhole
self.ctrl.findDlgMatchCase = self.matchCase
self.ctrl.findDlgDirUp = self.dirUp
self.ctrl.findDlgUseExtra = self.useExtra
tmp = []
for i in range(self.elements.GetCount()):
tmp.append(bool(self.elements.IsChecked(i)))
self.ctrl.findDlgElements = tmp
def OnMore(self, event):
self.showExtra(not self.useExtra)
def OnText(self, event):
if self.ctrl.sp.mark:
self.ctrl.sp.clearMark()
self.ctrl.updateScreen()
def OnCharEntry(self, event):
self.OnChar(event, True, False)
def OnCharButton(self, event):
self.OnChar(event, False, True)
def OnCharMisc(self, event):
self.OnChar(event, False, False)
def OnChar(self, event, isEntry, isButton):
kc = event.GetKeyCode()
if kc == wx.WXK_ESCAPE:
self.EndModal(wx.ID_OK)
return
if kc == wx.WXK_RETURN:
if isButton:
event.Skip()
return
else:
self.OnFind()
return
if isEntry:
event.Skip()
else:
if kc < 256:
if chr(kc) == "f":
self.OnFind()
elif chr(kc) == "r":
self.OnReplace()
else:
event.Skip()
else:
event.Skip()
def showExtra(self, flag):
self.extraLabel.Show(flag)
self.elements.Show(flag)
self.useExtra = flag
if flag:
self.moreButton.SetLabel("<<< Less")
pos = self.elements.GetPosition()
# don't know of a way to get the vertical spacing of items in
# a wx.CheckListBox, so estimate it at font height + 5 pixels,
# which is close enough on everything I've tested.
h = pos.y + len(self.elementTypes) * \
(util.getFontHeight(self.elements.GetFont()) + 5) + 15
else:
self.moreButton.SetLabel("More >>>")
h = max(self.extraLabel.GetPosition().y,
self.moreButton.GetPosition().y +
self.moreButton.GetClientSize().height + 5)
self.SetSizeHints(self.GetClientSize().width, h)
util.setWH(self, h = h)
def getParams(self):
self.dirUp = self.direction.GetSelection() == 0
self.matchWhole = self.matchWholeCb.IsChecked()
self.matchCase = self.matchCaseCb.IsChecked()
if self.useExtra:
self.elementMap = {}
for i in range(self.elements.GetCount()):
self.elementMap[self.elementTypes[i]] = \
self.elements.IsChecked(i)
def typeIncluded(self, lt):
if not self.useExtra:
return True
return self.elementMap[lt]
def OnFind(self, event = None, autoFind = False):
if not autoFind:
self.getParams()
value = misc.fromGUI(self.findEntry.GetValue())
if not self.matchCase:
value = util.upper(value)
if value == "":
return
self.searchWidth = len(value)
if self.dirUp:
inc = -1
else:
inc = 1
line = self.ctrl.sp.line
col = self.ctrl.sp.column
ls = self.ctrl.sp.lines
if (line == self.searchLine) and (col == self.searchColumn):
text = ls[line].text
col += inc
if col >= len(text):
line += 1
col = 0
elif col < 0:
line -= 1
if line >= 0:
col = max(len(ls[line].text) - 1, 0)
fullSearch = False
if inc > 0:
if (line == 0) and (col == 0):
fullSearch = True
else:
if (line == (len(ls) - 1)) and (col == (len(ls[line].text))):
fullSearch = True
self.searchLine = -1
while True:
found = False
while True:
if (line >= len(ls)) or (line < 0):
break
if self.typeIncluded(ls[line].lt):
text = ls[line].text
value = str(value)
if not self.matchCase:
text = util.upper(text)
if inc > 0:
res = text.find(value, col)
else:
res = text.rfind(value, 0, col + 1)
if res != -1:
if not self.matchWhole or (
util.isWordBoundary(text[res - 1 : res]) and
util.isWordBoundary(text[res + len(value) :
res + len(value) + 1])):
found = True
break
line += inc
if inc > 0:
col = 0
else:
if line >= 0:
col = max(len(ls[line].text) - 1, 0)
if found:
self.searchLine = line
self.searchColumn = res
self.ctrl.sp.gotoPos(line, res)
self.ctrl.sp.setMark(line, res + self.searchWidth - 1)
if not autoFind:
self.ctrl.makeLineVisible(line)
self.ctrl.updateScreen()
break
else:
if autoFind:
break
if fullSearch:
wx.MessageBox("Search finished without results.",
"No matches", wx.OK, self)
break
if inc > 0:
s1 = "end"
s2 = "start"
restart = 0
else:
s1 = "start"
s2 = "end"
restart = len(ls) - 1
if wx.MessageBox("Search finished at the %s of the script. Do\n"
"you want to continue at the %s of the script?"
% (s1, s2), "Continue?",
wx.YES_NO | wx.YES_DEFAULT, self) == wx.YES:
line = restart
fullSearch = True
else:
break
if not autoFind:
self.ctrl.updateScreen()
def OnReplace(self, event = None, autoFind = False):
if self.searchLine == -1:
return False
value = util.toInputStr(misc.fromGUI(self.replaceEntry.GetValue()))
ls = self.ctrl.sp.lines
sp = self.ctrl.sp
u = undo.SinglePara(sp, undo.CMD_MISC, self.searchLine)
ls[self.searchLine].text = util.replace(
ls[self.searchLine].text, value,
self.searchColumn, self.searchWidth)
sp.rewrapPara(sp.getParaFirstIndexFromLine(self.searchLine))
self.searchLine = -1
diff = len(value) - self.searchWidth
if not self.dirUp:
sp.column += self.searchWidth + diff
else:
sp.column -= 1
if sp.column < 0:
sp.line -= 1
if sp.line < 0:
sp.line = 0
sp.column = 0
self.searchLine = 0
self.searchColumn = 0
self.searchWidth = 0
else:
sp.column = len(ls[sp.line].text)
sp.clearMark()
sp.markChanged()
u.setAfter(sp)
sp.addUndo(u)
self.OnFind(autoFind = autoFind)
return True
def OnReplaceAll(self, event = None):
self.getParams()
if self.searchLine == -1:
self.OnFind(autoFind = True)
count = 0
while self.OnReplace(autoFind = True):
count += 1
if count != 0:
self.ctrl.makeLineVisible(self.ctrl.sp.line)
self.ctrl.updateScreen()
wx.MessageBox("Replaced %d matches" % count, "Results", wx.OK, self)

376
src/fontinfo.py Normal file
View File

@ -0,0 +1,376 @@
import pml
# character widths and general font information for each font. acquired
# from the PDF font metrics. ((width / 1000) * point_size) / 72.0 = how
# many inches wide that character is.
#
# all Courier-* fonts have characters 600 units wide.
# get the FontMetrics object for the given style
def getMetrics(style):
# the "& 15" gets rid of the underline flag
return _fontMetrics[style & 15]
class FontMetrics:
def __init__(self, fontWeight, flags, bbox, italicAngle, ascent, descent,
capHeight, stemV, stemH, xHeight, widths):
# character widths in an array of 256 integers, or None for the
# Courier fonts.
self.widths = widths
# see the PDF spec for the details on what these are.
self.fontWeight = fontWeight
self.flags = flags
self.bbox = bbox
self.italicAngle = italicAngle
self.ascent = ascent
self.descent = descent
self.capHeight = capHeight
self.stemV = stemV
self.stemH = stemH
self.xHeight = xHeight
# calculate width of 'text' in 'size', and return it in 1/72 inch
# units.
def getTextWidth(self, text, size):
widths = self.widths
# Courier
if not widths:
return 0.6 * (size * len(text))
total = 0
for ch in text:
total += widths[ord(ch)]
return (total / 1000.0) * size
_fontMetrics = {
pml.COURIER : FontMetrics(
fontWeight = 400, flags = 35, bbox = (-23, -250, 715, 805),
italicAngle = 0, ascent = 629, descent = -157, capHeight = 562,
stemV = 51, stemH = 51, xHeight = 426, widths = None),
pml.COURIER | pml.BOLD : FontMetrics(
fontWeight = 700, flags = 35, bbox = (-113, -250, 749, 801),
italicAngle = 0, ascent = 629, descent = -157, capHeight = 562,
stemV = 106, stemH = 84, xHeight = 439, widths = None),
pml.COURIER | pml.ITALIC : FontMetrics(
fontWeight = 400, flags = 99, bbox = (-27, -250, 849, 805),
italicAngle = -12, ascent = 629, descent = -157, capHeight = 562,
stemV = 51, stemH = 51, xHeight = 426, widths = None),
pml.COURIER | pml.BOLD | pml.ITALIC : FontMetrics(
fontWeight = 700, flags = 99, bbox = (-57, -250, 869, 801),
italicAngle = -12, ascent = 629, descent = -157, capHeight = 562,
stemV = 106, stemH = 84, xHeight = 439, widths = None),
pml.HELVETICA : FontMetrics(
fontWeight = 400, flags = 32, bbox = (-166, -225, 1000, 931),
italicAngle = 0, ascent = 718, descent = -207, capHeight = 718,
stemV = 88, stemH = 76, xHeight = 523, widths = [
545, 545, 545, 545, 545, 545, 545, 545,
545, 545, 545, 545, 545, 545, 545, 545,
545, 545, 545, 545, 545, 545, 545, 545,
545, 545, 545, 545, 545, 545, 545, 545,
278, 278, 355, 556, 556, 889, 667, 191,
333, 333, 389, 584, 278, 333, 278, 278,
556, 556, 556, 556, 556, 556, 556, 556,
556, 556, 278, 278, 584, 584, 584, 556,
1015, 667, 667, 722, 722, 667, 611, 778,
722, 278, 500, 667, 556, 833, 722, 778,
667, 778, 722, 667, 611, 722, 667, 944,
667, 667, 611, 278, 278, 278, 469, 556,
333, 556, 556, 500, 556, 556, 278, 556,
556, 222, 222, 500, 222, 833, 556, 556,
556, 556, 333, 500, 278, 556, 500, 722,
500, 500, 500, 334, 260, 334, 584, 545,
545, 545, 545, 545, 545, 545, 545, 545,
545, 545, 545, 545, 545, 545, 545, 545,
545, 545, 545, 545, 545, 545, 545, 545,
545, 545, 545, 545, 545, 545, 545, 545,
545, 333, 556, 556, 556, 556, 260, 556,
333, 737, 370, 556, 584, 545, 737, 333,
400, 584, 333, 333, 333, 556, 537, 278,
333, 333, 365, 556, 834, 834, 834, 611,
667, 667, 667, 667, 667, 667, 1000, 722,
667, 667, 667, 667, 278, 278, 278, 278,
722, 722, 778, 778, 778, 778, 778, 584,
778, 722, 722, 722, 722, 667, 667, 611,
556, 556, 556, 556, 556, 556, 889, 500,
556, 556, 556, 556, 278, 278, 278, 278,
556, 556, 556, 556, 556, 556, 556, 584,
611, 556, 556, 556, 556, 500, 556, 500
]),
pml.HELVETICA | pml.BOLD : FontMetrics(
fontWeight = 700, flags = 32, bbox = (-170, -228, 1003, 962),
italicAngle = 0, ascent = 718, descent = -207, capHeight = 718,
stemV = 140, stemH = 118, xHeight = 532, widths = [
564, 564, 564, 564, 564, 564, 564, 564,
564, 564, 564, 564, 564, 564, 564, 564,
564, 564, 564, 564, 564, 564, 564, 564,
564, 564, 564, 564, 564, 564, 564, 564,
278, 333, 474, 556, 556, 889, 722, 238,
333, 333, 389, 584, 278, 333, 278, 278,
556, 556, 556, 556, 556, 556, 556, 556,
556, 556, 333, 333, 584, 584, 584, 611,
975, 722, 722, 722, 722, 667, 611, 778,
722, 278, 556, 722, 611, 833, 722, 778,
667, 778, 722, 667, 611, 722, 667, 944,
667, 667, 611, 333, 278, 333, 584, 556,
333, 556, 611, 556, 611, 556, 333, 611,
611, 278, 278, 556, 278, 889, 611, 611,
611, 611, 389, 556, 333, 611, 556, 778,
556, 556, 500, 389, 280, 389, 584, 564,
564, 564, 564, 564, 564, 564, 564, 564,
564, 564, 564, 564, 564, 564, 564, 564,
564, 564, 564, 564, 564, 564, 564, 564,
564, 564, 564, 564, 564, 564, 564, 564,
564, 333, 556, 556, 556, 556, 280, 556,
333, 737, 370, 556, 584, 564, 737, 333,
400, 584, 333, 333, 333, 611, 556, 278,
333, 333, 365, 556, 834, 834, 834, 611,
722, 722, 722, 722, 722, 722, 1000, 722,
667, 667, 667, 667, 278, 278, 278, 278,
722, 722, 778, 778, 778, 778, 778, 584,
778, 722, 722, 722, 722, 667, 667, 611,
556, 556, 556, 556, 556, 556, 889, 556,
556, 556, 556, 556, 278, 278, 278, 278,
611, 611, 611, 611, 611, 611, 611, 584,
611, 611, 611, 611, 611, 556, 611, 556,
]),
pml.HELVETICA | pml.ITALIC : FontMetrics(
fontWeight = 400, flags = 96, bbox = (-170, -225, 1116, 931),
italicAngle = -12, ascent = 718, descent = -207, capHeight = 718,
stemV = 88, stemH = 76, xHeight = 523, widths = [
545, 545, 545, 545, 545, 545, 545, 545,
545, 545, 545, 545, 545, 545, 545, 545,
545, 545, 545, 545, 545, 545, 545, 545,
545, 545, 545, 545, 545, 545, 545, 545,
278, 278, 355, 556, 556, 889, 667, 191,
333, 333, 389, 584, 278, 333, 278, 278,
556, 556, 556, 556, 556, 556, 556, 556,
556, 556, 278, 278, 584, 584, 584, 556,
1015, 667, 667, 722, 722, 667, 611, 778,
722, 278, 500, 667, 556, 833, 722, 778,
667, 778, 722, 667, 611, 722, 667, 944,
667, 667, 611, 278, 278, 278, 469, 556,
333, 556, 556, 500, 556, 556, 278, 556,
556, 222, 222, 500, 222, 833, 556, 556,
556, 556, 333, 500, 278, 556, 500, 722,
500, 500, 500, 334, 260, 334, 584, 545,
545, 545, 545, 545, 545, 545, 545, 545,
545, 545, 545, 545, 545, 545, 545, 545,
545, 545, 545, 545, 545, 545, 545, 545,
545, 545, 545, 545, 545, 545, 545, 545,
545, 333, 556, 556, 556, 556, 260, 556,
333, 737, 370, 556, 584, 545, 737, 333,
400, 584, 333, 333, 333, 556, 537, 278,
333, 333, 365, 556, 834, 834, 834, 611,
667, 667, 667, 667, 667, 667, 1000, 722,
667, 667, 667, 667, 278, 278, 278, 278,
722, 722, 778, 778, 778, 778, 778, 584,
778, 722, 722, 722, 722, 667, 667, 611,
556, 556, 556, 556, 556, 556, 889, 500,
556, 556, 556, 556, 278, 278, 278, 278,
556, 556, 556, 556, 556, 556, 556, 584,
611, 556, 556, 556, 556, 500, 556, 500,
]),
pml.HELVETICA | pml.BOLD | pml.ITALIC : FontMetrics(
fontWeight = 700, flags = 96, bbox = (-174, -228, 1114, 962),
italicAngle = -12, ascent = 718, descent = -207, capHeight = 718,
stemV = 140, stemH = 118, xHeight = 532, widths = [
564, 564, 564, 564, 564, 564, 564, 564,
564, 564, 564, 564, 564, 564, 564, 564,
564, 564, 564, 564, 564, 564, 564, 564,
564, 564, 564, 564, 564, 564, 564, 564,
278, 333, 474, 556, 556, 889, 722, 238,
333, 333, 389, 584, 278, 333, 278, 278,
556, 556, 556, 556, 556, 556, 556, 556,
556, 556, 333, 333, 584, 584, 584, 611,
975, 722, 722, 722, 722, 667, 611, 778,
722, 278, 556, 722, 611, 833, 722, 778,
667, 778, 722, 667, 611, 722, 667, 944,
667, 667, 611, 333, 278, 333, 584, 556,
333, 556, 611, 556, 611, 556, 333, 611,
611, 278, 278, 556, 278, 889, 611, 611,
611, 611, 389, 556, 333, 611, 556, 778,
556, 556, 500, 389, 280, 389, 584, 564,
564, 564, 564, 564, 564, 564, 564, 564,
564, 564, 564, 564, 564, 564, 564, 564,
564, 564, 564, 564, 564, 564, 564, 564,
564, 564, 564, 564, 564, 564, 564, 564,
564, 333, 556, 556, 556, 556, 280, 556,
333, 737, 370, 556, 584, 564, 737, 333,
400, 584, 333, 333, 333, 611, 556, 278,
333, 333, 365, 556, 834, 834, 834, 611,
722, 722, 722, 722, 722, 722, 1000, 722,
667, 667, 667, 667, 278, 278, 278, 278,
722, 722, 778, 778, 778, 778, 778, 584,
778, 722, 722, 722, 722, 667, 667, 611,
556, 556, 556, 556, 556, 556, 889, 556,
556, 556, 556, 556, 278, 278, 278, 278,
611, 611, 611, 611, 611, 611, 611, 584,
611, 611, 611, 611, 611, 556, 611, 556,
]),
pml.TIMES_ROMAN : FontMetrics(
fontWeight = 400, flags = 34, bbox = (-168, -218, 1000, 898),
italicAngle = 0, ascent = 683, descent = -217, capHeight = 662,
stemV = 84, stemH = 28, xHeight = 450, widths = [
516, 516, 516, 516, 516, 516, 516, 516,
516, 516, 516, 516, 516, 516, 516, 516,
516, 516, 516, 516, 516, 516, 516, 516,
516, 516, 516, 516, 516, 516, 516, 516,
250, 333, 408, 500, 500, 833, 778, 180,
333, 333, 500, 564, 250, 333, 250, 278,
500, 500, 500, 500, 500, 500, 500, 500,
500, 500, 278, 278, 564, 564, 564, 444,
921, 722, 667, 667, 722, 611, 556, 722,
722, 333, 389, 722, 611, 889, 722, 722,
556, 722, 667, 556, 611, 722, 722, 944,
722, 722, 611, 333, 278, 333, 469, 500,
333, 444, 500, 444, 500, 444, 333, 500,
500, 278, 278, 500, 278, 778, 500, 500,
500, 500, 333, 389, 278, 500, 500, 722,
500, 500, 444, 480, 200, 480, 541, 516,
516, 516, 516, 516, 516, 516, 516, 516,
516, 516, 516, 516, 516, 516, 516, 516,
516, 516, 516, 516, 516, 516, 516, 516,
516, 516, 516, 516, 516, 516, 516, 516,
516, 333, 500, 500, 500, 500, 200, 500,
333, 760, 276, 500, 564, 516, 760, 333,
400, 564, 300, 300, 333, 500, 453, 250,
333, 300, 310, 500, 750, 750, 750, 444,
722, 722, 722, 722, 722, 722, 889, 667,
611, 611, 611, 611, 333, 333, 333, 333,
722, 722, 722, 722, 722, 722, 722, 564,
722, 722, 722, 722, 722, 722, 556, 500,
444, 444, 444, 444, 444, 444, 667, 444,
444, 444, 444, 444, 278, 278, 278, 278,
500, 500, 500, 500, 500, 500, 500, 564,
500, 500, 500, 500, 500, 500, 500, 500,
]),
pml.TIMES_ROMAN | pml.BOLD : FontMetrics(
fontWeight = 700, flags = 34, bbox = (-168, -218, 1000, 935),
italicAngle = 0, ascent = 683, descent = -217, capHeight = 676,
stemV = 139, stemH = 44, xHeight = 461, widths = [
540, 540, 540, 540, 540, 540, 540, 540,
540, 540, 540, 540, 540, 540, 540, 540,
540, 540, 540, 540, 540, 540, 540, 540,
540, 540, 540, 540, 540, 540, 540, 540,
250, 333, 555, 500, 500, 1000, 833, 278,
333, 333, 500, 570, 250, 333, 250, 278,
500, 500, 500, 500, 500, 500, 500, 500,
500, 500, 333, 333, 570, 570, 570, 500,
930, 722, 667, 722, 722, 667, 611, 778,
778, 389, 500, 778, 667, 944, 722, 778,
611, 778, 722, 556, 667, 722, 722, 1000,
722, 722, 667, 333, 278, 333, 581, 500,
333, 500, 556, 444, 556, 444, 333, 500,
556, 278, 333, 556, 278, 833, 556, 500,
556, 556, 444, 389, 333, 556, 500, 722,
500, 500, 444, 394, 220, 394, 520, 540,
540, 540, 540, 540, 540, 540, 540, 540,
540, 540, 540, 540, 540, 540, 540, 540,
540, 540, 540, 540, 540, 540, 540, 540,
540, 540, 540, 540, 540, 540, 540, 540,
540, 333, 500, 500, 500, 500, 220, 500,
333, 747, 300, 500, 570, 540, 747, 333,
400, 570, 300, 300, 333, 556, 540, 250,
333, 300, 330, 500, 750, 750, 750, 500,
722, 722, 722, 722, 722, 722, 1000, 722,
667, 667, 667, 667, 389, 389, 389, 389,
722, 722, 778, 778, 778, 778, 778, 570,
778, 722, 722, 722, 722, 722, 611, 556,
500, 500, 500, 500, 500, 500, 722, 444,
444, 444, 444, 444, 278, 278, 278, 278,
500, 556, 500, 500, 500, 500, 500, 570,
500, 556, 556, 556, 556, 500, 556, 500,
]),
pml.TIMES_ROMAN | pml.ITALIC : FontMetrics(
fontWeight = 400, flags = 98, bbox = (-169, -217, 1010, 883),
italicAngle = -15.5, ascent = 683, descent = -217, capHeight = 653,
stemV = 76, stemH = 32, xHeight = 441, widths = [
513, 513, 513, 513, 513, 513, 513, 513,
513, 513, 513, 513, 513, 513, 513, 513,
513, 513, 513, 513, 513, 513, 513, 513,
513, 513, 513, 513, 513, 513, 513, 513,
250, 333, 420, 500, 500, 833, 778, 214,
333, 333, 500, 675, 250, 333, 250, 278,
500, 500, 500, 500, 500, 500, 500, 500,
500, 500, 333, 333, 675, 675, 675, 500,
920, 611, 611, 667, 722, 611, 611, 722,
722, 333, 444, 667, 556, 833, 667, 722,
611, 722, 611, 500, 556, 722, 611, 833,
611, 556, 556, 389, 278, 389, 422, 500,
333, 500, 500, 444, 500, 444, 278, 500,
500, 278, 278, 444, 278, 722, 500, 500,
500, 500, 389, 389, 278, 500, 444, 667,
444, 444, 389, 400, 275, 400, 541, 513,
513, 513, 513, 513, 513, 513, 513, 513,
513, 513, 513, 513, 513, 513, 513, 513,
513, 513, 513, 513, 513, 513, 513, 513,
513, 513, 513, 513, 513, 513, 513, 513,
513, 389, 500, 500, 500, 500, 275, 500,
333, 760, 276, 500, 675, 513, 760, 333,
400, 675, 300, 300, 333, 500, 523, 250,
333, 300, 310, 500, 750, 750, 750, 500,
611, 611, 611, 611, 611, 611, 889, 667,
611, 611, 611, 611, 333, 333, 333, 333,
722, 667, 722, 722, 722, 722, 722, 675,
722, 722, 722, 722, 722, 556, 611, 500,
500, 500, 500, 500, 500, 500, 667, 444,
444, 444, 444, 444, 278, 278, 278, 278,
500, 500, 500, 500, 500, 500, 500, 675,
500, 500, 500, 500, 500, 444, 500, 444,
]),
pml.TIMES_ROMAN | pml.BOLD | pml.ITALIC : FontMetrics(
fontWeight = 700, flags = 98, bbox = (-200, -218, 996, 921),
italicAngle = -15, ascent = 683, descent = -217, capHeight = 669,
stemV = 121, stemH = 42, xHeight = 462, widths = [
523, 523, 523, 523, 523, 523, 523, 523,
523, 523, 523, 523, 523, 523, 523, 523,
523, 523, 523, 523, 523, 523, 523, 523,
523, 523, 523, 523, 523, 523, 523, 523,
250, 389, 555, 500, 500, 833, 778, 278,
333, 333, 500, 570, 250, 333, 250, 278,
500, 500, 500, 500, 500, 500, 500, 500,
500, 500, 333, 333, 570, 570, 570, 500,
832, 667, 667, 667, 722, 667, 667, 722,
778, 389, 500, 667, 611, 889, 722, 722,
611, 722, 667, 556, 611, 722, 667, 889,
667, 611, 611, 333, 278, 333, 570, 500,
333, 500, 500, 444, 500, 444, 333, 500,
556, 278, 278, 500, 278, 778, 556, 500,
500, 500, 389, 389, 278, 556, 444, 667,
500, 444, 389, 348, 220, 348, 570, 523,
523, 523, 523, 523, 523, 523, 523, 523,
523, 523, 523, 523, 523, 523, 523, 523,
523, 523, 523, 523, 523, 523, 523, 523,
523, 523, 523, 523, 523, 523, 523, 523,
523, 389, 500, 500, 500, 500, 220, 500,
333, 747, 266, 500, 606, 523, 747, 333,
400, 570, 300, 300, 333, 576, 500, 250,
333, 300, 300, 500, 750, 750, 750, 500,
667, 667, 667, 667, 667, 667, 944, 667,
667, 667, 667, 667, 389, 389, 389, 389,
722, 722, 722, 722, 722, 722, 722, 570,
722, 722, 722, 722, 722, 611, 611, 500,
500, 500, 500, 500, 500, 500, 722, 444,
444, 444, 444, 444, 278, 278, 278, 278,
500, 556, 500, 500, 500, 500, 500, 570,
500, 556, 556, 556, 556, 444, 500, 444,
])
}

96
src/gutil.py Normal file
View File

@ -0,0 +1,96 @@
from error import MiscError,TrelbyError
import misc
import util
import os
import tempfile
if "TRELBY_TESTING" in os.environ:
import unittest.mock as mock
wx = mock.Mock()
else:
import wx
# this contains misc GUI-related functions
# since at least GTK 1.2's single-selection listbox is buggy in that if we
# don't deselect the old item manually, it does multiple selections, we
# have this function that does the following:
#
# 1) deselects current selection, if any
# 2) select the item with the given index
def listBoxSelect(lb, index):
old = lb.GetSelection()
if old!= -1:
lb.SetSelection(old, False)
lb.SetSelection(index, True)
# add (name, cdata) to the listbox at the correct place, determined by
# cmp(cdata1, cdata2).
def listBoxAdd(lb, name, cdata):
for i in range(lb.GetCount()):
if util.cmpfunc(cdata, lb.GetClientData(i)) < 0:
lb.InsertItems([name], i)
lb.SetClientData(i, cdata)
return
lb.Append(name, cdata)
# create stock button.
def createStockButton(parent, label):
# wxMSW does not really have them: it does not have any icons and it
# inconsistently adds the shortcut key to some buttons, but not to
# all, so it's better not to use them at all on Windows.
if misc.isUnix:
ids = {
"OK" : wx.ID_OK,
"Cancel" : wx.ID_CANCEL,
"Apply" : wx.ID_APPLY,
"Add" : wx.ID_ADD,
"Delete" : wx.ID_DELETE,
"Preview" : wx.ID_PREVIEW
}
return wx.Button(parent, ids[label])
else:
return wx.Button(parent, -1, label)
# wxWidgets has a bug in 2.6 on wxGTK2 where double clicking on a button
# does not send two wx.EVT_BUTTON events, only one. since the wxWidgets
# maintainers do not seem interested in fixing this
# (http://sourceforge.net/tracker/index.php?func=detail&aid=1449838&group_id=9863&atid=109863),
# we work around it ourselves by binding the left mouse button double
# click event to the same callback function on the buggy platforms.
def btnDblClick(btn, func):
if misc.isUnix:
btn.Bind(wx.EVT_LEFT_DCLICK, func)
# show PDF document 'pdfData' in an external viewer program. writes out a
# temporary file, first deleting all old temporary files, then opens PDF
# viewer application. 'mainFrame' is used as a parent for message boxes in
# case there are any errors.
def showTempPDF(pdfData, cfgGl, mainFrame):
try:
try:
util.removeTempFiles(misc.tmpPrefix)
fd, filename = tempfile.mkstemp(prefix = misc.tmpPrefix,
suffix = ".pdf")
try:
os.write(fd, pdfData.encode("UTF-8"))
finally:
os.close(fd)
util.showPDF(filename, cfgGl, mainFrame)
except IOError as xxx_todo_changeme:
(errno, strerror) = xxx_todo_changeme.args
raise MiscError("IOError: %s" % strerror)
except TrelbyError as e:
wx.MessageBox("Error writing temporary PDF file: %s" % e,
"Error", wx.OK, mainFrame)

133
src/headers.py Normal file
View File

@ -0,0 +1,133 @@
import pml
import util
# a script's headers.
class Headers:
def __init__(self):
# list of HeaderString objects
self.hdrs = []
# how many empty lines after the headers
self.emptyLinesAfter = 1
# create standard headers
def addDefaults(self):
h = HeaderString()
h.text = "${PAGE}."
h.align = util.ALIGN_RIGHT
h.line = 1
self.hdrs.append(h)
# return how many header lines there are. includes number of empty
# lines after possible headers.
def getNrOfLines(self):
nr = 0
for h in self.hdrs:
nr = max(nr, h.line)
if nr > 0:
nr += self.emptyLinesAfter
return nr
# add headers to given page. 'pageNr' must be a string.
def generatePML(self, page, pageNr, cfg):
for h in self.hdrs:
h.generatePML(page, pageNr, cfg)
# a single header string
class HeaderString:
def __init__(self):
# which line, 1-based
self.line = 1
# x offset, in characters
self.xoff = 0
# contents of string
self.text = ""
# whether this is centered in the horizontal direction
self.align = util.ALIGN_CENTER
# style flags
self.isBold = False
self.isItalic = False
self.isUnderlined = False
def generatePML(self, page, pageNr, cfg):
fl = 0
if self.isBold:
fl |= pml.BOLD
if self.isItalic:
fl |= pml.ITALIC
if self.isUnderlined:
fl |= pml.UNDERLINED
if self.align == util.ALIGN_LEFT:
x = cfg.marginLeft
elif self.align == util.ALIGN_CENTER:
x = (cfg.marginLeft + (cfg.paperWidth - cfg.marginRight)) / 2.0
else:
x = cfg.paperWidth - cfg.marginRight
fs = cfg.fontSize
if self.xoff != 0:
x += util.getTextWidth(" ", pml.COURIER, fs) * self.xoff
y = cfg.marginTop + (self.line - 1) * util.getTextHeight(fs)
text = self.text.replace("${PAGE}", pageNr)
page.add(pml.TextOp(text, x, y, fs, fl, self.align))
# parse information from s, which must be a string created by __str__,
# and set object state accordingly. keeps default settings on any
# errors, does not throw any exceptions.
#
# sample of the format: '1,0,r,,${PAGE}.'
def load(self, s):
a = util.fromUTF8(s).split(",", 4)
if len(a) != 5:
return
self.line = util.str2int(a[0], 1, 1, 5)
self.xoff = util.str2int(a[1], 0, -100, 100)
l, c, self.isBold, self.isItalic, self.isUnderlined = \
util.flags2bools(a[2], "lcbiu")
if l:
self.align = util.ALIGN_LEFT
elif c:
self.align = util.ALIGN_CENTER
else:
self.align = util.ALIGN_RIGHT
self.text = a[4]
def __str__(self):
s = "%d,%d," % (self.line, self.xoff)
if self.align == util.ALIGN_LEFT:
s += "l"
elif self.align == util.ALIGN_CENTER:
s += "c"
else:
s += "r"
s += util.bools2flags("biu", self.isBold, self.isItalic,
self.isUnderlined)
s += ",,%s" % self.text
return s

305
src/headersdlg.py Normal file
View File

@ -0,0 +1,305 @@
import gutil
import headers
import misc
import pdf
import pml
import util
import wx
class HeadersDlg(wx.Dialog):
def __init__(self, parent, headers, cfg, cfgGl, applyFunc):
wx.Dialog.__init__(self, parent, -1, "Headers",
style = wx.DEFAULT_DIALOG_STYLE)
self.headers = headers
self.cfg = cfg
self.cfgGl = cfgGl
self.applyFunc = applyFunc
# whether some events are blocked
self.block = False
self.hdrIndex = -1
if len(self.headers.hdrs) > 0:
self.hdrIndex = 0
vsizer = wx.BoxSizer(wx.VERTICAL)
hsizer = wx.BoxSizer(wx.HORIZONTAL)
hsizer.Add(wx.StaticText(self, -1, "Empty lines after headers:"), 0,
wx.ALIGN_CENTER_VERTICAL)
self.elinesEntry = wx.SpinCtrl(self, -1)
self.elinesEntry.SetRange(0, 5)
self.Bind(wx.EVT_SPINCTRL, self.OnMisc, id=self.elinesEntry.GetId())
self.elinesEntry.Bind(wx.EVT_KILL_FOCUS, self.OnKillFocus)
hsizer.Add(self.elinesEntry, 0, wx.LEFT, 10)
vsizer.Add(hsizer)
vsizer.Add(wx.StaticLine(self, -1), 0, wx.EXPAND | wx.TOP | wx.BOTTOM,
10)
tmp = wx.StaticText(self, -1, "Strings:")
vsizer.Add(tmp)
self.stringsLb = wx.ListBox(self, -1, size = (200, 100))
vsizer.Add(self.stringsLb, 0, wx.EXPAND)
hsizer = wx.BoxSizer(wx.HORIZONTAL)
self.addBtn = gutil.createStockButton(self, "Add")
hsizer.Add(self.addBtn)
self.Bind(wx.EVT_BUTTON, self.OnAddString, id=self.addBtn.GetId())
gutil.btnDblClick(self.addBtn, self.OnAddString)
self.delBtn = gutil.createStockButton(self, "Delete")
hsizer.Add(self.delBtn, 0, wx.LEFT, 10)
self.Bind(wx.EVT_BUTTON, self.OnDeleteString, id=self.delBtn.GetId())
gutil.btnDblClick(self.delBtn, self.OnDeleteString)
vsizer.Add(hsizer, 0, wx.TOP, 5)
hsizer = wx.BoxSizer(wx.HORIZONTAL)
hsizer.Add(wx.StaticText(self, -1, "Text:"), 0,
wx.ALIGN_CENTER_VERTICAL)
self.textEntry = wx.TextCtrl(self, -1)
hsizer.Add(self.textEntry, 1, wx.LEFT, 10)
self.Bind(wx.EVT_TEXT, self.OnMisc, id=self.textEntry.GetId())
vsizer.Add(hsizer, 0, wx.EXPAND | wx.TOP, 20)
vsizer.Add(wx.StaticText(self, -1,
"'${PAGE}' will be replaced by the page number."), 0,
wx.ALIGN_CENTER | wx.TOP, 5)
hsizerTop = wx.BoxSizer(wx.HORIZONTAL)
gsizer = wx.FlexGridSizer(3, 2, 5, 0)
gsizer.Add(wx.StaticText(self, -1, "Header line:"), 0,
wx.ALIGN_CENTER_VERTICAL)
self.lineEntry = wx.SpinCtrl(self, -1)
self.lineEntry.SetRange(1, 5)
self.Bind(wx.EVT_SPINCTRL, self.OnMisc, id=self.lineEntry.GetId())
self.lineEntry.Bind(wx.EVT_KILL_FOCUS, self.OnKillFocus)
gsizer.Add(self.lineEntry)
gsizer.Add(wx.StaticText(self, -1, "X offset (characters):"), 0,
wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 10)
self.xoffEntry = wx.SpinCtrl(self, -1)
self.xoffEntry.SetRange(-100, 100)
self.Bind(wx.EVT_SPINCTRL, self.OnMisc, id=self.xoffEntry.GetId())
self.xoffEntry.Bind(wx.EVT_KILL_FOCUS, self.OnKillFocus)
gsizer.Add(self.xoffEntry)
gsizer.Add(wx.StaticText(self, -1, "Alignment:"), 0,
wx.ALIGN_CENTER_VERTICAL)
self.alignCombo = wx.ComboBox(self, -1, style = wx.CB_READONLY)
for it in [ ("Left", util.ALIGN_LEFT), ("Center", util.ALIGN_CENTER),
("Right", util.ALIGN_RIGHT) ]:
self.alignCombo.Append(it[0], it[1])
gsizer.Add(self.alignCombo)
self.Bind(wx.EVT_COMBOBOX, self.OnMisc, id=self.alignCombo.GetId())
hsizerTop.Add(gsizer)
bsizer = wx.StaticBoxSizer(
wx.StaticBox(self, -1, "Style"), wx.HORIZONTAL)
vsizer2 = wx.BoxSizer(wx.VERTICAL)
# wxGTK adds way more space by default than wxMSW between the
# items, have to adjust for that
pad = 0
if misc.isWindows:
pad = 5
self.addCheckBox("Bold", self, vsizer2, pad)
self.addCheckBox("Italic", self, vsizer2, pad)
self.addCheckBox("Underlined", self, vsizer2, pad)
bsizer.Add(vsizer2)
hsizerTop.Add(bsizer, 0, wx.LEFT, 40)
vsizer.Add(hsizerTop, 0, wx.TOP, 20)
hsizer = wx.BoxSizer(wx.HORIZONTAL)
hsizer.Add((1, 1), 1)
previewBtn = gutil.createStockButton(self, "Preview")
hsizer.Add(previewBtn)
applyBtn = gutil.createStockButton(self, "Apply")
hsizer.Add(applyBtn, 0, wx.LEFT, 10)
cancelBtn = gutil.createStockButton(self, "Cancel")
hsizer.Add(cancelBtn, 0, wx.LEFT, 10)
okBtn = gutil.createStockButton(self, "OK")
hsizer.Add(okBtn, 0, wx.LEFT, 10)
vsizer.Add(hsizer, 0, wx.EXPAND | wx.TOP, 20)
util.finishWindow(self, vsizer)
self.Bind(wx.EVT_BUTTON, self.OnPreview, id=previewBtn.GetId())
self.Bind(wx.EVT_BUTTON, self.OnApply, id=applyBtn.GetId())
self.Bind(wx.EVT_BUTTON, self.OnCancel, id=cancelBtn.GetId())
self.Bind(wx.EVT_BUTTON, self.OnOK, id=okBtn.GetId())
self.Bind(wx.EVT_LISTBOX, self.OnStringsLb, id=self.stringsLb.GetId())
# list of widgets that are specific to editing the selected string
self.widList = [ self.textEntry, self.xoffEntry, self.alignCombo,
self.lineEntry, self.boldCb, self.italicCb,
self.underlinedCb ]
self.updateGui()
self.textEntry.SetFocus()
def addCheckBox(self, name, parent, sizer, pad):
cb = wx.CheckBox(parent, -1, name)
self.Bind(wx.EVT_CHECKBOX, self.OnMisc, id=cb.GetId())
sizer.Add(cb, 0, wx.TOP, pad)
setattr(self, name.lower() + "Cb", cb)
def OnOK(self, event):
self.EndModal(wx.ID_OK)
def OnCancel(self, event):
self.EndModal(wx.ID_CANCEL)
def OnApply(self, event):
self.applyFunc(self.headers)
def OnPreview(self, event):
doc = pml.Document(self.cfg.paperWidth, self.cfg.paperHeight)
pg = pml.Page(doc)
self.headers.generatePML(pg, "42", self.cfg)
fs = self.cfg.fontSize
chY = util.getTextHeight(fs)
y = self.cfg.marginTop + self.headers.getNrOfLines() * chY
pg.add(pml.TextOp("Mindy runs away from the dinosaur, but trips on"
" the power", self.cfg.marginLeft, y, fs))
pg.add(pml.TextOp("cord. The raptor approaches her slowly.",
self.cfg.marginLeft, y + chY, fs))
doc.add(pg)
tmp = pdf.generate(doc)
gutil.showTempPDF(tmp, self.cfgGl, self)
def OnKillFocus(self, event):
self.OnMisc()
# if we don't call this, the spin entry on wxGTK gets stuck in
# some weird state
event.Skip()
def OnStringsLb(self, event = None):
self.hdrIndex = self.stringsLb.GetSelection()
self.updateHeaderGui()
def OnAddString(self, event):
h = headers.HeaderString()
h.text = "new string"
self.headers.hdrs.append(h)
self.hdrIndex = len(self.headers.hdrs) - 1
self.updateGui()
def OnDeleteString(self, event):
if self.hdrIndex == -1:
return
del self.headers.hdrs[self.hdrIndex]
self.hdrIndex = min(self.hdrIndex, len(self.headers.hdrs) - 1)
self.updateGui()
# update listbox
def updateGui(self):
self.stringsLb.Clear()
self.elinesEntry.SetValue(self.headers.emptyLinesAfter)
self.delBtn.Enable(self.hdrIndex != -1)
for h in self.headers.hdrs:
self.stringsLb.Append(h.text)
if self.hdrIndex != -1:
self.stringsLb.SetSelection(self.hdrIndex)
self.updateHeaderGui()
# update selected header stuff
def updateHeaderGui(self):
if self.hdrIndex == -1:
for w in self.widList:
w.Disable()
self.textEntry.SetValue("")
self.lineEntry.SetValue(1)
self.xoffEntry.SetValue(0)
self.boldCb.SetValue(False)
self.italicCb.SetValue(False)
self.underlinedCb.SetValue(False)
return
self.block = True
h = self.headers.hdrs[self.hdrIndex]
for w in self.widList:
w.Enable(True)
self.textEntry.SetValue(h.text)
self.xoffEntry.SetValue(h.xoff)
util.reverseComboSelect(self.alignCombo, h.align)
self.lineEntry.SetValue(h.line)
self.boldCb.SetValue(h.isBold)
self.italicCb.SetValue(h.isItalic)
self.underlinedCb.SetValue(h.isUnderlined)
self.block = False
def OnMisc(self, event = None):
self.headers.emptyLinesAfter = util.getSpinValue(self.elinesEntry)
if (self.hdrIndex == -1) or self.block:
return
h = self.headers.hdrs[self.hdrIndex]
h.text = util.toInputStr(misc.fromGUI(self.textEntry.GetValue()))
self.stringsLb.SetString(self.hdrIndex, h.text)
h.xoff = util.getSpinValue(self.xoffEntry)
h.line = util.getSpinValue(self.lineEntry)
h.align = self.alignCombo.GetClientData(self.alignCombo.GetSelection())
h.isBold = self.boldCb.GetValue()
h.isItalic = self.italicCb.GetValue()
h.isUnderlined = self.underlinedCb.GetValue()

140
src/locationreport.py Normal file
View File

@ -0,0 +1,140 @@
import misc
import pdf
import pml
import screenplay
import util
import functools
import operator
class LocationReport:
# sr = SceneReport
def __init__(self, sr):
self.sp = sr.sp
# key = scene name, value = LocationInfo. note that multiple keys
# can point to the same LocationInfo.
locations = {}
# like locations, but this one stores per-scene information
self.scenes = {}
# make grouped scenes point to the same LocationInfos.
for sceneList in self.sp.locations.locations:
li = LocationInfo(self.sp)
for scene in sceneList:
locations[scene] = li
# merge scene information for locations and store scene
# information
for si in sr.scenes:
locations.setdefault(si.name, LocationInfo(self.sp)).addScene(si)
self.scenes.setdefault(si.name, LocationInfo(self.sp)).\
addScene(si)
# remove empty LocationInfos, sort them and store to a list
tmp = []
for li in locations.values():
if (len(li.scenes) > 0) and (li not in tmp):
tmp.append(li)
def cmpfunc(a, b):
return (a > b) - (a < b)
def sortFunc(o1, o2):
ret = cmpfunc(o2.lines, o1.lines)
if ret != 0:
return ret
else:
return cmpfunc(o1.scenes[0], o2.scenes[0])
tmp = sorted(tmp, key=functools.cmp_to_key(sortFunc))
self.locations = tmp
# information about what to include (and yes, the comma is needed
# to unpack the list)
self.INF_SPEAKERS, = list(range(1))
self.inf = []
for s in ["Speakers"]:
self.inf.append(misc.CheckBoxItem(s))
def generate(self):
tf = pml.TextFormatter(self.sp.cfg.paperWidth,
self.sp.cfg.paperHeight, 15.0, 12)
scriptLines = sum([li.lines for li in self.locations])
for li in self.locations:
tf.addSpace(5.0)
# list of (scenename, lines_in_scene) tuples, which we sort in
# DESC(lines_in_scene) ASC(scenename) order.
tmp = [(scene, self.scenes[scene].lines) for scene in li.scenes]
tmp.sort(key = operator.itemgetter(0))
tmp.sort(key = operator.itemgetter(1), reverse=True)
for scene, lines in tmp:
if len(tmp) > 1:
pct = " (%d%%)" % util.pct(lines, li.lines)
else:
pct = ""
tf.addText("%s%s" % (scene, pct), style = pml.BOLD)
tf.addSpace(1.0)
tf.addWrappedText("Lines: %d (%d%% action, %d%% of script),"
" Scenes: %d, Pages: %d (%s)" % (li.lines,
util.pct(li.actionLines, li.lines),
util.pct(li.lines, scriptLines), li.sceneCount,
len(li.pages), li.pages), " ")
if self.inf[self.INF_SPEAKERS].selected:
tf.addSpace(2.5)
for it in util.sortDict(li.chars):
tf.addText(" %3d %s" % (it[1], it[0]))
return pdf.generate(tf.doc)
# information about one location
class LocationInfo:
def __init__(self, sp):
# number of scenes
self.sceneCount = 0
# scene names, e.g. ["INT. MOTEL ROOM - NIGHT", "EXT. MOTEL -
# NIGHT"]
self.scenes = []
# total lines, excluding scene lines
self.lines = 0
# action lines
self.actionLines = 0
# page numbers
self.pages = screenplay.PageList(sp.getPageNumbers())
# key = character name (upper cased), value = number of dialogue
# lines
self.chars = {}
# add a scene. si = SceneInfo
def addScene(self, si):
if si.name not in self.scenes:
self.scenes.append(si.name)
self.sceneCount += 1
self.lines += si.lines
self.actionLines += si.actionLines
self.pages += si.pages
for name, dlines in si.chars.items():
self.chars[name] = self.chars.get(name, 0) + dlines

70
src/locations.py Normal file
View File

@ -0,0 +1,70 @@
import mypickle
import util
# manages location-information for a single screenplay. a "location" is a
# single place that can be referred to using multiple scene names, e.g.
# INT. MOTEL ROOM - DAY
# INT. MOTEL ROOM - DAY - 2 HOURS LATER
# INT. MOTEL ROOM - NIGHT
class Locations:
cvars = None
def __init__(self):
if not self.__class__.cvars:
v = self.__class__.cvars = mypickle.Vars()
v.addList("locations", [], "Locations",
mypickle.ListVar("", [], "",
mypickle.StrLatin1Var("", "", "")))
v.makeDicts()
self.__class__.cvars.setDefaults(self)
# self.locations is a list of lists of strings, where the inner
# lists list scene names to combine into one location. e.g.
# [
# [
# "INT. ROOM 413 - DAY",
# "INT. ROOM 413 - NIGHT"
# ]
# ]
# load from string 's'. does not throw any exceptions and silently
# ignores any errors.
def load(self, s):
self.cvars.load(self.cvars.makeVals(s), "", self)
# save to a string and return that.
def save(self):
return self.cvars.save("", self)
# refresh location list against the given scene names (in the format
# returned by Screenplay.getSceneNames()). removes unknown and
# duplicate scenes from locations, and if that results in a location
# with 0 scenes, removes that location completely. also upper-cases
# all the scene names, sorts the lists, first each location list's
# scenes, and then the locations based on the first scene of the
# location.
def refresh(self, sceneNames):
locs = []
added = {}
for sceneList in self.locations:
scenes = []
for scene in sceneList:
name = util.upper(scene)
if (name in sceneNames) and (name not in added):
scenes.append(name)
added[name] = None
if scenes:
scenes.sort()
locs.append(scenes)
locs.sort()
self.locations = locs

181
src/locationsdlg.py Normal file
View File

@ -0,0 +1,181 @@
import gutil
import util
import wx
class LocationsDlg(wx.Dialog):
def __init__(self, parent, sp):
wx.Dialog.__init__(self, parent, -1, "Locations",
style = wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
self.sp = sp
vsizer = wx.BoxSizer(wx.VERTICAL)
tmp = wx.StaticText(self, -1, "Locations:")
vsizer.Add(tmp)
self.locationsLb = wx.ListBox(self, -1, size = (450, 200))
vsizer.Add(self.locationsLb, 1, wx.EXPAND)
hsizer = wx.BoxSizer(wx.HORIZONTAL)
self.addBtn = gutil.createStockButton(self, "Add")
hsizer.Add(self.addBtn)
self.Bind(wx.EVT_BUTTON, self.OnAdd, id=self.addBtn.GetId())
self.delBtn = gutil.createStockButton(self, "Delete")
hsizer.Add(self.delBtn, 0, wx.LEFT, 10)
self.Bind(wx.EVT_BUTTON, self.OnDelete, id=self.delBtn.GetId())
vsizer.Add(hsizer, 0, wx.ALIGN_CENTER | wx.TOP, 10)
tmp = wx.StaticText(self, -1, "Scenes:")
vsizer.Add(tmp)
self.scenesLb = wx.ListBox(self, -1, size = (450, 200),
style = wx.LB_EXTENDED)
vsizer.Add(self.scenesLb, 1, wx.EXPAND)
hsizer = wx.BoxSizer(wx.HORIZONTAL)
hsizer.Add((1, 1), 1)
cancelBtn = gutil.createStockButton(self, "Cancel")
hsizer.Add(cancelBtn, 0, wx.LEFT, 10)
okBtn = gutil.createStockButton(self, "OK")
hsizer.Add(okBtn, 0, wx.LEFT, 10)
vsizer.Add(hsizer, 0, wx.EXPAND | wx.TOP, 10)
util.finishWindow(self, vsizer)
self.Bind(wx.EVT_BUTTON, self.OnCancel, id=cancelBtn.GetId())
self.Bind(wx.EVT_BUTTON, self.OnOK, id=okBtn.GetId())
self.fillGui()
def OnOK(self, event):
# master list
ml = []
# sub-list
sl = []
for i in range(self.locationsLb.GetCount()):
scene = self.locationsLb.GetClientData(i)
if scene:
sl.append(scene)
elif sl:
ml.append(sl)
sl = []
self.sp.locations.locations = ml
self.sp.locations.refresh(self.sp.getSceneNames())
self.EndModal(wx.ID_OK)
def OnCancel(self, event):
self.EndModal(wx.ID_CANCEL)
def OnAdd(self, event):
selected = self.scenesLb.GetSelections()
if not selected:
wx.MessageBox("No scenes selected in the lower list.", "Error",
wx.OK, self)
return
locIdx = self.locationsLb.GetSelection()
# if user has selected a separator line, treat it as no selection
if (locIdx != -1) and\
(self.locationsLb.GetClientData(locIdx) == None):
locIdx = -1
addSep = False
for idx in selected:
scene = self.scenesLb.GetClientData(idx)
# insert at selected position, or at the bottom if a new group
if locIdx != -1:
self.locationsLb.InsertItems([scene], locIdx)
self.locationsLb.SetClientData(locIdx, scene)
gutil.listBoxSelect(self.locationsLb, locIdx)
else:
addSep = True
self.locationsLb.Append(scene, scene)
locIdx = self.locationsLb.GetCount() - 1
gutil.listBoxSelect(self.locationsLb, locIdx)
if addSep:
self.locationsLb.Append("-" * 40, None)
# we need these to be in sorted order, which they probably are,
# but wxwidgets documentation doesn't say that, so to be safe we
# sort it ourselves. and as tuples can't be sorted, we change it
# to a list first.
selected = [it for it in selected]
selected.sort()
for i in range(len(selected)):
self.scenesLb.Delete(selected[i] - i)
def OnDelete(self, event):
scene = None
idx = self.locationsLb.GetSelection()
if idx != -1:
scene = self.locationsLb.GetClientData(idx)
if scene == None:
wx.MessageBox("No scene selected in the upper list.", "Error",
wx.OK, self)
return
gutil.listBoxAdd(self.scenesLb, scene, scene)
self.locationsLb.Delete(idx)
# was the last item we looked at a separator
lastWasSep = False
# go through locations, remove first encountered double separator
# (appears when a location group is deleted completely)
for i in range(self.locationsLb.GetCount()):
cdata = self.locationsLb.GetClientData(i)
if lastWasSep and (cdata == None):
self.locationsLb.Delete(i)
break
lastWasSep = cdata == None
# if it goes completely empty, remove the single separator line
if (self.locationsLb.GetCount() == 1) and\
(self.locationsLb.GetClientData(0) == None):
self.locationsLb.Delete(0)
def fillGui(self):
self.sp.locations.refresh(self.sp.getSceneNames())
separator = "-" * 40
added = {}
for locList in self.sp.locations.locations:
for scene in locList:
self.locationsLb.Append(scene, scene)
added[scene] = None
self.locationsLb.Append(separator, None)
sceneNames = sorted(self.sp.getSceneNames().keys())
for scene in sceneNames:
if scene not in added:
self.scenesLb.Append(scene, scene)

1004
src/misc.py Normal file

File diff suppressed because it is too large Load Diff

966
src/myimport.py Normal file
View File

@ -0,0 +1,966 @@
import config
import gutil
import misc
import screenplay
import util
from lxml import etree
import wx
import io
import re
import zipfile
# special linetype that means that indent contains action and scene lines,
# and scene lines are the ones that begin with "EXT." or "INT."
SCENE_ACTION = -2
# special linetype that means don't import those lines; useful for page
# numbers etc
IGNORE = -3
#like importTextFile, but for Adobe Story files.
def importAstx(fileName, frame):
# astx files are xml files. The textlines can be found under
# AdobeStory/document/stream/section/scene/paragraph which contain
# one or more textRun/break elements, to be joined. The paragraph
# attribute "element" gives us the element style.
data = util.loadFile(fileName, frame, 5000000)
if data == None:
return None
if len(data) == 0:
wx.MessageBox("File is empty.", "Error", wx.OK, frame)
return None
elemMap = {
"Action" : screenplay.ACTION,
"Character" : screenplay.CHARACTER,
"Dialog" : screenplay.DIALOGUE,
"Parenthetical" : screenplay.PAREN,
"SceneHeading" : screenplay.SCENE,
"Shot" : screenplay.SHOT,
"Transition" : screenplay.TRANSITION,
}
try:
root = etree.XML(data)
except etree.XMLSyntaxError as e:
wx.MessageBox("Error parsing file: %s" %e, "Error", wx.OK, frame)
return None
lines = []
def addElem(eleType, items):
# if elem ends in a newline, last line is empty and useless;
# get rid of it
if not items[-1] and (len(items) > 1):
items = items[:-1]
for s in items[:-1]:
lines.append(screenplay.Line(
screenplay.LB_FORCED, eleType, util.cleanInput(s)))
lines.append(screenplay.Line(
screenplay.LB_LAST, eleType, util.cleanInput(items[-1])))
for para in root.xpath("/AdobeStory/document/stream/section/scene/paragraph"):
lt = elemMap.get(para.get("element"), screenplay.ACTION)
items = []
s = ""
for text in para:
if text.tag == "textRun" and text.text:
s += text.text
elif text.tag == "break":
items.append(s.rstrip())
s = ""
items.append(s.rstrip())
addElem(lt, items)
if not lines:
wx.MessageBox("File has no content.", "Error", wx.OK, frame)
return None
return lines
# like importTextFile, but for fadein files.
def importFadein(fileName, frame):
# Fadein file is a zipped document.xml file.
# the .xml is in open screenplay format:
# http://sourceforge.net/projects/openscrfmt/files/latest/download
# the 5 MB limit is arbitrary, we just want to avoid getting a
# MemoryError exception for /dev/zero etc.
data = util.loadFile(fileName, frame, 5000000)
if data == None:
return None
if len(data) == 0:
wx.MessageBox("File is empty.", "Error", wx.OK, frame)
return None
buf = io.StringIO(data)
try:
z = zipfile.ZipFile(buf)
f = z.open("document.xml")
content = f.read()
z.close()
except:
wx.MessageBox("File is not a valid .fadein file.", "Error", wx.OK, frame)
return None
if not content:
wx.MessageBox("Script seems to be empty.", "Error", wx.OK, frame)
return None
elemMap = {
"Action" : screenplay.ACTION,
"Character" : screenplay.CHARACTER,
"Dialogue" : screenplay.DIALOGUE,
"Parenthetical" : screenplay.PAREN,
"Scene Heading" : screenplay.SCENE,
"Shot" : screenplay.SHOT,
"Transition" : screenplay.TRANSITION,
}
try:
root = etree.XML(content)
except etree.XMLSyntaxError as e:
wx.MessageBox("Error parsing file: %s" %e, "Error", wx.OK, frame)
return None
lines = []
def addElem(eleType, lns):
# if elem ends in a newline, last line is empty and useless;
# get rid of it
if not lns[-1] and (len(lns) > 1):
lns = lns[:-1]
for s in lns[:-1]:
lines.append(screenplay.Line(
screenplay.LB_FORCED, eleType, util.cleanInput(s)))
lines.append(screenplay.Line(
screenplay.LB_LAST, eleType, util.cleanInput(lns[-1])))
# removes html formatting from s, and returns list of lines.
# if s is None, return a list with single empty string.
re_rem = [r'<font[^>]*>', r'<size[^>]*>', r'<bgcolor[^>]*>']
rem = ["<b>", "</b>", "<i>", "</i>", "<u>",
"</u>", "</font>", "</size>", "</bgcolor>"]
def sanitizeStr(s):
if s:
s = "" + s
for r in re_rem:
s = re.sub(r, "", s)
for r in rem:
s = s.replace(r,"")
if s:
return s.split("<br>")
else:
return [""]
else:
return [""]
for para in root.xpath("paragraphs/para"):
# check for notes/synopsis, import as Note.
if para.get("note"):
lt = screenplay.NOTE
items = sanitizeStr("" + para.get("note"))
addElem(lt, items)
if para.get("synopsis"):
lt = screenplay.NOTE
items = sanitizeStr("" + para.get("synopsis"))
addElem(lt, items)
# look for the <style> and <text> tags. Bail if no <text> found.
styl = para.xpath("style")
txt = para.xpath("text")
if txt:
if styl:
lt = elemMap.get(styl[0].get("basestylename"), screenplay.ACTION)
else:
lt = screenplay.ACTION
items = sanitizeStr(txt[0].text)
if (lt == screenplay.PAREN) and items and (items[0][0] != "("):
items[0] = "(" + items[0]
items[-1] = items[-1] + ")"
else:
continue
addElem(lt, items)
if len(lines) == 0:
wx.MessageBox("The file contains no importable lines", "Error", wx.OK, frame)
return None
return lines
# like importTextFile, but for Celtx files.
def importCeltx(fileName, frame):
# Celtx files are zipfiles, and the script content is within a file
# called "script-xxx.html", where xxx can be random.
# the 5 MB limit is arbitrary, we just want to avoid getting a
# MemoryError exception for /dev/zero etc.
data = util.loadFile(fileName, frame, 5000000)
if data == None:
return None
if len(data) == 0:
wx.MessageBox("File is empty.", "Error", wx.OK, frame)
return None
buf = io.StringIO(data)
try:
z = zipfile.ZipFile(buf)
except:
wx.MessageBox("File is not a valid Celtx script file.", "Error", wx.OK, frame)
return None
files = z.namelist()
scripts = [s for s in files if s.startswith("script") ]
if len(scripts) == 0:
wx.MessageBox("Unable to find script in this Celtx file.", "Error", wx.OK, frame)
return None
f = z.open(scripts[0])
content = f.read()
z.close()
if not content:
wx.MessageBox("Script seems to be empty.", "Error", wx.OK, frame)
return None
elemMap = {
"action" : screenplay.ACTION,
"character" : screenplay.CHARACTER,
"dialog" : screenplay.DIALOGUE,
"parenthetical" : screenplay.PAREN,
"sceneheading" : screenplay.SCENE,
"shot" : screenplay.SHOT,
"transition" : screenplay.TRANSITION,
"act" : screenplay.ACTBREAK,
}
try:
parser = etree.HTMLParser()
root = etree.XML(content, parser)
except etree.XMLSyntaxError as e:
wx.MessageBox("Error parsing file: %s" %e, "Error", wx.OK, frame)
return None
lines = []
def addElem(eleType, lns):
# if elem ends in a newline, last line is empty and useless;
# get rid of it
if not lns[-1] and (len(lns) > 1):
lns = lns[:-1]
for s in lns[:-1]:
lines.append(screenplay.Line(
screenplay.LB_FORCED, eleType, util.cleanInput(s)))
lines.append(screenplay.Line(
screenplay.LB_LAST, eleType, util.cleanInput(lns[-1])))
for para in root.xpath("/html/body/p"):
items = []
for line in para.itertext():
items.append(str(line.replace("\n", " ")))
lt = elemMap.get(para.get("class"), screenplay.ACTION)
if items:
addElem(lt, items)
if len(lines) == 0:
wx.MessageBox("The file contains no importable lines", "Error", wx.OK, frame)
return None
return lines
# like importTextFile, but for Final Draft files.
def importFDX(fileName, frame):
elemMap = {
"Action" : screenplay.ACTION,
"Character" : screenplay.CHARACTER,
"Dialogue" : screenplay.DIALOGUE,
"Parenthetical" : screenplay.PAREN,
"Scene Heading" : screenplay.SCENE,
"Shot" : screenplay.SHOT,
"Transition" : screenplay.TRANSITION,
}
# the 5 MB limit is arbitrary, we just want to avoid getting a
# MemoryError exception for /dev/zero etc.
data = util.loadFile(fileName, frame, 5000000)
if data == None:
return None
if len(data) == 0:
wx.MessageBox("File is empty.", "Error", wx.OK, frame)
return None
try:
root = etree.XML(data.encode("UTF-8"))
lines = []
def addElem(eleType, eleText):
lns = eleText.split("\n")
# if elem ends in a newline, last line is empty and useless;
# get rid of it
if not lns[-1] and (len(lns) > 1):
lns = lns[:-1]
for s in lns[:-1]:
lines.append(screenplay.Line(
screenplay.LB_FORCED, eleType, util.cleanInput(s)))
lines.append(screenplay.Line(
screenplay.LB_LAST, eleType, util.cleanInput(lns[-1])))
for para in root.xpath("Content//Paragraph"):
addedNote = False
et = para.get("Type")
# Check for script notes
s = ""
for notes in para.xpath("ScriptNote/Paragraph/Text"):
if notes.text:
s += notes.text
# FD has AdornmentStyle set to "0" on notes with newline.
if notes.get("AdornmentStyle") == "0":
s += "\n"
if s:
addElem(screenplay.NOTE, s)
addedNote = True
# "General" has embedded Dual Dialogue paragraphs inside it;
# nothing to do for the General element itself.
#
# If no type is defined (like inside scriptnote), skip.
if (et == "General") or (et is None):
continue
s = ""
for text in para.xpath("Text"):
# text.text is None for paragraphs with no text, and +=
# blows up trying to add a string object and None, so
# guard against that
if text.text:
s += text.text
# don't remove paragraphs with no text, unless that paragraph
# contained a scriptnote
if s or not addedNote:
lt = elemMap.get(et, screenplay.ACTION)
addElem(lt, s)
if len(lines) == 0:
wx.MessageBox("The file contains no importable lines", "Error", wx.OK, frame)
return None
return lines
except etree.XMLSyntaxError as e:
wx.MessageBox("Error parsing file: %s" %e, "Error", wx.OK, frame)
return None
# import Fountain files.
# http://fountain.io
def importFountain(fileName, frame):
# regular expressions for fountain markdown.
# https://github.com/vilcans/screenplain/blob/master/screenplain/richstring.py
ire = re.compile(
# one star
r'\*'
# anything but a space, then text
r'([^\s].*?)'
# finishing with one star
r'\*'
# must not be followed by star
r'(?!\*)'
)
bre = re.compile(
# two stars
r'\*\*'
# must not be followed by space
r'(?=\S)'
# inside text
r'(.+?[*_]*)'
# finishing with two stars
r'(?<=\S)\*\*'
)
ure = re.compile(
# underline
r'_'
# must not be followed by space
r'(?=\S)'
# inside text
r'([^_]+)'
# finishing with underline
r'(?<=\S)_'
)
boneyard_re = re.compile('/\\*.*?\\*/', flags=re.DOTALL)
# random magicstring used to escape literal star '\*'
literalstar = "Aq7RR"
# returns s with markdown formatting removed.
def unmarkdown(s):
s = s.replace("\\*", literalstar)
for style in (bre, ire, ure):
s = style.sub(r'\1', s)
return s.replace(literalstar, "*")
data = util.loadFile(fileName, frame, 1000000)
if data == None:
return None
if len(data) == 0:
wx.MessageBox("File is empty.", "Error", wx.OK, frame)
return None
inf = []
inf.append(misc.CheckBoxItem("Import titles as action lines."))
inf.append(misc.CheckBoxItem("Remove unsupported formatting markup."))
inf.append(misc.CheckBoxItem("Import section/synopsis as notes."))
dlg = misc.CheckBoxDlg(frame, "Fountain import options", inf,
"Import options:", False)
if dlg.ShowModal() != wx.ID_OK:
dlg.Destroy()
return None
importTitles = inf[0].selected
removeMarkdown = inf[1].selected
importSectSyn = inf[2].selected
# pre-process data - fix newlines, remove boneyard.
data = util.fixNL(data)
data = boneyard_re.sub('', data)
prelines = data.split("\n")
for i in range(len(prelines)):
try:
util.toLatin1(prelines[i])
except:
prelines[i] = util.cleanInput("" + prelines[i].decode('UTF-8', "ignore"))
lines = []
tabWidth = 4
lns = []
TWOSPACE = " "
skipone = False
# First check if title lines are present:
c = 0
while c < len(prelines):
if prelines[c] != "":
c = c+1
else:
break
# prelines[0:i] are the first bunch of lines, that could be titles.
# Our check for title is simple:
# - the line does not start with 'fade'
# - the first line has a single ':'
if c > 0:
l = util.toInputStr(prelines[0].expandtabs(tabWidth).lstrip().lower())
if not l.startswith("fade") and l.count(":") == 1:
# these are title lines. Now do what the user requested.
if importTitles:
# add TWOSPACE to all the title lines.
for i in range(c):
prelines[i] += TWOSPACE
else:
#remove these lines
prelines = prelines[c+1:]
for l in prelines:
if l != TWOSPACE:
lines.append(util.toInputStr(l.expandtabs(tabWidth)))
else:
lines.append(TWOSPACE)
linesLen = len(lines)
def isPrevEmpty():
if lns and lns[-1].text == "":
return True
return False
def isPrevType(ltype):
return (lns and lns[-1].lt == ltype)
# looks ahead to check if next line is not empty
def isNextEmpty(i):
return (i+1 < len(lines) and lines[i+1] == "")
def getPrevType():
if lns:
return lns[-1].lt
else:
return screenplay.ACTION
def isParen(s):
return (s.startswith('(') and s.endswith(')'))
def isScene(s):
if s.endswith(TWOSPACE):
return False
if s.startswith(".") and not s.startswith(".."):
return True
tmp = s.upper()
if (re.match(r'^(INT|EXT|EST)[ .]', tmp) or
re.match(r'^(INT\.?/EXT\.?)[ .]', tmp) or
re.match(r'^I/E[ .]', tmp)):
return True
return False
def isTransition(s):
return ((s.isupper() and s.endswith("TO:")) or
(s.startswith(">") and not s.endswith("<")))
def isCentered(s):
return s.startswith(">") and s.endswith("<")
def isPageBreak(s):
return s.startswith('===') and s.lstrip('=') == ''
def isNote(s):
return s.startswith("[[") and s.endswith("]]")
def isSection(s):
return s.startswith("#")
def isSynopsis(s):
return s.startswith("=") and not s.startswith("==")
# first pass - identify linetypes
for i in range(linesLen):
if skipone:
skipone = False
continue
s = lines[i]
sl = s.lstrip()
# mark as ACTION by default.
line = screenplay.Line(screenplay.LB_FORCED, screenplay.ACTION, s)
# Start testing lines for element type. Go in order:
# Scene Character, Paren, Dialog, Transition, Note.
if s == "" or isCentered(s) or isPageBreak(s):
# do nothing - import as action.
pass
elif s == TWOSPACE:
line.lt = getPrevType()
elif isScene(s):
line.lt = screenplay.SCENE
if sl.startswith('.'):
line.text = sl[1:]
else:
line.text = sl
elif isTransition(sl) and isPrevEmpty() and isNextEmpty(i):
line.lt = screenplay.TRANSITION
if line.text.startswith('>'):
line.text = sl[1:].lstrip()
elif s.isupper() and isPrevEmpty() and not isNextEmpty(i):
line.lt = screenplay.CHARACTER
if s.endswith(TWOSPACE):
line.lt = screenplay.ACTION
elif isParen(sl) and (isPrevType(screenplay.CHARACTER) or
isPrevType(screenplay.DIALOGUE)):
line.lt = screenplay.PAREN
elif (isPrevType(screenplay.CHARACTER) or
isPrevType(screenplay.DIALOGUE) or
isPrevType(screenplay.PAREN)):
line.lt = screenplay.DIALOGUE
elif isNote(sl):
line.lt = screenplay.NOTE
line.text = sl.strip('[]')
elif isSection(s) or isSynopsis(s):
if not importSectSyn:
if isNextEmpty(i):
skipone = True
continue
line.lt = screenplay.NOTE
line.text = sl.lstrip('=#')
if line.text == TWOSPACE:
pass
elif line.lt != screenplay.ACTION:
line.text = line.text.lstrip()
else:
tmp = line.text.rstrip()
# we don't support center align, so simply add required indent.
if isCentered(tmp):
tmp = tmp[1:-1].strip()
width = frame.panel.ctrl.sp.cfg.getType(screenplay.ACTION).width
if len(tmp) < width:
tmp = ' ' * ((width - len(tmp)) // 2) + tmp
line.text = tmp
if removeMarkdown:
line.text = unmarkdown(line.text)
if line.lt == screenplay.CHARACTER and line.text.endswith('^'):
line.text = line.text[:-1]
lns.append(line)
ret = []
# second pass helper functions.
def isLastLBForced():
return ret and ret[-1].lb == screenplay.LB_FORCED
def makeLastLBLast():
if ret:
ret[-1].lb = screenplay.LB_LAST
def isRetPrevType(t):
return ret and ret[-1].lt == t
# second pass - remove unneeded empty lines, and fix the linebreaks.
for ln in lns:
if ln.text == '':
if isLastLBForced():
makeLastLBLast()
else:
ret.append(ln)
elif not isRetPrevType(ln.lt):
makeLastLBLast()
ret.append(ln)
else:
ret.append(ln)
makeLastLBLast()
return ret
# import text file from fileName, return list of Line objects for the
# screenplay or None if something went wrong. returned list always
# contains at least one line.
def importTextFile(fileName, frame):
# the 1 MB limit is arbitrary, we just want to avoid getting a
# MemoryError exception for /dev/zero etc.
data = util.loadFile(fileName, frame, 1000000)
if data == None:
return None
if len(data) == 0:
wx.MessageBox("File is empty.", "Error", wx.OK, frame)
return None
data = util.fixNL(data)
lines = data.split("\n")
tabWidth = 4
# key = indent level, value = Indent
indDict = {}
for i in range(len(lines)):
s = util.toInputStr(lines[i].rstrip().expandtabs(tabWidth))
# don't count empty lines towards indentation statistics
if s.strip() == "":
lines[i] = ""
continue
cnt = util.countInitial(s, " ")
ind = indDict.get(cnt)
if not ind:
ind = Indent(cnt)
indDict[cnt] = ind
tmp = s.upper()
if util.multiFind(tmp, ["EXT.", "INT."]):
ind.sceneStart += 1
if util.multiFind(tmp, ["CUT TO:", "DISSOLVE TO:"]):
ind.trans += 1
if re.match(r"^ +\(.*\)$", tmp):
ind.paren += 1
ind.lines.append(s.lstrip())
lines[i] = s
if len(indDict) == 0:
wx.MessageBox("File contains only empty lines.", "Error", wx.OK, frame)
return None
# scene/action indent
setType(SCENE_ACTION, indDict, lambda v: v.sceneStart)
# indent with most lines is dialogue in non-pure-action scripts
setType(screenplay.DIALOGUE, indDict, lambda v: len(v.lines))
# remaining indent with lines is character most likely
setType(screenplay.CHARACTER, indDict, lambda v: len(v.lines))
# transitions
setType(screenplay.TRANSITION, indDict, lambda v: v.trans)
# parentheticals
setType(screenplay.PAREN, indDict, lambda v: v.paren)
# some text files have this type of parens:
#
# JOE
# (smiling and
# hopping along)
#
# this handles them.
parenIndent = findIndent(indDict, lambda v: v.lt == screenplay.PAREN)
if parenIndent != -1:
paren2Indent = findIndent(indDict,
lambda v, var: (v.lt == -1) and (v.indent == var),
parenIndent + 1)
if paren2Indent != -1:
indDict[paren2Indent].lt = screenplay.PAREN
# set line type to ACTION for any indents not recognized
for v in indDict.values():
if v.lt == -1:
v.lt = screenplay.ACTION
dlg = ImportDlg(frame, list(indDict.values()))
if dlg.ShowModal() != wx.ID_OK:
dlg.Destroy()
return None
dlg.Destroy()
ret = []
for i in range(len(lines)):
s = lines[i]
cnt = util.countInitial(s, " ")
s = s.lstrip()
sUp = s.upper()
if s:
lt = indDict[cnt].lt
if lt == IGNORE:
continue
if lt == SCENE_ACTION:
if s.startswith("EXT.") or s.startswith("INT."):
lt = screenplay.SCENE
else:
lt = screenplay.ACTION
if ret and (ret[-1].lt != lt):
ret[-1].lb = screenplay.LB_LAST
if lt == screenplay.CHARACTER:
if sUp.endswith("(CONT'D)"):
s = sUp[:-8].rstrip()
elif lt == screenplay.PAREN:
if s == "(continuing)":
s = ""
if s:
line = screenplay.Line(screenplay.LB_SPACE, lt, s)
ret.append(line)
elif ret:
ret[-1].lb = screenplay.LB_LAST
if len(ret) == 0:
ret.append(screenplay.Line(screenplay.LB_LAST, screenplay.ACTION))
# make sure the last line ends an element
ret[-1].lb = screenplay.LB_LAST
return ret
# go through indents, find the one with maximum value in something, and
# set its linetype to given lt.
def setType(lt, indDict, func):
maxCount = 0
found = -1
for v in indDict.values():
# don't touch indents already set
if v.lt != -1:
continue
val = func(v)
if val > maxCount:
maxCount = val
found = v.indent
if found != -1:
indDict[found].lt = lt
# go through indents calling func(it, *vars) on each. return indent count
# for the indent func returns True, or -1 if it returns False for each.
def findIndent(indDict, func, *vars):
for v in indDict.values():
if func(v, *vars):
return v.indent
return -1
# information about one indent level in imported text files.
class Indent:
def __init__(self, indent):
# indent level, i.e. spaces at the beginning
self.indent = indent
# lines with this indent, leading spaces removed
self.lines = []
# assigned line type, or -1 if not assigned yet.
self.lt = -1
# how many of the lines start with "EXT." or "INT."
self.sceneStart = 0
# how many of the lines have "CUT TO:" or "DISSOLVE TO:"
self.trans = 0
# how many of the lines have a form of "^ +\(.*)$", i.e. are most
# likely parentheticals
self.paren = 0
class ImportDlg(wx.Dialog):
def __init__(self, parent, indents):
wx.Dialog.__init__(self, parent, -1, "Adjust styles",
style = wx.DEFAULT_DIALOG_STYLE)
indents.sort(key=lambda indent: indent.lines)
vsizer = wx.BoxSizer(wx.VERTICAL)
tmp = wx.StaticText(self, -1, "Input:")
vsizer.Add(tmp)
self.inputLb = wx.ListBox(self, -1, size = (400, 200))
for it in indents:
self.inputLb.Append("%d lines (indented %d characters)" %
(len(it.lines), it.indent), it)
vsizer.Add(self.inputLb, 0, wx.EXPAND)
hsizer = wx.BoxSizer(wx.HORIZONTAL)
hsizer.Add(wx.StaticText(self, -1, "Style:"), 0,
wx.ALIGN_CENTER_VERTICAL)
self.styleCombo = wx.ComboBox(self, -1, style = wx.CB_READONLY)
self.styleCombo.Append("Scene / Action", SCENE_ACTION)
for t in config.getTIs():
self.styleCombo.Append(t.name, t.lt)
self.styleCombo.Append("Ignore", IGNORE)
util.setWH(self.styleCombo, w = 150)
hsizer.Add(self.styleCombo, 0, wx.LEFT, 10)
vsizer.Add(hsizer, 0, wx.TOP | wx.BOTTOM, 10)
vsizer.Add(wx.StaticText(self, -1, "Lines:"))
self.linesEntry = wx.TextCtrl(self, -1, size = (400, 200),
style = wx.TE_MULTILINE | wx.TE_DONTWRAP)
vsizer.Add(self.linesEntry, 0, wx.EXPAND)
hsizer = wx.BoxSizer(wx.HORIZONTAL)
hsizer.Add((1, 1), 1)
cancelBtn = gutil.createStockButton(self, "Cancel")
hsizer.Add(cancelBtn)
okBtn = gutil.createStockButton(self, "OK")
hsizer.Add(okBtn, 0, wx.LEFT, 10)
vsizer.Add(hsizer, 0, wx.EXPAND | wx.TOP, 10)
util.finishWindow(self, vsizer)
self.Bind(wx.EVT_COMBOBOX, self.OnStyleCombo, id=self.styleCombo.GetId())
self.Bind(wx.EVT_LISTBOX, self.OnInputLb, id=self.inputLb.GetId())
self.Bind(wx.EVT_BUTTON, self.OnCancel, id=cancelBtn.GetId())
self.Bind(wx.EVT_BUTTON, self.OnOK, id=okBtn.GetId())
self.inputLb.SetSelection(0)
self.OnInputLb()
def OnOK(self, event):
self.EndModal(wx.ID_OK)
def OnCancel(self, event):
self.EndModal(wx.ID_CANCEL)
def OnInputLb(self, event = None):
self.selected = self.inputLb.GetClientData(self.inputLb.GetSelection())
util.reverseComboSelect(self.styleCombo, self.selected.lt)
self.linesEntry.SetValue("\n".join(self.selected.lines))
def OnStyleCombo(self, event):
self.selected.lt = self.styleCombo.GetClientData(
self.styleCombo.GetSelection())

17
src/mypager.py Normal file
View File

@ -0,0 +1,17 @@
import screenplay
import pml
# used to iteratively add PML pages to a document
class Pager:
def __init__(self, cfg):
self.doc = pml.Document(cfg.paperWidth, cfg.paperHeight)
# used in several places, so keep around
self.charIndent = cfg.getType(screenplay.CHARACTER).indent
self.sceneIndent = cfg.getType(screenplay.SCENE).indent
# current scene number
self.scene = 0
# number of CONTINUED:'s lines added for current scene
self.sceneContNr = 0

268
src/mypickle.py Normal file
View File

@ -0,0 +1,268 @@
import config
import util
import copy
# keep track about one object's variables
class Vars:
def __init__(self):
self.cvars = []
def __iter__(self):
for v in self.cvars:
yield v
# make various dictionaries pointing to the config variables.
def makeDicts(self):
self.all = self.getDict()
self.color = self.getDict(ColorVar)
self.numeric = self.getDict(NumericVar)
self.stringLatin1 = self.getDict(StrLatin1Var)
# return dictionary containing given type of variable objects, or all
# if typeObj is None.
def getDict(self, typeObj = None):
tmp = {}
for it in self.cvars:
if not typeObj or isinstance(it, typeObj):
tmp[it.name] = it
return tmp
# get default value of a setting
def getDefault(self, name):
return self.all[name].defVal
# get minimum value of a numeric setting
def getMin(self, name):
return self.numeric[name].minVal
# get maximum value of a numeric setting
def getMax(self, name):
return self.numeric[name].maxVal
# get minimum and maximum value of a numeric setting as a (min,max)
# tuple.
def getMinMax(self, name):
return (self.getMin(name), self.getMax(name))
def setDefaults(self, obj):
for it in self.cvars:
setattr(obj, it.name, copy.deepcopy(it.defVal))
# transform string 's' (loaded from file) into a form suitable for
# load() to take.
@staticmethod
def makeVals(s):
tmp = util.fixNL(str(s)).split("\n")
vals = {}
for it in tmp:
if it.find(":") != -1:
name, v = it.split(":", 1)
vals[name] = v
return vals
def save(self, prefix, obj):
s = ""
for it in self.cvars:
if it.name2:
s += it.toStr(getattr(obj, it.name), prefix + it.name2)
return s
def load(self, vals, prefix, obj):
for it in self.cvars:
if it.name2:
name = prefix + it.name2
if name in vals:
res = it.fromStr(vals, vals[name], name)
setattr(obj, it.name, res)
del vals[name]
def addVar(self, var):
self.cvars.append(var)
def addBool(self, *params):
self.addVar(BoolVar(*params))
def addColor(self, name, r, g, b, name2, descr):
self.addVar(ColorVar(name + "Color", util.MyColor(r, g, b),
"Color/" + name2, descr))
def addFloat(self, *params):
self.addVar(FloatVar(*params))
def addInt(self, *params):
self.addVar(IntVar(*params))
def addStrLatin1(self, *params):
self.addVar(StrLatin1Var(*params))
def addStrUnicode(self, *params):
self.addVar(StrUnicodeVar(*params))
def addStrBinary(self, *params):
self.addVar(StrBinaryVar(*params))
def addElemName(self, *params):
self.addVar(ElementNameVar(*params))
def addList(self, *params):
self.addVar(ListVar(*params))
class ConfVar:
# name2 is the name to use while saving/loading the variable. if it's
# empty, the variable is not loaded/saved, i.e. is used only
# internally.
def __init__(self, name, defVal, name2):
self.name = name
self.defVal = defVal
self.name2 = name2
class BoolVar(ConfVar):
def __init__(self, name, defVal, name2):
ConfVar.__init__(self, name, defVal, name2)
def toStr(self, val, prefix):
return "%s:%s\n" % (prefix, str(bool(val)))
def fromStr(self, vals, val, prefix):
return val == "True"
class ColorVar(ConfVar):
def __init__(self, name, defVal, name2, descr):
ConfVar.__init__(self, name, defVal, name2)
self.descr = descr
def toStr(self, val, prefix):
return "%s:%d,%d,%d\n" % (prefix, val.r, val.g, val.b)
def fromStr(self, vals, val, prefix):
v = val.split(",")
if len(v) != 3:
return copy.deepcopy(self.defVal)
r = util.str2int(v[0], 0, 0, 255)
g = util.str2int(v[1], 0, 0, 255)
b = util.str2int(v[2], 0, 0, 255)
return util.MyColor(r, g, b)
class NumericVar(ConfVar):
def __init__(self, name, defVal, name2, minVal, maxVal):
ConfVar.__init__(self, name, defVal, name2)
self.minVal = minVal
self.maxVal = maxVal
class FloatVar(NumericVar):
def __init__(self, name, defVal, name2, minVal, maxVal, precision = 2):
NumericVar.__init__(self, name, defVal, name2, minVal, maxVal)
self.precision = precision
def toStr(self, val, prefix):
return "%s:%.*f\n" % (prefix, self.precision, val)
def fromStr(self, vals, val, prefix):
return util.str2float(val, self.defVal, self.minVal, self.maxVal)
class IntVar(NumericVar):
def __init__(self, name, defVal, name2, minVal, maxVal):
NumericVar.__init__(self, name, defVal, name2, minVal, maxVal)
def toStr(self, val, prefix):
return "%s:%d\n" % (prefix, val)
def fromStr(self, vals, val, prefix):
return util.str2int(val, self.defVal, self.minVal, self.maxVal)
# ISO-8859-1 (Latin 1) string.
class StrLatin1Var(ConfVar):
def __init__(self, name, defVal, name2):
ConfVar.__init__(self, name, defVal, name2)
def toStr(self, val, prefix):
return "%s:%s\n" % (prefix, util.toUTF8(val))
def fromStr(self, vals, val, prefix):
return util.fromUTF8(val)
# Unicode string.
class StrUnicodeVar(ConfVar):
def __init__(self, name, defVal, name2):
ConfVar.__init__(self, name, defVal, name2)
def toStr(self, val, prefix):
return "%s:%s\n" % (prefix, str(val))
def fromStr(self, vals, val, prefix):
return val
# binary string, can contain anything. characters outside of printable
# ASCII (and \ itself) are encoded as \XX, where XX is the hex code of the
# character.
class StrBinaryVar(ConfVar):
def __init__(self, name, defVal, name2):
ConfVar.__init__(self, name, defVal, name2)
def toStr(self, val, prefix):
return "%s:%s\n" % (prefix, util.encodeStr(val))
def fromStr(self, vals, val, prefix):
return util.decodeStr(val)
# screenplay.ACTION <-> "Action"
class ElementNameVar(ConfVar):
def __init__(self, name, defVal, name2):
ConfVar.__init__(self, name, defVal, name2)
def toStr(self, val, prefix):
return "%s:%s\n" % (prefix, config.lt2ti(val).name)
def fromStr(self, vals, val, prefix):
ti = config.name2ti(val)
if ti:
return ti.lt
else:
return self.defVal
class ListVar(ConfVar):
def __init__(self, name, defVal, name2, itemType):
ConfVar.__init__(self, name, defVal, name2)
# itemType is an instance of one of the *Var classes, and is the
# type of item contained in the list.
self.itemType = itemType
def toStr(self, val, prefix):
s = ""
s += "%s:%d\n" % (prefix, len(val))
i = 1
for v in val:
s += self.itemType.toStr(v, prefix + "/%d" % i)
i += 1
return s
def fromStr(self, vals, val, prefix):
# 1000 is totally arbitrary, increase if needed
count = util.str2int(val, -1, -1, 1000)
if count == -1:
return copy.deepcopy(self.defVal)
tmp = []
for i in range(1, count + 1):
name = prefix + "/%d" % i
if name in vals:
res = self.itemType.fromStr(vals, vals[name], name)
tmp.append(res)
del vals[name]
return tmp

53
src/namearray.py Normal file
View File

@ -0,0 +1,53 @@
import array
import collections
class NameArray:
def __init__(self):
self.maxCount = 205000
self.count = 0
self.name = [None] * self.maxCount
self.type = array.array('B')
self.type.frombytes(str.encode(chr(0) * self.maxCount))
# 0 = female, 1 = male
self.sex = array.array('B')
self.sex.frombytes(str.encode(chr(0) * self.maxCount))
# key = type name, value = count of names for that type
self.typeNamesCnt = collections.defaultdict(int)
# key = type name, value = integer id for that type
self.typeId = {}
# type names indexed by their integer id
self.typeNamesById = []
def append(self, name, type, sex):
if self.count >= self.maxCount:
for i in range(1000):
self.name.append(None)
self.type.append(0)
self.sex.append(0)
self.maxCount += 1000
typeId = self.addType(type)
self.name[self.count] = name
self.type[self.count] = typeId
self.sex[self.count] = 0 if sex == "F" else 1
self.count += 1
def addType(self, type):
self.typeNamesCnt[type] += 1
typeId = self.typeId.get(type)
if typeId is None:
typeId = len(self.typeNamesById)
self.typeId[type] = typeId
self.typeNamesById.append(type)
return typeId

275
src/namesdlg.py Normal file
View File

@ -0,0 +1,275 @@
import misc
import namearray
import util
import wx
# NameArray, or None if not loaded
nameArr = None
# if not already loaded, read the name database from disk and store it.
# returns False on errors.
def readNames(frame):
global nameArr
if nameArr:
# already loaded
return True
try:
data = util.loadMaybeCompressedFile("names.txt", frame)
if not data:
return False
res = namearray.NameArray()
nameType = None
for line in data.splitlines():
ch = line[0]
if ch == "#":
continue
elif ch == "N":
nameType = line[1:]
elif ch in ("M", "F"):
if not nameType:
raise Exception("No name type set before line: '%s'" % line)
res.append(line[1:], nameType, ch)
else:
raise Exception("Unknown linetype for line: '%s'" % line)
nameArr = res
return True
except Exception as e:
wx.MessageBox("Error loading name database: %s" % str(e),
"Error", wx.OK, frame)
return False
class NamesDlg(wx.Dialog):
def __init__(self, parent, ctrl):
wx.Dialog.__init__(self, parent, -1, "Character name database",
style = wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
self.ctrl = ctrl
hsizer = wx.BoxSizer(wx.HORIZONTAL)
vsizer = wx.BoxSizer(wx.VERTICAL)
vsizer.Add(wx.StaticText(self, -1, "Search in:"))
self.typeList = wx.ListCtrl(self, -1,
style = wx.LC_REPORT | wx.LC_HRULES | wx.LC_VRULES)
self.typeList.InsertColumn(0, "Count")
self.typeList.InsertColumn(1, "Type")
for i in range(len(nameArr.typeNamesById)):
typeName = nameArr.typeNamesById[i]
self.typeList.InsertItem(i, str(nameArr.typeNamesCnt[typeName]))
self.typeList.SetItem(i, 1, typeName)
self.typeList.SetItemData(i, i)
self.typeList.SetColumnWidth(0, wx.LIST_AUTOSIZE)
self.typeList.SetColumnWidth(1, wx.LIST_AUTOSIZE)
w = 0
w += self.typeList.GetColumnWidth(0)
w += self.typeList.GetColumnWidth(1)
util.setWH(self.typeList, w + 15, 425)
self.typeList.SortItems(self.CmpFreq)
self.selectAllTypes()
vsizer.Add(self.typeList, 1, wx.EXPAND | wx.BOTTOM, 5)
selectAllBtn = wx.Button(self, -1, "Select all")
vsizer.Add(selectAllBtn)
hsizer.Add(vsizer, 0, wx.EXPAND)
vsizer = wx.BoxSizer(wx.VERTICAL)
hsizer2 = wx.BoxSizer(wx.HORIZONTAL)
vsizer2 = wx.BoxSizer(wx.VERTICAL)
searchBtn = wx.Button(self, -1, "Search")
self.Bind(wx.EVT_BUTTON, self.OnSearch, id=searchBtn.GetId())
vsizer2.Add(searchBtn, 0, wx.BOTTOM | wx.TOP, 10)
self.searchEntry = wx.TextCtrl(self, -1, style = wx.TE_PROCESS_ENTER)
vsizer2.Add(self.searchEntry, 0, wx.EXPAND)
tmp = wx.Button(self, -1, "Insert")
self.Bind(wx.EVT_BUTTON, self.OnInsertName, id=tmp.GetId())
vsizer2.Add(tmp, 0, wx.BOTTOM | wx.TOP, 10)
hsizer2.Add(vsizer2, 1, wx.RIGHT, 10)
self.nameRb = wx.RadioBox(self, -1, "Name",
style = wx.RA_SPECIFY_COLS, majorDimension = 1,
choices = [ "begins with", "contains", "ends in" ])
hsizer2.Add(self.nameRb)
self.sexRb = wx.RadioBox(self, -1, "Sex",
style = wx.RA_SPECIFY_COLS, majorDimension = 1,
choices = [ "Male", "Female", "Both" ])
self.sexRb.SetSelection(2)
hsizer2.Add(self.sexRb, 0, wx.LEFT, 5)
vsizer.Add(hsizer2, 0, wx.EXPAND | wx.ALIGN_CENTER)
vsizer.Add(wx.StaticText(self, -1, "Results:"))
self.list = MyListCtrl(self)
vsizer.Add(self.list, 1, wx.EXPAND | wx.BOTTOM, 5)
self.foundLabel = wx.StaticText(self, -1, "",
style = wx.ALIGN_CENTRE | wx.ST_NO_AUTORESIZE)
vsizer.Add(self.foundLabel, 0, wx.EXPAND)
hsizer.Add(vsizer, 20, wx.EXPAND | wx.LEFT, 10)
self.Bind(wx.EVT_TEXT_ENTER, self.OnSearch, id=self.searchEntry.GetId())
self.Bind(wx.EVT_BUTTON, self.selectAllTypes, id=selectAllBtn.GetId())
self.Bind(wx.EVT_LIST_COL_CLICK, self.OnHeaderClick, id=self.typeList.GetId())
util.finishWindow(self, hsizer)
self.OnSearch()
self.searchEntry.SetFocus()
def selectAllTypes(self, event = None):
for i in range(len(nameArr.typeNamesById)):
self.typeList.SetItemState(i, wx.LIST_STATE_SELECTED,
wx.LIST_STATE_SELECTED)
def OnHeaderClick(self, event):
if event.GetColumn() == 0:
self.typeList.SortItems(self.CmpFreq)
else:
self.typeList.SortItems(self.CmpType)
def CmpFreq(self, i1, i2):
return nameArr.typeNamesCnt[nameArr.typeNamesById[i2]] - nameArr.typeNamesCnt[nameArr.typeNamesById[i1]]
def cmpfunc(a, b):
return (a > b) - (a < b)
def CmpType(self, i1, i2):
return util.cmpfunc(nameArr.typeNamesById[i1], nameArr.typeNamesById[i2])
def OnInsertName(self, event):
item = self.list.GetNextItem(-1, wx.LIST_NEXT_ALL,
wx.LIST_STATE_SELECTED)
if item == -1:
return
# this seems to return column 0's text, which is lucky, because I
# don't see a way of getting other columns' texts...
name = self.list.GetItemText(item)
for ch in name:
self.ctrl.OnKeyChar(util.MyKeyEvent(ord(ch)))
def OnSearch(self, event = None):
l = []
wx.BeginBusyCursor()
s = str(util.lower(misc.fromGUI(self.searchEntry.GetValue())))
sex = self.sexRb.GetSelection()
nt = self.nameRb.GetSelection()
selTypes = {}
item = -1
while 1:
item = self.typeList.GetNextItem(item, wx.LIST_NEXT_ALL,
wx.LIST_STATE_SELECTED)
if item == -1:
break
selTypes[self.typeList.GetItemData(item)] = True
if len(selTypes) == len(nameArr.typeNamesCnt):
doTypes = False
else:
doTypes = True
for i in range(nameArr.count):
if (sex != 2) and (sex == nameArr.sex[i]):
continue
if doTypes and nameArr.type[i] not in selTypes:
continue
if s:
name = util.lower(nameArr.name[i])
if nt == 0:
if not name.startswith(s):
continue
elif nt == 1:
if name.find(s) == -1:
continue
elif nt == 2:
if not name.endswith(s):
continue
l.append(i)
self.list.items = l
self.list.SetItemCount(len(l))
if self.list.GetItemCount() > 0:
self.list.EnsureVisible(0)
wx.EndBusyCursor()
self.foundLabel.SetLabel("%d names found." % len(l))
class MyListCtrl(wx.ListCtrl):
def __init__(self, parent):
wx.ListCtrl.__init__(self, parent, -1,
style = wx.LC_REPORT | wx.LC_VIRTUAL | wx.LC_SINGLE_SEL |
wx.LC_HRULES | wx.LC_VRULES)
self.sex = ["Female", "Male"]
self.InsertColumn(0, "Name")
self.InsertColumn(1, "Type")
self.InsertColumn(2, "Sex")
self.SetColumnWidth(0, 120)
self.SetColumnWidth(1, 120)
# we can't use wx.LIST_AUTOSIZE since this is a virtual control,
# so calculate the size ourselves since we know the longest string
# possible.
w = util.getTextExtent(self.GetFont(), "Female")[0] + 15
self.SetColumnWidth(2, w)
util.setWH(self, w = 120*2 + w + 25)
def OnGetItemText(self, item, col):
n = self.items[item]
if col == 0:
return nameArr.name[n]
elif col == 1:
return nameArr.typeNamesById[nameArr.type[n]]
elif col == 2:
return self.sex[nameArr.sex[n]]
# shouldn't happen
return ""
# for some reason this must be overridden as well, otherwise we get
# assert failures under windows.
def OnGetItemImage(self, item):
return -1

32
src/opts.py Normal file
View File

@ -0,0 +1,32 @@
import sys
# TODO: Python, at least up to 2.4, does not support Unicode command line
# arguments on Windows. Since UNIXes use UTF-8, just assume all command
# line arguments are UTF-8 for now, and silently ignore any coding errors
# that may result on Windows in some cases.
def init():
global isTest, conf, filenames
# script filenames to load
filenames = []
# name of config file to use, or None
conf = None
# are we in test mode
isTest = False
i = 1
while i < len(sys.argv):
arg = str(sys.argv[i])
if arg == "--test":
isTest = True
elif arg == "--conf":
if (i + 1) < len(sys.argv):
conf = str(sys.argv[i + 1], "UTF-8", "ignore")
i += 1
else:
filenames.append(arg)
i += 1

543
src/pdf.py Normal file
View File

@ -0,0 +1,543 @@
import fontinfo
import pml
import util
# PDF transform matrixes where key is the angle from x-axis
# in counter-clockwise direction.
TRANSFORM_MATRIX = {
45 : (1, 1, -1, 1),
90 : (0, 1, -1, 0),
}
# users should only use this.
def generate(doc):
tmp = PDFExporter(doc)
return tmp.generate()
# An abstract base class for all PDF drawing operations.
class PDFDrawOp:
# write PDF drawing operations corresponding to the PML object pmlOp
# to output (util.String). pe = PDFExporter.
def draw(self, pmlOp, pageNr, output, pe):
raise Exception("draw not implemented")
class PDFTextOp(PDFDrawOp):
def draw(self, pmlOp, pageNr, output, pe):
if pmlOp.toc:
pmlOp.toc.pageObjNr = pe.pageObjs[pageNr].nr
# we need to adjust y position since PDF uses baseline of text as
# the y pos, but pml uses top of the text as y pos. The Adobe
# standard Courier family font metrics give 157 units in 1/1000
# point units as the Descender value, thus giving (1000 - 157) =
# 843 units from baseline to top of text.
# http://partners.adobe.com/asn/tech/type/ftechnotes.jsp contains
# the "Font Metrics for PDF Core 14 Fonts" document.
x = pe.x(pmlOp.x)
y = pe.y(pmlOp.y) - 0.843 * pmlOp.size
newFont = "F%d %d" % (pe.getFontNr(pmlOp.flags), pmlOp.size)
if newFont != pe.currentFont:
output += "/%s Tf\n" % newFont
pe.currentFont = newFont
if pmlOp.angle is not None:
matrix = TRANSFORM_MATRIX.get(pmlOp.angle)
if matrix:
output += "BT\n"\
"%f %f %f %f %f %f Tm\n"\
"(%s) Tj\n"\
"ET\n" % (matrix[0], matrix[1], matrix[2], matrix[3],
x, y, pe.escapeStr(pmlOp.text))
else:
# unsupported angle, don't print it.
pass
else:
output += "BT\n"\
"%f %f Td\n"\
"(%s) Tj\n"\
"ET\n" % (x, y, pe.escapeStr(pmlOp.text))
if pmlOp.flags & pml.UNDERLINED:
undLen = fontinfo.getMetrics(pmlOp.flags).getTextWidth(
pmlOp.text, pmlOp.size)
# all standard PDF fonts have the underline line 100 units
# below baseline with a thickness of 50
undY = y - 0.1 * pmlOp.size
output += "%f w\n"\
"%f %f m\n"\
"%f %f l\n"\
"S\n" % (0.05 * pmlOp.size, x, undY, x + undLen, undY)
class PDFLineOp(PDFDrawOp):
def draw(self, pmlOp, pageNr, output, pe):
p = pmlOp.points
pc = len(p)
if pc < 2:
print("LineOp contains only %d points" % pc)
return
output += "%f w\n"\
"%s m\n" % (pe.mm2points(pmlOp.width), pe.xy(p[0]))
for i in range(1, pc):
output += "%s l\n" % (pe.xy(p[i]))
if pmlOp.isClosed:
output += "s\n"
else:
output += "S\n"
class PDFRectOp(PDFDrawOp):
def draw(self, pmlOp, pageNr, output, pe):
if pmlOp.lw != -1:
output += "%f w\n" % pe.mm2points(pmlOp.lw)
output += "%f %f %f %f re\n" % (
pe.x(pmlOp.x),
pe.y(pmlOp.y) - pe.mm2points(pmlOp.height),
pe.mm2points(pmlOp.width), pe.mm2points(pmlOp.height))
if pmlOp.fillType == pml.NO_FILL:
output += "S\n"
elif pmlOp.fillType == pml.FILL:
output += "f\n"
elif pmlOp.fillType == pml.STROKE_FILL:
output += "B\n"
else:
print("Invalid fill type for RectOp")
class PDFQuarterCircleOp(PDFDrawOp):
def draw(self, pmlOp, pageNr, output, pe):
sX = pmlOp.flipX and -1 or 1
sY = pmlOp.flipY and -1 or 1
# The traditional constant is: 0.552284749
# however, as described here:
# http://spencermortensen.com/articles/bezier-circle/,
# this has a maximum radial drift of 0.027253%.
# The constant calculated by Spencer Mortensen
# has a max. drift of 0.019608% which is 28% better.
A = pmlOp.radius * 0.551915024494
output += "%f w\n"\
"%s m\n" % (pe.mm2points(pmlOp.width),
pe.xy((pmlOp.x - pmlOp.radius * sX, pmlOp.y)))
output += "%f %f %f %f %f %f c\n" % (
pe.x(pmlOp.x - pmlOp.radius * sX), pe.y(pmlOp.y - A * sY),
pe.x(pmlOp.x - A * sX), pe.y(pmlOp.y - pmlOp.radius * sY),
pe.x(pmlOp.x), pe.y(pmlOp.y - pmlOp.radius * sY))
output += "S\n"
class PDFArbitraryOp(PDFDrawOp):
def draw(self, pmlOp, pageNr, output, pe):
output += "%s\n" % pmlOp.cmds
# used for keeping track of used fonts
class FontInfo:
def __init__(self, name):
self.name = name
# font number (the name in the /F PDF command), or -1 if not used
self.number = -1
# PDFObject that contains the /Font object for this font, or None
self.pdfObj = None
# one object in a PDF file
class PDFObject:
def __init__(self, nr, data = ""):
# PDF object number
self.nr = nr
# all data between 'obj/endobj' tags, excluding newlines
self.data = data
# start position of object, stored in the xref table. initialized
# when the object is written out (by the caller of write).
self.xrefPos = -1
# write object to output (util.String).
def write(self, output):
output += "%d 0 obj\n" % self.nr
output += self.data
output += "\nendobj\n"
class PDFExporter:
# see genWidths
_widthsStr = None
def __init__(self, doc):
# pml.Document
self.doc = doc
# generate PDF document and return it as a string
def generate(self):
#lsdjflksj = util.TimerDev("generate")
doc = self.doc
# fast lookup of font information
self.fonts = {
pml.COURIER : FontInfo("Courier"),
pml.COURIER | pml.BOLD: FontInfo("Courier-Bold"),
pml.COURIER | pml.ITALIC: FontInfo("Courier-Oblique"),
pml.COURIER | pml.BOLD | pml.ITALIC:
FontInfo("Courier-BoldOblique"),
pml.HELVETICA : FontInfo("Helvetica"),
pml.HELVETICA | pml.BOLD: FontInfo("Helvetica-Bold"),
pml.HELVETICA | pml.ITALIC: FontInfo("Helvetica-Oblique"),
pml.HELVETICA | pml.BOLD | pml.ITALIC:
FontInfo("Helvetica-BoldOblique"),
pml.TIMES_ROMAN : FontInfo("Times-Roman"),
pml.TIMES_ROMAN | pml.BOLD: FontInfo("Times-Bold"),
pml.TIMES_ROMAN | pml.ITALIC: FontInfo("Times-Italic"),
pml.TIMES_ROMAN | pml.BOLD | pml.ITALIC:
FontInfo("Times-BoldItalic"),
}
# list of PDFObjects
self.objects = []
# number of fonts used
self.fontCnt = 0
# PDF object count. it starts at 1 because the 'f' thingy in the
# xref table is an object of some kind or something...
self.objectCnt = 1
pages = len(doc.pages)
self.catalogObj = self.addObj()
self.infoObj = self.createInfoObj()
pagesObj = self.addObj()
# we only create this when needed, in genWidths
self.widthsObj = None
if doc.tocs:
self.outlinesObj = self.addObj()
# each outline is a single PDF object
self.outLineObjs = []
for i in range(len(doc.tocs)):
self.outLineObjs.append(self.addObj())
self.outlinesObj.data = ("<< /Type /Outlines\n"
"/Count %d\n"
"/First %d 0 R\n"
"/Last %d 0 R\n"
">>" % (len(doc.tocs),
self.outLineObjs[0].nr,
self.outLineObjs[-1].nr))
outlinesStr = "/Outlines %d 0 R\n" % self.outlinesObj.nr
if doc.showTOC:
outlinesStr += "/PageMode /UseOutlines\n"
else:
outlinesStr = ""
# each page has two PDF objects: 1) a /Page object that links to
# 2) a stream object that has the actual page contents.
self.pageObjs = []
self.pageContentObjs = []
for i in range(pages):
self.pageObjs.append(self.addObj("<< /Type /Page\n"
"/Parent %d 0 R\n"
"/Contents %d 0 R\n"
">>" % (pagesObj.nr,
self.objectCnt + 1)))
self.pageContentObjs.append(self.addObj())
if doc.defPage != -1:
outlinesStr += "/OpenAction [%d 0 R /XYZ null null 0]\n" % (
self.pageObjs[0].nr + doc.defPage * 2)
self.catalogObj.data = ("<< /Type /Catalog\n"
"/Pages %d 0 R\n"
"%s"
">>" % (pagesObj.nr, outlinesStr))
for i in range(pages):
self.genPage(i)
kids = util.String()
kids += "["
for obj in self.pageObjs:
kids += "%d 0 R\n" % obj.nr
kids += "]"
fontStr = ""
for fi in self.fonts.values():
if fi.number != -1:
fontStr += "/F%d %d 0 R " % (fi.number, fi.pdfObj.nr)
pagesObj.data = ("<< /Type /Pages\n"
"/Kids %s\n"
"/Count %d\n"
"/MediaBox [0 0 %f %f]\n"
"/Resources << /Font <<\n"
"%s >> >>\n"
">>" % (str(kids), pages, self.mm2points(doc.w),
self.mm2points(doc.h), fontStr))
if doc.tocs:
for i in range(len(doc.tocs)):
self.genOutline(i)
return self.genPDF()
def createInfoObj(self):
version = self.escapeStr(self.doc.version)
if self.doc.uniqueId:
extra = "/Keywords (%s)\n" % self.doc.uniqueId
else:
extra = ""
return self.addObj("<< /Creator (Trelby %s)\n"
"/Producer (Trelby %s)\n"
"%s"
">>" % (version, version, extra))
# create a PDF object containing a 256-entry array for the widths of a
# font, with all widths being 600
def genWidths(self):
if self.widthsObj:
return
if not self.__class__._widthsStr:
self.__class__._widthsStr = "[%s]" % ("600 " * 256).rstrip()
self.widthsObj = self.addObj(self.__class__._widthsStr)
# generate a single page
def genPage(self, pageNr):
pg = self.doc.pages[pageNr]
# content stream
cont = util.String()
self.currentFont = ""
for op in pg.ops:
op.pdfOp.draw(op, pageNr, cont, self)
self.pageContentObjs[pageNr].data = self.genStream(str(cont))
# generate outline number 'i'
def genOutline(self, i):
toc = self.doc.tocs[i]
obj = self.outLineObjs[i]
if i != (len(self.doc.tocs) - 1):
nextStr = "/Next %d 0 R\n" % (obj.nr + 1)
else:
nextStr = ""
if i != 0:
prevStr = "/Prev %d 0 R\n" % (obj.nr - 1)
else:
prevStr = ""
obj.data = ("<< /Parent %d 0 R\n"
"/Dest [%d 0 R /XYZ %f %f 0]\n"
"/Title (%s)\n"
"%s"
"%s"
">>" % (
self.outlinesObj.nr, toc.pageObjNr, self.x(toc.op.x),
self.y(toc.op.y), self.escapeStr(toc.text),
prevStr, nextStr))
# generate a stream object's contents. 's' is all data between
# 'stream/endstream' tags, excluding newlines.
def genStream(self, s, isFontStream = False):
compress = False
# embedded TrueType font program streams for some reason need a
# Length1 entry that records the uncompressed length of the stream
if isFontStream:
lenStr = "/Length1 %d\n" % len(s)
else:
lenStr = ""
filterStr = " "
if compress:
s = s.encode("zlib")
filterStr = "/Filter /FlateDecode\n"
return ("<< /Length %d\n%s%s>>\n"
"stream\n"
"%s\n"
"endstream" % (len(s), lenStr, filterStr, s))
# add a new object and return it. 'data' is all data between
# 'obj/endobj' tags, excluding newlines.
def addObj(self, data = ""):
obj = PDFObject(self.objectCnt, data)
self.objects.append(obj)
self.objectCnt += 1
return obj
# write out object to 'output' (util.String)
def writeObj(self, output, obj):
obj.xrefPos = len(output)
obj.write(output)
# write a xref table entry to 'output' (util.String), using position
# 'pos, generation 'gen' and type 'typ'.
def writeXref(self, output, pos, gen = 0, typ = "n"):
output += "%010d %05d %s \n" % (pos, gen, typ)
# generate PDF file and return it as a string
def genPDF(self):
data = util.String()
data += "%PDF-1.5\n"
for obj in self.objects:
self.writeObj(data, obj)
xrefStartPos = len(data)
data += "xref\n0 %d\n" % self.objectCnt
self.writeXref(data, 0, 65535, "f")
for obj in self.objects:
self.writeXref(data, obj.xrefPos)
data += "\n"
data += ("trailer\n"
"<< /Size %d\n"
"/Root %d 0 R\n"
"/Info %d 0 R\n>>\n" % (
self.objectCnt, self.catalogObj.nr, self.infoObj.nr))
data += "startxref\n%d\n%%%%EOF\n" % xrefStartPos
return str(data)
# get font number to use for given flags. also creates the PDF object
# for the font if it does not yet exist.
def getFontNr(self, flags):
# the "& 15" gets rid of the underline flag
fi = self.fonts.get(flags & 15)
if not fi:
print("PDF.getfontNr: invalid flags %d" % flags)
return 0
if fi.number == -1:
fi.number = self.fontCnt
self.fontCnt += 1
# the "& 15" gets rid of the underline flag
pfi = self.doc.fonts.get(flags & 15)
if not pfi:
fi.pdfObj = self.addObj("<< /Type /Font\n"
"/Subtype /Type1\n"
"/BaseFont /%s\n"
"/Encoding /WinAnsiEncoding\n"
">>" % fi.name)
else:
self.genWidths()
fi.pdfObj = self.addObj("<< /Type /Font\n"
"/Subtype /TrueType\n"
"/BaseFont /%s\n"
"/Encoding /WinAnsiEncoding\n"
"/FirstChar 0\n"
"/LastChar 255\n"
"/Widths %d 0 R\n"
"/FontDescriptor %d 0 R\n"
">>" % (pfi.name, self.widthsObj.nr,
self.objectCnt + 1))
fm = fontinfo.getMetrics(flags)
if pfi.fontProgram:
fpStr = "/FontFile2 %d 0 R\n" % (self.objectCnt + 1)
else:
fpStr = ""
# we use a %s format specifier for the italic angle since
# it sometimes contains integers, sometimes floating point
# values.
self.addObj("<< /Type /FontDescriptor\n"
"/FontName /%s\n"
"/FontWeight %d\n"
"/Flags %d\n"
"/FontBBox [%d %d %d %d]\n"
"/ItalicAngle %s\n"
"/Ascent %s\n"
"/Descent %s\n"
"/CapHeight %s\n"
"/StemV %s\n"
"/StemH %s\n"
"/XHeight %d\n"
"%s"
">>" % (pfi.name,
fm.fontWeight,
fm.flags,
fm.bbox[0], fm.bbox[1],
fm.bbox[2], fm.bbox[3],
fm.italicAngle,
fm.ascent,
fm.descent,
fm.capHeight,
fm.stemV,
fm.stemH,
fm.xHeight,
fpStr))
if pfi.fontProgram:
self.addObj(self.genStream(pfi.fontProgram, True))
return fi.number
# escape string
def escapeStr(self, s):
return s.replace("\\", "\\\\").replace("(", "\\(").replace(")", "\\)")
# convert mm to points (1/72 inch).
def mm2points(self, mm):
# 2.834 = 72 / 25.4
return mm * 2.83464567
# convert x coordinate
def x(self, x):
return self.mm2points(x)
# convert y coordinate
def y(self, y):
return self.mm2points(self.doc.h - y)
# convert xy, which is (x, y) pair, into PDF coordinates, and format
# it as "%f %f", and return that.
def xy(self, xy):
x = self.x(xy[0])
y = self.y(xy[1])
return "%f %f" % (x, y)

267
src/pml.py Normal file
View File

@ -0,0 +1,267 @@
# PML is short for Page Modeling Language, our own neat little PDF-wannabe
# format for expressing a script's complete contents in a neutral way
# that's easy to render to almost anything, e.g. PDF, Postscript, Windows
# GDI, etc.
#
# A PML document is a collection of pages plus possibly some metadata.
# Each page is a collection of simple drawing commands, executed
# sequentially in the order given, assuming "complete overdraw" semantics
# on the output device, i.e. whatever is drawn completely covers things it
# is painted on top of.
# All measurements in PML are in (floating point) millimeters.
import misc
import pdf
import util
import textwrap
# text flags. don't change these unless you know what you're doing.
NORMAL = 0
BOLD = 1
ITALIC = 2
COURIER = 0
TIMES_ROMAN = 4
HELVETICA = 8
UNDERLINED = 16
# fill types
NO_FILL = 0
FILL = 1
STROKE_FILL = 2
# A single document.
class Document:
# (w, h) is the size of each page.
def __init__(self, w, h):
self.w = w
self.h = h
# a collection of Page objects
self.pages = []
# a collection of TOCItem objects
self.tocs = []
# user-specified fonts, if any. key = 2 lowest bits of
# TextOp.flags, value = pml.PDFFontInfo
self.fonts = {}
# whether to show TOC by default on document open
self.showTOC = False
# page number to display on document open, or -1
self.defPage = -1
# when running testcases, misc.version does not exist, so store a
# dummy value in that case, correct value otherwise.
self.version = getattr(misc, "version", "dummy_version")
# a random string to embed in the PDF; only used by watermarked
# PDFs
self.uniqueId = None
def add(self, page):
self.pages.append(page)
def addTOC(self, toc):
self.tocs.append(toc)
def addFont(self, style, pfi):
self.fonts[style] = pfi
class Page:
def __init__(self, doc):
# link to containing document
self.doc = doc
# a collection of Operation objects
self.ops = []
def add(self, op):
self.ops.append(op)
def addOpsToFront(self, opsList):
self.ops = opsList + self.ops
# Table of content item (Outline item, in PDF lingo)
class TOCItem:
def __init__(self, text, op):
# text to show in TOC
self.text = text
# pointer to the TextOp that this item links to (used to get the
# correct positioning information)
self.op = op
# the PDF object number of the page we point to
self.pageObjNr = -1
# information about one PDF font
class PDFFontInfo:
def __init__(self, name, fontProgram):
# name to use in generated PDF file ("CourierNew", "MyFontBold",
# etc.). if empty, use the default PDF font.
self.name = name
# the font program (in practise, the contents of the .ttf file for
# the font), or None, in which case the font is not embedded.
self.fontProgram = fontProgram
# An abstract base class for all drawing operations.
class DrawOp:
pass
# Draw text string 'text', at position (x, y) mm from the upper left
# corner of the page. Font used is 'size' points, and Courier / Times/
# Helvetica as indicated by the flags, possibly being bold / italic /
# underlined. angle is None, or an integer from 0 to 360 that gives the
# slant of the text counter-clockwise from x-axis.
class TextOp(DrawOp):
pdfOp = pdf.PDFTextOp()
def __init__(self, text, x, y, size, flags = NORMAL | COURIER,
align = util.ALIGN_LEFT, valign = util.VALIGN_TOP,
line = -1, angle = None):
self.text = text
self.x = x
self.y = y
self.size = size
self.flags = flags
self.angle = angle
# TOCItem, by default we have none
self.toc = None
# index of line in Screenplay.lines, or -1 if some other text.
# only used when drawing display, pdf output doesn't use this.
self.line = line
if align != util.ALIGN_LEFT:
w = util.getTextWidth(text, flags, size)
if align == util.ALIGN_CENTER:
self.x -= w / 2.0
elif align == util.ALIGN_RIGHT:
self.x -= w
if valign != util.VALIGN_TOP:
h = util.getTextHeight(size)
if valign == util.VALIGN_CENTER:
self.y -= h / 2.0
elif valign == util.VALIGN_BOTTOM:
self.y -= h
# Draw consecutive lines. 'points' is a list of (x, y) pairs (minimum 2
# pairs) and 'width' is the line width, with 0 being the thinnest possible
# line. if 'isClosed' is True, the last point on the list is connected to
# the first one.
class LineOp(DrawOp):
pdfOp = pdf.PDFLineOp()
def __init__(self, points, width, isClosed = False):
self.points = points
self.width = width
self.isClosed = isClosed
# helper function for creating simple lines
def genLine(x, y, xd, yd, width):
return LineOp([(x, y), (x + xd, y + yd)], width)
# Draw a rectangle, possibly filled, with specified lineWidth (which can
# be -1 if fillType is FILL). (x, y) is position of upper left corner.
class RectOp(DrawOp):
pdfOp = pdf.PDFRectOp()
def __init__(self, x, y, width, height, fillType = FILL, lineWidth = -1):
self.x = x
self.y = y
self.width = width
self.height = height
self.fillType = fillType
self.lw = lineWidth
# Draw a quarter circle centered at (x, y) with given radius and line
# width. By default it will be the upper left quadrant of a circle, but
# using the flip[XY] parameters you can choose other quadrants.
class QuarterCircleOp(DrawOp):
pdfOp = pdf.PDFQuarterCircleOp()
def __init__(self, x, y, radius, width, flipX = False, flipY = False):
self.x = x
self.y = y
self.radius = radius
self.width = width
self.flipX = flipX
self.flipY = flipY
# Arbitrary PDF commands. Should not have whitespace in the beginning or
# the end. Should be used only for non-critical things like tweaking line
# join styles etc, because non-PDF renderers will ignore these.
class PDFOp(DrawOp):
pdfOp = pdf.PDFArbitraryOp()
def __init__(self, cmds):
self.cmds = cmds
# create a PML document containing text (possibly linewrapped) divided
# into pages automatically.
class TextFormatter:
def __init__(self, width, height, margin, fontSize):
self.doc = Document(width, height)
# how much to leave empty on each side (mm)
self.margin = margin
# font size
self.fontSize = fontSize
# number of chararacters that fit on a single line
self.charsToLine = int((width - margin * 2.0) /
util.getTextWidth(" ", COURIER, fontSize))
self.createPage()
# add new empty page, select it as current, reset y pos
def createPage(self):
self.pg = Page(self.doc)
self.doc.add(self.pg)
self.y = self.margin
# add blank vertical space, unless we're at the top of the page
def addSpace(self, mm):
if self.y > self.margin:
self.y += mm
# add text
def addText(self, text, x = None, fs = None, style = NORMAL):
if x == None:
x = self.margin
if fs == None:
fs = self.fontSize
yd = util.getTextHeight(fs)
if (self.y + yd) > (self.doc.h - self.margin):
self.createPage()
self.pg.add(TextOp(text, x, self.y, fs, style))
self.y += yd
# wrap text into lines that fit on the page, using Courier and default
# font size and style, and add the lines. 'indent' is the text to
# prefix lines other than the first one with.
def addWrappedText(self, text, indent):
tmp = textwrap.wrap(text, self.charsToLine,
subsequent_indent = indent)
for s in tmp:
self.addText(s)

87
src/reports.py Normal file
View File

@ -0,0 +1,87 @@
import gutil
import misc
import util
import characterreport
import locationreport
import scenereport
import scriptreport
import wx
def genSceneReport(mainFrame, sp):
report = scenereport.SceneReport(sp)
dlg = misc.CheckBoxDlg(mainFrame, "Report type", report.inf,
"Information to include:", False)
ok = False
if dlg.ShowModal() == wx.ID_OK:
ok = True
dlg.Destroy()
if not ok:
return
data = report.generate()
gutil.showTempPDF(data, sp.cfgGl, mainFrame)
def genLocationReport(mainFrame, sp):
report = locationreport.LocationReport(scenereport.SceneReport(sp))
dlg = misc.CheckBoxDlg(mainFrame, "Report type", report.inf,
"Information to include:", False)
ok = False
if dlg.ShowModal() == wx.ID_OK:
ok = True
dlg.Destroy()
if not ok:
return
data = report.generate()
gutil.showTempPDF(data, sp.cfgGl, mainFrame)
def genCharacterReport(mainFrame, sp):
report = characterreport.CharacterReport(sp)
if not report.cinfo:
wx.MessageBox("No characters speaking found.",
"Error", wx.OK, mainFrame)
return
charNames = []
for s in util.listify(report.cinfo, "name"):
charNames.append(misc.CheckBoxItem(s))
dlg = misc.CheckBoxDlg(mainFrame, "Report type", report.inf,
"Information to include:", False, charNames,
"Characters to include:", True)
ok = False
if dlg.ShowModal() == wx.ID_OK:
ok = True
for i in range(len(report.cinfo)):
report.cinfo[i].include = charNames[i].selected
dlg.Destroy()
if not ok:
return
data = report.generate()
gutil.showTempPDF(data, sp.cfgGl, mainFrame)
def genScriptReport(mainFrame, sp):
report = scriptreport.ScriptReport(sp)
data = report.generate()
gutil.showTempPDF(data, sp.cfgGl, mainFrame)

175
src/scenereport.py Normal file
View File

@ -0,0 +1,175 @@
import misc
import pdf
import pml
import screenplay
import util
class SceneReport:
def __init__(self, sp):
self.sp = sp
# list of SceneInfos
self.scenes = []
line = 0
while 1:
if line >= len(sp.lines):
break
startLine, endLine = sp.getSceneIndexesFromLine(line)
si = SceneInfo(sp)
si.read(sp, startLine, endLine)
self.scenes.append(si)
line = endLine + 1
# we don't use these, but ScriptReport does
lineSeq = [si.lines for si in self.scenes]
self.longestScene = max(lineSeq)
self.avgScene = sum(lineSeq) / float(len(self.scenes))
# information about what to include (and yes, the comma is needed
# to unpack the list)
self.INF_SPEAKERS, = list(range(1))
self.inf = []
for s in ["Speakers"]:
self.inf.append(misc.CheckBoxItem(s))
def generate(self):
tf = pml.TextFormatter(self.sp.cfg.paperWidth,
self.sp.cfg.paperHeight, 15.0, 12)
for si in self.scenes:
tf.addSpace(5.0)
tf.addText("%-4s %s" % (si.number, si.name), style = pml.BOLD)
tf.addSpace(1.0)
tf.addText(" Lines: %d (%s%% action), Pages: %d"
" (%s)" % (si.lines, util.pct(si.actionLines, si.lines),
len(si.pages), si.pages))
if self.inf[self.INF_SPEAKERS].selected:
tf.addSpace(2.5)
for it in util.sortDict(si.chars):
tf.addText(" %3d %s" % (it[1], it[0]))
return pdf.generate(tf.doc)
# information about one scene
class SceneInfo:
def __init__(self, sp):
# scene number, e.g. "42A"
self.number = None
# scene name, e.g. "INT. MOTEL ROOM - NIGHT"
self.name = None
# total lines, excluding scene lines
self.lines = 0
# action lines
self.actionLines = 0
# page numbers
self.pages = screenplay.PageList(sp.getPageNumbers())
# key = character name (upper cased), value = number of dialogue
# lines
self.chars = {}
# read information for scene within given lines.
def read(self, sp, startLine, endLine):
self.number = sp.getSceneNumber(startLine)
ls = sp.lines
# TODO: handle multi-line scene names
if ls[startLine].lt == screenplay.SCENE:
s = util.upper(ls[startLine].text)
if len(s.strip()) == 0:
self.name = "(EMPTY SCENE NAME)"
else:
self.name = s
else:
self.name = "(NO SCENE NAME)"
self.pages.addPage(sp.line2page(startLine))
line = startLine
# skip over scene headers
while (line <= endLine) and (ls[line].lt == screenplay.SCENE):
line = sp.getElemLastIndexFromLine(line) + 1
if line > endLine:
# empty scene
return
# re-define startLine to be first line after scene header
startLine = line
self.lines = endLine - startLine + 1
# get number of action lines and store page information
for i in range(startLine, endLine + 1):
self.pages.addPage(sp.line2page(i))
if ls[i].lt == screenplay.ACTION:
self.actionLines += 1
line = startLine
while 1:
line = self.readSpeech(sp, line, endLine)
if line >= endLine:
break
# read information for one (or zero) speech, beginning at given line.
# return line number of the last line of the speech + 1, or endLine +
# 1 if no speech found.
def readSpeech(self, sp, line, endLine):
ls = sp.lines
# find start of speech
while (line < endLine) and (ls[line].lt != screenplay.CHARACTER):
line += 1
if line >= endLine:
# no speech found, or CHARACTER was on last line, leaving no
# space for dialogue.
return endLine
# TODO: handle multi-line character names
s = util.upper(ls[line].text)
if len(s.strip()) == 0:
name = "(EMPTY CHARACTER NAME)"
else:
name = s
# skip over character name
line = sp.getElemLastIndexFromLine(line) + 1
# dialogue lines
dlines = 0
while 1:
if line > endLine:
break
lt = ls[line].lt
if lt == screenplay.DIALOGUE:
dlines += 1
elif lt != screenplay.PAREN:
break
line += 1
if dlines > 0:
self.chars[name] = self.chars.get(name, 0) + dlines
return line

3356
src/screenplay.py Normal file

File diff suppressed because it is too large Load Diff

88
src/scriptreport.py Normal file
View File

@ -0,0 +1,88 @@
import characterreport
import config
import pdf
import pml
import scenereport
import screenplay
import util
class ScriptReport:
def __init__(self, sp):
self.sp = sp
self.sr = scenereport.SceneReport(sp)
self.cr = characterreport.CharacterReport(sp)
def generate(self):
tf = pml.TextFormatter(self.sp.cfg.paperWidth,
self.sp.cfg.paperHeight, 15.0, 12)
ls = self.sp.lines
total = len(ls)
tf.addText("Total lines in script: %5d" % total)
tf.addSpace(2.0)
for t in config.getTIs():
cnt = sum([1 for line in ls if line.lt == t.lt])
tf.addText(" %13s: %4d (%d%%)" % (t.name, cnt,
util.pct(cnt, total)))
tf.addSpace(4.0)
intLines = sum([si.lines for si in self.sr.scenes if
util.upper(si.name).startswith("INT.")])
extLines = sum([si.lines for si in self.sr.scenes if
util.upper(si.name).startswith("EXT.")])
tf.addText("Interior / exterior scenes: %d%% / %d%%" % (
util.pct(intLines, intLines + extLines),
util.pct(extLines, intLines + extLines)))
tf.addSpace(4.0)
tf.addText("Max / avg. scene length in lines: %d / %.2f" % (
self.sr.longestScene, self.sr.avgScene))
# lengths of action elements
actions = []
# length of current action element
curLen = 0
for ln in ls:
if curLen > 0:
if ln.lt == screenplay.ACTION:
curLen += 1
if ln.lb == screenplay.LB_LAST:
actions.append(curLen)
curLen = 0
else:
actions.append(curLen)
curLen = 0
else:
if ln.lt == screenplay.ACTION:
curLen = 1
if curLen > 0:
actions.append(curLen)
tf.addSpace(4.0)
# avoid divide-by-zero
if len(actions) > 0:
maxA = max(actions)
avgA = sum(actions) / float(len(actions))
else:
maxA = 0
avgA = 0.0
tf.addText("Max / avg. action element length in lines: %d / %.2f" % (
maxA, avgA))
tf.addSpace(4.0)
tf.addText("Speaking characters: %d" % len(self.cr.cinfo))
return pdf.generate(tf.doc)

216
src/spellcheck.py Normal file
View File

@ -0,0 +1,216 @@
import mypickle
import util
# words loaded from dict_en.dat.
gdict = set()
# key = util.getWordPrefix(word), value = set of words beginning with
# that prefix (only words in gdict)
prefixDict = {}
# load word dictionary. returns True on success or if it's already loaded,
# False on errors.
def loadDict(frame):
if gdict:
return True
s = util.loadMaybeCompressedFile("dict_en.dat", frame)
if not s:
return False
lines = s.splitlines()
chars = "abcdefghijklmnopqrstuvwxyz"
for ch1 in chars:
for ch2 in chars:
prefixDict[ch1 + ch2] = set()
gwp = util.getWordPrefix
for word in lines:
# theoretically, we should do util.lower(util.toInputStr(it)), but:
#
# -user's aren't supposed to modify the file
#
# -it takes 1.35 secs, compared to 0.56 secs if we don't, on an
# 1.33GHz Athlon
gdict.add(word)
if len(word) > 2:
prefixDict[gwp(word)].add(word)
return True
# dictionary, a list of known words that the user has specified.
class Dict:
cvars = None
def __init__(self):
if not self.__class__.cvars:
v = self.__class__.cvars = mypickle.Vars()
v.addList("wordsList", [], "Words",
mypickle.StrLatin1Var("", "", ""))
v.makeDicts()
self.__class__.cvars.setDefaults(self)
# we have wordsList that we use for saving/loading, and words,
# which we use during normal operation. it's possible we should
# introduce a mypickle.SetVar...
# key = word, lowercased, value = None
self.words = {}
# load from string 's'. does not throw any exceptions and silently
# ignores any errors.
def load(self, s):
self.cvars.load(self.cvars.makeVals(s), "", self)
self.words = {}
for w in self.wordsList:
self.words[w] = None
self.refresh()
# save to a string and return that.
def save(self):
self.wordsList = self.get()
return self.cvars.save("", self)
# fix up invalid values.
def refresh(self):
ww = {}
for w in list(self.words.keys()):
w = self.cleanWord(w)
if w:
ww[w] = None
self.words = ww
# returns True if word is known
def isKnown(self, word):
return word in self.words
# add word
def add(self, word):
word = self.cleanWord(word)
if word:
self.words[word] = None
# set words from a list
def set(self, words):
self.words = {}
for w in words:
self.add(w)
# get a sorted list of all the words.
def get(self):
keys = list(self.words.keys())
keys.sort()
return keys
# clean up word in all possible ways and return it, or an empty string
# if nothing remains.
def cleanWord(self, word):
word = util.splitToWords(util.lower(util.toInputStr(word)))
if len(word) == 0:
return ""
return word[0]
# spell check a script
class SpellChecker:
def __init__(self, sp, gScDict):
self.sp = sp
# user's global dictionary (Dict)
self.gScDict = gScDict
# key = word found in character names, value = None
self.cnames = {}
for it in sp.getCharacterNames():
for w in util.splitToWords(it):
self.cnames[w] = None
self.word = None
self.line = self.sp.line
# we can't use the current column, because if the cursor is in the
# middle of a word, we flag the partial word as misspelled.
self.col = 0
# find next possibly misspelled word and store its location. returns
# True if such a word found.
def findNext(self):
line = self.line
col = self.col
# clear these so there's no chance of them left pointing to
# something, we return False, and someone tries to access them
# anyhow.
self.word = None
self.line = 0
self.col = 0
while 1:
word, line, col = self.sp.getWord(line, col)
if not word:
return False
if not self.isKnown(word):
self.word = word
self.line = line
self.col = col
return True
col += len(word)
# return True if word is a known word.
def isKnown(self, word):
word = util.lower(word)
return word in gdict or \
word in self.cnames or \
self.sp.scDict.isKnown(word) or \
self.gScDict.isKnown(word) or \
word.isdigit()
# Calculates the Levenshtein distance between a and b.
def lev(a, b):
n, m = len(a), len(b)
if n > m:
# Make sure n <= m, to use O(min(n, m)) space
a, b = b, a
n, m = m, n
current = list(range(n + 1))
for i in range(1, m + 1):
previous, current = current, [i] + [0] * m
for j in range(1, n + 1):
add, delete = previous[j] + 1, current[j - 1] + 1
change = previous[j - 1]
if a[j - 1] != b[i - 1]:
change += 1
current[j] = min(add, delete, change)
return current[n]

58
src/spellcheckcfgdlg.py Normal file
View File

@ -0,0 +1,58 @@
import gutil
import misc
import util
import wx
class SCDictDlg(wx.Dialog):
def __init__(self, parent, scDict, isGlobal):
wx.Dialog.__init__(self, parent, -1, "Spell checker dictionary",
style = wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
self.scDict = scDict
vsizer = wx.BoxSizer(wx.VERTICAL)
if isGlobal:
s = "Global words:"
else:
s = "Script-specific words:"
vsizer.Add(wx.StaticText(self, -1, s))
self.itemsEntry = wx.TextCtrl(self, -1, style = wx.TE_MULTILINE |
wx.TE_DONTWRAP, size = (300, 300))
vsizer.Add(self.itemsEntry, 1, wx.EXPAND)
hsizer = wx.BoxSizer(wx.HORIZONTAL)
hsizer.Add((1, 1), 1)
cancelBtn = gutil.createStockButton(self, "Cancel")
hsizer.Add(cancelBtn, 0, wx.LEFT, 10)
okBtn = gutil.createStockButton(self, "OK")
hsizer.Add(okBtn, 0, wx.LEFT, 10)
vsizer.Add(hsizer, 0, wx.EXPAND | wx.TOP, 10)
self.cfg2gui()
util.finishWindow(self, vsizer)
self.Bind(wx.EVT_TEXT, self.OnMisc, id=self.itemsEntry.GetId())
self.Bind(wx.EVT_BUTTON, self.OnCancel, id=cancelBtn.GetId())
self.Bind(wx.EVT_BUTTON, self.OnOK, id=okBtn.GetId())
def OnOK(self, event):
self.scDict.refresh()
self.EndModal(wx.ID_OK)
def OnCancel(self, event):
self.EndModal(wx.ID_CANCEL)
def OnMisc(self, event):
self.scDict.set(misc.fromGUI(self.itemsEntry.GetValue()).split("\n"))
def cfg2gui(self):
self.itemsEntry.SetValue("\n".join(self.scDict.get()))

231
src/spellcheckdlg.py Normal file
View File

@ -0,0 +1,231 @@
import misc
import spellcheck
import undo
import util
import wx
class SpellCheckDlg(wx.Dialog):
def __init__(self, parent, ctrl, sc, gScDict):
wx.Dialog.__init__(self, parent, -1, "Spell checker",
style = wx.DEFAULT_DIALOG_STYLE | wx.WANTS_CHARS)
self.ctrl = ctrl
# spellcheck.SpellCheck
self.sc = sc
# user's global spell checker dictionary
self.gScDict = gScDict
# have we added any words to global dictionary
self.changedGlobalDict = False
vsizer = wx.BoxSizer(wx.VERTICAL)
hsizer = wx.BoxSizer(wx.HORIZONTAL)
hsizer.Add(wx.StaticText(self, -1, "Word:"), 0,
wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 10)
self.replaceEntry = wx.TextCtrl(self, -1, style = wx.TE_PROCESS_ENTER)
hsizer.Add(self.replaceEntry, 1, wx.EXPAND)
vsizer.Add(hsizer, 1, wx.EXPAND | wx.BOTTOM, 15)
gsizer = wx.FlexGridSizer(2, 2, 10, 10)
gsizer.AddGrowableCol(1)
replaceBtn = wx.Button(self, -1, "&Replace")
gsizer.Add(replaceBtn)
addScriptBtn = wx.Button(self, -1, "Add to &script dictionary")
gsizer.Add(addScriptBtn, 0, wx.EXPAND)
skipBtn = wx.Button(self, -1, "S&kip")
gsizer.Add(skipBtn)
addGlobalBtn = wx.Button(self, -1, "Add to &global dictionary")
gsizer.Add(addGlobalBtn, 0, wx.EXPAND)
vsizer.Add(gsizer, 0, wx.EXPAND, 0)
suggestBtn = wx.Button(self, -1, "S&uggest replacement")
vsizer.Add(suggestBtn, 0, wx.EXPAND | wx.TOP, 10)
self.Bind(wx.EVT_TEXT_ENTER, self.OnReplace, id=self.replaceEntry.GetId())
self.Bind(wx.EVT_BUTTON, self.OnReplace, id=replaceBtn.GetId())
self.Bind(wx.EVT_BUTTON, self.OnAddScript, id=addScriptBtn.GetId())
self.Bind(wx.EVT_BUTTON, self.OnAddGlobal, id=addGlobalBtn.GetId())
self.Bind(wx.EVT_BUTTON, self.OnSkip, id=skipBtn.GetId())
self.Bind(wx.EVT_BUTTON, self.OnSuggest, id=suggestBtn.GetId())
self.Bind(wx.EVT_CHAR, self.OnChar)
self.replaceEntry.Bind(wx.EVT_CHAR, self.OnChar)
replaceBtn.Bind(wx.EVT_CHAR, self.OnChar)
addScriptBtn.Bind(wx.EVT_CHAR, self.OnChar)
skipBtn.Bind(wx.EVT_CHAR, self.OnChar)
addGlobalBtn.Bind(wx.EVT_CHAR, self.OnChar)
suggestBtn.Bind(wx.EVT_CHAR, self.OnChar)
util.finishWindow(self, vsizer)
self.showWord()
def showWord(self):
self.ctrl.sp.line = self.sc.line
self.ctrl.sp.column = self.sc.col
self.ctrl.sp.setMark(self.sc.line, self.sc.col + len(self.sc.word) - 1)
self.replaceEntry.SetValue(self.sc.word)
self.ctrl.makeLineVisible(self.sc.line)
self.ctrl.updateScreen()
def gotoNext(self, incCol = True):
if incCol:
self.sc.col += len(self.sc.word)
if not self.sc.findNext():
wx.MessageBox("No more incorrect words found.", "Results",
wx.OK, self)
self.EndModal(wx.ID_OK)
return
self.showWord()
def OnChar(self, event):
kc = event.GetKeyCode()
if kc == wx.WXK_ESCAPE:
self.EndModal(wx.ID_OK)
return
event.Skip()
def OnReplace(self, event):
if not self.sc.word:
return
sp = self.ctrl.sp
u = undo.SinglePara(sp, undo.CMD_MISC, self.sc.line)
word = util.toInputStr(misc.fromGUI(self.replaceEntry.GetValue()))
ls = sp.lines
sp.gotoPos(self.sc.line, self.sc.col)
ls[self.sc.line].text = util.replace(
ls[self.sc.line].text, word,
self.sc.col, len(self.sc.word))
sp.rewrapPara(sp.getParaFirstIndexFromLine(self.sc.line))
# rewrapping a paragraph can have moved the cursor, so get the new
# location of it, and then advance past the just-changed word
self.sc.line = sp.line
self.sc.col = sp.column + len(word)
sp.clearMark()
sp.markChanged()
u.setAfter(sp)
sp.addUndo(u)
self.gotoNext(False)
def OnSkip(self, event = None, autoFind = False):
if not self.sc.word:
return
self.gotoNext()
def OnAddScript(self, event):
if not self.sc.word:
return
self.ctrl.sp.scDict.add(self.sc.word)
self.ctrl.sp.markChanged()
self.gotoNext()
def OnAddGlobal(self, event):
if not self.sc.word:
return
self.gScDict.add(self.sc.word)
self.changedGlobalDict = True
self.gotoNext()
def OnSuggest(self, event):
if not self.sc.word:
return
isAllCaps = self.sc.word == util.upper(self.sc.word)
isCapitalized = self.sc.word[:1] == util.upper(self.sc.word[:1])
word = util.lower(self.sc.word)
wl = len(word)
wstart = word[:2]
d = 500
fifo = util.FIFO(5)
wx.BeginBusyCursor()
for w in spellcheck.prefixDict[util.getWordPrefix(word)]:
if w.startswith(wstart):
d = self.tryWord(word, wl, w, d, fifo)
for w in self.gScDict.words.keys():
if w.startswith(wstart):
d = self.tryWord(word, wl, w, d, fifo)
for w in self.ctrl.sp.scDict.words.keys():
if w.startswith(wstart):
d = self.tryWord(word, wl, w, d, fifo)
items = fifo.get()
wx.EndBusyCursor()
if len(items) == 0:
wx.MessageBox("No similar words found.", "Results",
wx.OK, self)
return
dlg = wx.SingleChoiceDialog(
self, "Most similar words:", "Suggestions", items)
if dlg.ShowModal() == wx.ID_OK:
sel = dlg.GetSelection()
newWord = items[sel]
if isAllCaps:
newWord = util.upper(newWord)
elif isCapitalized:
newWord = util.capitalize(newWord)
self.replaceEntry.SetValue(newWord)
dlg.Destroy()
# if w2 is closer to w1 in Levenshtein distance than d, add it to
# fifo. return min(d, new_distance).
def tryWord(self, w1, w1len, w2, d, fifo):
if abs(w1len - len(w2)) > 3:
return d
d2 = spellcheck.lev(w1, w2)
if d2 <= d:
fifo.add(w2)
return d2
return d

156
src/splash.py Normal file
View File

@ -0,0 +1,156 @@
# -*- coding: utf-8 -*-
import misc
import util
import random
import wx
class Quote:
def __init__(self, source, lines):
# unicode string
self.source = source
# list of unicode strings
self.lines = lines
class SplashWindow(wx.Frame):
inited = False
# Quote objects
quotes = []
def __init__(self, parent, delay):
wx.Frame.__init__(
self, parent, -1, "Splash",
style = wx.FRAME_FLOAT_ON_PARENT | wx.NO_BORDER)
if not SplashWindow.inited:
SplashWindow.inited = True
wx.Image.AddHandler(wx.JPEGHandler())
self.loadQuotes(parent)
self.pickRandomQuote()
self.pic = misc.getBitmap("resources/logo.jpg")
if self.pic.IsOk():
w, h = (self.pic.GetWidth(), self.pic.GetHeight())
else:
w, h = (375, 300)
util.setWH(self, w, h)
self.CenterOnScreen()
self.textColor = wx.Colour(0, 0, 0)
self.font = util.createPixelFont(
14, wx.FONTFAMILY_MODERN, wx.NORMAL, wx.NORMAL)
self.quoteFont = util.createPixelFont(
16, wx.FONTFAMILY_DEFAULT, wx.NORMAL, wx.NORMAL)
self.sourceFont = util.createPixelFont(
15, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_ITALIC, wx.NORMAL)
if delay != -1:
self.timer = wx.Timer(self)
wx.Timer()
self.timer.Start(delay, True)
self.Bind(wx.EVT_LEFT_DOWN, self.OnClick)
self.Bind(wx.EVT_PAINT, self.OnPaint)
self.Bind(wx.EVT_CLOSE, self.OnCloseWindow)
def OnClick(self, event):
self.Close()
def OnPaint(self, event):
dc = wx.PaintDC(self)
dc.SetFont(self.font)
dc.SetTextForeground(self.textColor)
if self.pic.IsOk():
dc.DrawBitmap(self.pic, 0, 0, False)
util.drawText(dc, "Version %s" % (misc.version),
200, 170, util.ALIGN_RIGHT)
util.drawText(dc, "http://www.trelby.org/", 200, 185, util.ALIGN_RIGHT)
if self.quote:
dc.SetFont(self.sourceFont)
dc.DrawText(self.quote.source, 50, 280)
dc.SetFont(self.quoteFont)
for i,line in enumerate(self.quote.lines):
x = 10
y = 260 - (len(self.quote.lines) - i - 1) * 17
if i == 0:
dc.DrawText("", x - 5, y)
if i == (len(self.quote.lines) - 1):
line = line + ""
dc.DrawText(line, x, y)
def OnTimer(self, event):
self.timer.Stop()
self.Close()
def OnCloseWindow(self, event):
self.Destroy()
self.Refresh()
def pickRandomQuote(self):
if not SplashWindow.quotes:
self.quote = None
else:
self.quote = random.choice(SplashWindow.quotes)
@staticmethod
def loadQuotes(parent):
try:
data = util.loadFile(misc.getFullPath("resources/quotes.txt"), parent)
if data is None:
return
#data = data.decode("utf-8")
lines = data.splitlines()
quotes = []
# lines saved for current quote being processed
tmp = []
for i,line in enumerate(lines):
if line.startswith("#") or not line.strip():
continue
if line.startswith(" "):
if not tmp:
raise Exception("No lines defined for quote at line %d" % (i + 1))
if len(tmp) > 3:
raise Exception("Too many lines defined for quote at line %d" % (i + 1))
quotes.append(Quote(line.strip(), tmp))
tmp = []
else:
tmp.append(line.strip())
if tmp:
raise Exception("Last quote does not have source")
SplashWindow.quotes = quotes
except Exception as e:
wx.MessageBox("Error loading quotes: %s" % str(e),
"Error", wx.OK, parent)

201
src/titles.py Normal file
View File

@ -0,0 +1,201 @@
import pml
import util
import functools
# a script's title pages.
class Titles:
def __init__(self):
# list of lists of TitleString objects
self.pages = []
# create semi-standard title page
def addDefaults(self):
a = []
y = 105.0
a.append(TitleString(["UNTITLED SCREENPLAY"], y = y, size = 24,
isBold = True, font = pml.HELVETICA))
a.append(TitleString(["by", "", "My Name Here"], y = y + 15.46))
x = 15.0
y = 240.0
a.append(TitleString(["123/456-7890", "no.such@thing.com"], x, y + 8.46, False))
self.pages.append(a)
# add title pages to doc.
def generatePages(self, doc):
for page in self.pages:
pg = pml.Page(doc)
for s in page:
s.generatePML(pg)
doc.add(pg)
# return a (rough) RTF fragment representation of title pages
def generateRTF(self):
s = util.String()
for page in self.pages:
for p in page:
s += p.generateRTF()
s += "\\page\n"
return str(s)
# sort the title strings in y,x order (makes editing them easier
# and RTF output better)
def sort(self):
def cmpfunc(a, b):
return ((a.y > b.y) - (a.y < b.y)) or ((a.x > b.x) - (a.x < a.y))
for page in self.pages:
page = sorted(page, key=functools.cmp_to_key(cmpfunc))
# a single string displayed on a title page
class TitleString:
def __init__(self, items, x = 0.0, y = 0.0, isCentered = True,
isBold = False, size = 12, font = pml.COURIER):
# list of text strings
self.items = items
# position
self.x = x
self.y = y
# size in points
self.size = size
# whether this is centered in the horizontal direction
self.isCentered = isCentered
# whether this is right-justified (xpos = rightmost edge of last
# character)
self.isRightJustified = False
# style flags
self.isBold = isBold
self.isItalic = False
self.isUnderlined = False
# font
self.font = font
def getStyle(self):
fl = self.font
if self.isBold:
fl |= pml.BOLD
if self.isItalic:
fl |= pml.ITALIC
if self.isUnderlined:
fl |= pml.UNDERLINED
return fl
def getAlignment(self):
if self.isCentered:
return util.ALIGN_CENTER
elif self.isRightJustified:
return util.ALIGN_RIGHT
else:
return util.ALIGN_LEFT
def setAlignment(self, align):
if align == util.ALIGN_CENTER:
self.isCentered = True
self.isRightJustified = False
elif align == util.ALIGN_RIGHT:
self.isCentered = False
self.isRightJustified = True
else:
self.isCentered = False
self.isRightJustified = False
def generatePML(self, page):
y = self.y
for line in self.items:
x = self.x
if self.isCentered:
x = page.doc.w / 2.0
page.add(pml.TextOp(line, x, y, self.size,
self.getStyle(), self.getAlignment()))
y += util.getTextHeight(self.size)
# return a (rough) RTF fragment representation of this string
def generateRTF(self):
s = ""
for line in self.items:
tmp = "\\fs%d" % (self.size * 2)
if self.isCentered:
tmp += " \qc"
elif self.isRightJustified:
tmp += " \qr"
if self.isBold:
tmp += r" \b"
if self.isItalic:
tmp += r" \i"
if self.isUnderlined:
tmp += r" \ul"
s += r"{\pard\plain%s %s}{\par}" % (tmp, util.escapeRTF(line))
return s
# parse information from s, which must be a string created by __str__,
# and set object state accordingly. keeps default settings on any
# errors, does not throw any exceptions.
#
# sample of the format: '0.000000,70.000000,24,cb,Helvetica,,text here'
def load(self, s):
a = util.fromUTF8(s).split(",", 6)
if len(a) != 7:
return
self.x = util.str2float(a[0], 0.0)
self.y = util.str2float(a[1], 0.0)
self.size = util.str2int(a[2], 12, 4, 288)
self.isCentered, self.isRightJustified, self.isBold, self.isItalic, \
self.isUnderlined = util.flags2bools(a[3], "crbiu")
tmp = { "Courier" : pml.COURIER,
"Helvetica" : pml.HELVETICA,
"Times" : pml.TIMES_ROMAN }
self.font = tmp.get(a[4], pml.COURIER)
self.items = util.unescapeStrings(a[6])
def __str__(self):
s = "%f,%f,%d," % (self.x, self.y, self.size)
s += util.bools2flags("crbiu", self.isCentered, self.isRightJustified, self.isBold,
self.isItalic, self.isUnderlined)
s += ","
if self.font == pml.COURIER:
s += "Courier"
elif self.font == pml.HELVETICA:
s += "Helvetica"
else:
s += "Times"
s += ",,%s" % util.escapeStrings(self.items)
return s

510
src/titlesdlg.py Normal file
View File

@ -0,0 +1,510 @@
import gutil
import misc
import pdf
import pml
import titles
import util
import copy
import wx
class TitlesDlg(wx.Dialog):
def __init__(self, parent, titles, cfg, cfgGl):
wx.Dialog.__init__(self, parent, -1, "Title pages",
style = wx.DEFAULT_DIALOG_STYLE)
self.titles = titles
self.cfg = cfg
self.cfgGl = cfgGl
# whether some events are blocked
self.block = False
self.setPage(0)
vsizer = wx.BoxSizer(wx.VERTICAL)
self.pageLabel = wx.StaticText(self, -1, "")
vsizer.Add(self.pageLabel, 0, wx.ADJUST_MINSIZE)
hsizer = wx.BoxSizer(wx.HORIZONTAL)
tmp = wx.Button(self, -1, "Add")
hsizer.Add(tmp)
self.Bind(wx.EVT_BUTTON, self.OnAddPage, id=tmp.GetId())
gutil.btnDblClick(tmp, self.OnAddPage)
self.delPageBtn = wx.Button(self, -1, "Delete")
hsizer.Add(self.delPageBtn, 0, wx.LEFT, 10)
self.Bind(wx.EVT_BUTTON, self.OnDeletePage, id=self.delPageBtn.GetId())
gutil.btnDblClick(self.delPageBtn, self.OnDeletePage)
self.moveBtn = wx.Button(self, -1, "Move")
hsizer.Add(self.moveBtn, 0, wx.LEFT, 10)
self.Bind(wx.EVT_BUTTON, self.OnMovePage, id=self.moveBtn.GetId())
gutil.btnDblClick(self.moveBtn, self.OnMovePage)
self.nextBtn = wx.Button(self, -1, "Next")
hsizer.Add(self.nextBtn, 0, wx.LEFT, 10)
self.Bind(wx.EVT_BUTTON, self.OnNextPage, id=self.nextBtn.GetId())
gutil.btnDblClick(self.nextBtn, self.OnNextPage)
vsizer.Add(hsizer, 0, wx.TOP, 5)
vsizer.Add(wx.StaticLine(self, -1), 0, wx.EXPAND | wx.TOP | wx.BOTTOM,
10)
hsizer = wx.BoxSizer(wx.HORIZONTAL)
vsizer2 = wx.BoxSizer(wx.VERTICAL)
tmp = wx.StaticText(self, -1, "Strings:")
vsizer2.Add(tmp)
self.stringsLb = wx.ListBox(self, -1, size = (200, 150))
vsizer2.Add(self.stringsLb)
hsizer2 = wx.BoxSizer(wx.HORIZONTAL)
self.addBtn = gutil.createStockButton(self, "Add")
hsizer2.Add(self.addBtn)
self.Bind(wx.EVT_BUTTON, self.OnAddString, id=self.addBtn.GetId())
gutil.btnDblClick(self.addBtn, self.OnAddString)
self.delBtn = gutil.createStockButton(self, "Delete")
hsizer2.Add(self.delBtn, 0, wx.LEFT, 10)
self.Bind(wx.EVT_BUTTON, self.OnDeleteString, id=self.delBtn.GetId())
gutil.btnDblClick(self.delBtn, self.OnDeleteString)
vsizer2.Add(hsizer2, 0, wx.TOP, 5)
hsizer.Add(vsizer2)
self.previewCtrl = TitlesPreview(self, self, self.cfg)
util.setWH(self.previewCtrl, 150, 150)
hsizer.Add(self.previewCtrl, 1, wx.EXPAND | wx.LEFT, 10)
vsizer.Add(hsizer, 0, wx.EXPAND)
hsizer = wx.BoxSizer(wx.HORIZONTAL)
hsizer.Add(wx.StaticText(self, -1, "Text:"), 0,
wx.ALIGN_CENTER_VERTICAL)
self.textEntry = wx.TextCtrl(
self, -1, style = wx.TE_MULTILINE | wx.TE_DONTWRAP, size = (200, 75))
hsizer.Add(self.textEntry, 1, wx.LEFT, 10)
self.Bind(wx.EVT_TEXT, self.OnMisc, id=self.textEntry.GetId())
vsizer.Add(hsizer, 0, wx.EXPAND | wx.TOP, 20)
# TODO: should use FlexGridSizer, like headersdlg, to get neater
# layout
hsizerTop = wx.BoxSizer(wx.HORIZONTAL)
vsizer2 = wx.BoxSizer(wx.VERTICAL)
hsizer = wx.BoxSizer(wx.HORIZONTAL)
hsizer.Add(wx.StaticText(self, -1, "Alignment:"), 0,
wx.ALIGN_CENTER_VERTICAL)
self.alignCombo = wx.ComboBox(self, -1, style = wx.CB_READONLY)
for it in [ ("Left", util.ALIGN_LEFT), ("Center", util.ALIGN_CENTER),
("Right", util.ALIGN_RIGHT) ]:
self.alignCombo.Append(it[0], it[1])
hsizer.Add(self.alignCombo, 0, wx.LEFT, 10)
self.Bind(wx.EVT_COMBOBOX, self.OnMisc, id=self.alignCombo.GetId())
vsizer2.Add(hsizer, 0, wx.TOP, 5)
hsizer = wx.BoxSizer(wx.HORIZONTAL)
hsizer.Add(wx.StaticText(self, -1, "X / Y Pos (mm):"), 0,
wx.ALIGN_CENTER_VERTICAL)
self.xEntry = wx.TextCtrl(self, -1)
hsizer.Add(self.xEntry, 0, wx.LEFT, 10)
self.Bind(wx.EVT_TEXT, self.OnMisc, id=self.xEntry.GetId())
self.yEntry = wx.TextCtrl(self, -1)
hsizer.Add(self.yEntry, 0, wx.LEFT, 10)
self.Bind(wx.EVT_TEXT, self.OnMisc, id=self.yEntry.GetId())
vsizer2.Add(hsizer, 0, wx.TOP, 5)
hsizer = wx.BoxSizer(wx.HORIZONTAL)
hsizer.Add(wx.StaticText(self, -1, "Font / Size:"), 0,
wx.ALIGN_CENTER_VERTICAL)
self.fontCombo = wx.ComboBox(self, -1, style = wx.CB_READONLY)
for it in [ ("Courier", pml.COURIER), ("Helvetica", pml.HELVETICA),
("Times-Roman", pml.TIMES_ROMAN) ]:
self.fontCombo.Append(it[0], it[1])
hsizer.Add(self.fontCombo, 0, wx.LEFT, 10)
self.Bind(wx.EVT_COMBOBOX, self.OnMisc, id=self.fontCombo.GetId())
self.sizeEntry = wx.SpinCtrl(self, -1, size = (50, -1))
self.sizeEntry.SetRange(4, 288)
self.Bind(wx.EVT_SPINCTRL, self.OnMisc, id=self.sizeEntry.GetId())
self.sizeEntry.Bind(wx.EVT_KILL_FOCUS, self.OnKillFocus)
hsizer.Add(self.sizeEntry, 0, wx.LEFT, 10)
vsizer2.Add(hsizer, 0, wx.TOP, 10)
hsizerTop.Add(vsizer2)
bsizer = wx.StaticBoxSizer(wx.StaticBox(self, -1, "Style"),
wx.HORIZONTAL)
vsizer2 = wx.BoxSizer(wx.VERTICAL)
# wxGTK adds way more space by default than wxMSW between the
# items, have to adjust for that
pad = 0
if misc.isWindows:
pad = 5
self.addCheckBox("Bold", self, vsizer2, pad)
self.addCheckBox("Italic", self, vsizer2, pad)
self.addCheckBox("Underlined", self, vsizer2, pad)
bsizer.Add(vsizer2)
hsizerTop.Add(bsizer, 0, wx.LEFT, 20)
vsizer.Add(hsizerTop, 0, wx.TOP, 10)
hsizer = wx.BoxSizer(wx.HORIZONTAL)
hsizer.Add((1, 1), 1)
self.previewBtn = gutil.createStockButton(self, "Preview")
hsizer.Add(self.previewBtn)
cancelBtn = gutil.createStockButton(self, "Cancel")
hsizer.Add(cancelBtn, 0, wx.LEFT, 10)
okBtn = gutil.createStockButton(self, "OK")
hsizer.Add(okBtn, 0, wx.LEFT, 10)
vsizer.Add(hsizer, 0, wx.EXPAND | wx.TOP, 20)
util.finishWindow(self, vsizer)
self.Bind(wx.EVT_BUTTON, self.OnPreview, id=self.previewBtn.GetId())
self.Bind(wx.EVT_BUTTON, self.OnCancel, id=cancelBtn.GetId())
self.Bind(wx.EVT_BUTTON, self.OnOK, id=okBtn.GetId())
self.Bind(wx.EVT_LISTBOX, self.OnStringsLb, id=self.stringsLb.GetId())
# list of widgets that are specific to editing the selected string
self.widList = [ self.textEntry, self.xEntry, self.alignCombo,
self.yEntry, self.fontCombo, self.sizeEntry,
self.boldCb, self.italicCb, self.underlinedCb ]
self.updateGui()
self.textEntry.SetFocus()
def addCheckBox(self, name, parent, sizer, pad):
cb = wx.CheckBox(parent, -1, name)
self.Bind(wx.EVT_CHECKBOX, self.OnMisc, id=cb.GetId())
sizer.Add(cb, 0, wx.TOP, pad)
setattr(self, name.lower() + "Cb", cb)
def OnOK(self, event):
self.titles.sort()
self.EndModal(wx.ID_OK)
def OnCancel(self, event):
self.EndModal(wx.ID_CANCEL)
def OnPreview(self, event):
doc = pml.Document(self.cfg.paperWidth, self.cfg.paperHeight)
self.titles.generatePages(doc)
tmp = pdf.generate(doc)
gutil.showTempPDF(tmp, self.cfgGl, self)
# set given page. 'page' can be an invalid value.
def setPage(self, page):
# selected page index or -1
self.pageIndex = -1
if self.titles.pages:
self.pageIndex = 0
if (page >= 0) and (len(self.titles.pages) > page):
self.pageIndex = page
# selected string index or -1
self.tsIndex = -1
if self.pageIndex == -1:
return
if len(self.titles.pages[self.pageIndex]) > 0:
self.tsIndex = 0
def OnKillFocus(self, event):
self.OnMisc()
# if we don't call this, the spin entry on wxGTK gets stuck in
# some weird state
event.Skip()
def OnStringsLb(self, event = None):
self.tsIndex = self.stringsLb.GetSelection()
self.updateStringGui()
def OnAddPage(self, event):
self.titles.pages.append([])
self.setPage(len(self.titles.pages) - 1)
self.updateGui()
def OnDeletePage(self, event):
del self.titles.pages[self.pageIndex]
self.setPage(0)
self.updateGui()
def OnMovePage(self, event):
newIndex = (self.pageIndex + 1) % len(self.titles.pages)
self.titles.pages[self.pageIndex], self.titles.pages[newIndex] = (
self.titles.pages[newIndex], self.titles.pages[self.pageIndex])
self.setPage(newIndex)
self.updateGui()
def OnNextPage(self, event):
self.setPage((self.pageIndex + 1) % len(self.titles.pages))
self.updateGui()
def OnAddString(self, event):
if self.pageIndex == -1:
return
if self.tsIndex != -1:
ts = copy.deepcopy(self.titles.pages[self.pageIndex][self.tsIndex])
ts.y += util.getTextHeight(ts.size)
else:
ts = titles.TitleString(["new string"], 0.0, 100.0)
self.titles.pages[self.pageIndex].append(ts)
self.tsIndex = len(self.titles.pages[self.pageIndex]) - 1
self.updateGui()
def OnDeleteString(self, event):
if (self.pageIndex == -1) or (self.tsIndex == -1):
return
del self.titles.pages[self.pageIndex][self.tsIndex]
self.tsIndex = min(self.tsIndex,
len(self.titles.pages[self.pageIndex]) - 1)
self.updateGui()
# update page/string listboxes and selection
def updateGui(self):
self.stringsLb.Clear()
pgCnt = len(self.titles.pages)
self.delPageBtn.Enable(pgCnt > 0)
self.moveBtn.Enable(pgCnt > 1)
self.nextBtn.Enable(pgCnt > 1)
self.previewBtn.Enable(pgCnt > 0)
if self.pageIndex != -1:
page = self.titles.pages[self.pageIndex]
self.pageLabel.SetLabel("Page: %d / %d" % (self.pageIndex + 1,
pgCnt))
self.addBtn.Enable(True)
self.delBtn.Enable(len(page) > 0)
for s in page:
self.stringsLb.Append("--".join(s.items))
if self.tsIndex != -1:
self.stringsLb.SetSelection(self.tsIndex)
else:
self.pageLabel.SetLabel("No pages.")
self.addBtn.Disable()
self.delBtn.Disable()
self.updateStringGui()
self.previewCtrl.Refresh()
# update selected string stuff
def updateStringGui(self):
if self.tsIndex == -1:
for w in self.widList:
w.Disable()
self.textEntry.SetValue("")
self.xEntry.SetValue("")
self.yEntry.SetValue("")
self.sizeEntry.SetValue(12)
self.boldCb.SetValue(False)
self.italicCb.SetValue(False)
self.underlinedCb.SetValue(False)
return
self.block = True
ts = self.titles.pages[self.pageIndex][self.tsIndex]
for w in self.widList:
w.Enable(True)
if ts.isCentered:
self.xEntry.Disable()
self.textEntry.SetValue("\n".join(ts.items))
self.xEntry.SetValue("%.2f" % ts.x)
self.yEntry.SetValue("%.2f" % ts.y)
util.reverseComboSelect(self.alignCombo, ts.getAlignment())
util.reverseComboSelect(self.fontCombo, ts.font)
self.sizeEntry.SetValue(ts.size)
self.boldCb.SetValue(ts.isBold)
self.italicCb.SetValue(ts.isItalic)
self.underlinedCb.SetValue(ts.isUnderlined)
self.block = False
self.previewCtrl.Refresh()
def OnMisc(self, event = None):
if (self.tsIndex == -1) or self.block:
return
ts = self.titles.pages[self.pageIndex][self.tsIndex]
ts.items = [util.toInputStr(s) for s in
misc.fromGUI(self.textEntry.GetValue()).split("\n")]
self.stringsLb.SetString(self.tsIndex, "--".join(ts.items))
ts.x = util.str2float(self.xEntry.GetValue(), 0.0)
ts.y = util.str2float(self.yEntry.GetValue(), 0.0)
ts.setAlignment(self.alignCombo.GetClientData(self.alignCombo.GetSelection()))
self.xEntry.Enable(not ts.isCentered)
ts.size = util.getSpinValue(self.sizeEntry)
ts.font = self.fontCombo.GetClientData(self.fontCombo.GetSelection())
ts.isBold = self.boldCb.GetValue()
ts.isItalic = self.italicCb.GetValue()
ts.isUnderlined = self.underlinedCb.GetValue()
self.previewCtrl.Refresh()
class TitlesPreview(wx.Window):
def __init__(self, parent, ctrl, cfg):
wx.Window.__init__(self, parent, -1)
self.cfg = cfg
self.ctrl = ctrl
self.Bind(wx.EVT_SIZE, self.OnSize)
self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground)
self.Bind(wx.EVT_PAINT, self.OnPaint)
def OnSize(self, event):
size = self.GetClientSize()
self.screenBuf = wx.Bitmap(size.width, size.height)
def OnEraseBackground(self, event):
pass
def OnPaint(self, event):
dc = wx.BufferedPaintDC(self, self.screenBuf)
# widget size
ww, wh = self.GetClientSize()
dc.SetBrush(wx.Brush(self.GetBackgroundColour()))
dc.SetPen(wx.Pen(self.GetBackgroundColour()))
dc.DrawRectangle(0, 0, ww, wh)
# aspect ratio of paper
aspect = self.cfg.paperWidth / self.cfg.paperHeight
# calculate which way we can best fit the paper on screen
h = wh
w = int(aspect * wh)
if w > ww:
w = ww
h = int(ww / aspect)
# offset of paper
ox = (ww - w) // 2
oy = (wh - h) // 2
dc.SetPen(wx.BLACK_PEN)
dc.SetBrush(wx.WHITE_BRUSH)
dc.DrawRectangle(ox, oy, w, h)
if self.ctrl.pageIndex != -1:
page = self.ctrl.titles.pages[self.ctrl.pageIndex]
for i in range(len(page)):
ts = page[i]
# text height in mm
textHinMM = util.getTextHeight(ts.size)
textH = int((textHinMM / self.cfg.paperHeight) * h)
textH = max(1, textH)
y = ts.y
for line in ts.items:
# people may have empty lines in between non-empty
# lines to achieve double spaced lines; don't draw a
# rectangle for lines consisting of nothing but
# whitespace
if line.strip():
textW = int((util.getTextWidth(line, ts.getStyle(),
ts.size) / self.cfg.paperWidth) * w)
textW = max(1, textW)
if ts.isCentered:
xp = w // 2 - textW // 2
else:
xp = int((ts.x / self.cfg.paperWidth) * w)
if ts.isRightJustified:
xp -= textW
if i == self.ctrl.tsIndex:
dc.SetPen(wx.RED_PEN)
dc.SetBrush(wx.RED_BRUSH)
else:
dc.SetPen(wx.BLACK_PEN)
dc.SetBrush(wx.BLACK_BRUSH)
yp = int((y / self.cfg.paperHeight) * h)
dc.DrawRectangle(ox + xp, oy + yp, textW, textH)
y += textHinMM

2649
src/trelby.py Normal file

File diff suppressed because it is too large Load Diff

150
src/truetype.py Normal file
View File

@ -0,0 +1,150 @@
import struct
unpack = struct.unpack
OFFSET_TABLE_SIZE = 12
TABLE_DIR_SIZE = 16
NAME_TABLE_SIZE = 6
NAME_RECORD_SIZE = 12
class ParseError(Exception):
def __init__(self, msg):
Exception.__init__(self, msg)
self.msg = msg
def __str__(self):
return str(self.msg)
def check(val):
if not val:
raise ParseError("")
# a parser for TrueType/OpenType fonts.
# http://www.microsoft.com/typography/otspec/default.htm contained the
# spec at the time of the writing.
class Font:
# load font from string s, which is the whole contents of a font file
def __init__(self, s):
# is this a valid font
self.ok = False
# parse functions for tables, and a flag for whether each has been
# parsed successfully
self.parseFuncs = {
"head" : [self.parseHead, False],
"name" : [self.parseName, False],
"OS/2" : [self.parseOS2, False]
}
try:
self.parse(s)
except (struct.error, ParseError) as e:
self.error = e
return
self.ok = True
# check if font was parsed correctly. none of the other
# (user-oriented) functions can be called if this returns False.
def isOK(self):
return self.ok
# get font's Postscript name.
def getPostscriptName(self):
return self.psName
# returns True if font allows embedding.
def allowsEmbedding(self):
return self.embeddingOK
# parse whole file
def parse(self, s):
version, self.tableCnt = unpack(">LH", s[:6])
check(version == 0x00010000)
offset = OFFSET_TABLE_SIZE
for i in range(self.tableCnt):
self.parseTag(offset, s)
offset += TABLE_DIR_SIZE
for name, func in self.parseFuncs.items():
if not func[1]:
raise ParseError("Table %s missing/invalid" % name)
# parse a single tag
def parseTag(self, offset, s):
tag, checkSum, tagOffset, length = unpack(">4s3L",
s[offset : offset + TABLE_DIR_SIZE])
check(tagOffset >= (OFFSET_TABLE_SIZE +
self.tableCnt * TABLE_DIR_SIZE))
func = self.parseFuncs.get(tag)
if func:
func[0](s[tagOffset : tagOffset + length])
func[1] = True
# parse head table
def parseHead(self, s):
magic = unpack(">L", s[12:16])[0]
check(magic == 0x5F0F3CF5)
# parse name table
def parseName(self, s):
fmt, nameCnt, storageOffset = unpack(">3H", s[:NAME_TABLE_SIZE])
check(fmt == 0)
storage = s[storageOffset:]
offset = NAME_TABLE_SIZE
for i in range(nameCnt):
if self.parseNameRecord(s[offset : offset + NAME_RECORD_SIZE],
storage):
return
offset += NAME_RECORD_SIZE
raise ParseError("No Postscript name found")
# parse a single name record. s2 is string storage. returns True if
# this record is a valid Postscript name.
def parseNameRecord(self, s, s2):
platformID, encodingID, langID, nameID, strLen, strOffset = \
unpack(">6H", s)
if nameID != 6:
return False
if (platformID == 1) and (encodingID == 0) and (langID == 0):
# Macintosh, 1-byte strings
self.psName = unpack("%ds" % strLen,
s2[strOffset : strOffset + strLen])[0]
return True
elif (platformID == 3) and (encodingID == 1) and (langID == 0x409):
# Windows, UTF-16BE
tmp = unpack("%ds" % strLen,
s2[strOffset : strOffset + strLen])[0]
self.psName = tmp.decode("UTF-16BE", "ignore").encode(
"ISO-8859-1", "ignore")
return True
return False
def parseOS2(self, s):
fsType = unpack(">H", s[8:10])[0]
# the font embedding bits are a mess, the meanings have changed
# over time in the TrueType/OpenType specs. this is the least
# restrictive interpretation common to them all.
self.embeddingOK = (fsType & 0xF) != 2

323
src/undo.py Normal file
View File

@ -0,0 +1,323 @@
import screenplay
import zlib
# Which command uses which undo object:
#
# command type
# ------- ------
#
# removeElementTypes FullCopy
# addChar SinglePara (possibly merged)
# charmap
# namesDlg
# spellCheck SinglePara
# findAndReplace SinglePara
# NewElement ManyElems(1, 2)
# Tab:
# (end of elem) ManyElems(1, 2)
# (middle of elem) ManyElems(1, 1)
# TabPrev ManyElems(1, 1)
# insertForcedLineBreak ManyElems(1, 1)
# deleteForward:
# (not end of elem) ManyElems(1, 1) (possibly merged)
# (end of elem) ManyElems(2, 1)
# deleteBackward:
# (not start of elem) ManyElems(1, 1) (possibly merged)
# (start of elem) ManyElems(2, 1)
# convertTypeTo ManyElems(N, N)
# cut AnyDifference
# paste AnyDifference
# extremely rough estimate for the base memory usage of a single undo
# object, WITHOUT counting the actual textual differences stored inside
# it. so this figure accounts for the Python object overhead, member
# variable overhead, memory allocation overhead, etc.
#
# this figure does not need to be very accurate.
BASE_MEMORY_USAGE = 1500
# possible command types. only used for possibly merging consecutive
# edits.
(CMD_ADD_CHAR,
CMD_ADD_CHAR_SPACE,
CMD_DEL_FORWARD,
CMD_DEL_BACKWARD,
CMD_MISC) = list(range(5))
# convert a list of Screenplay.Line objects into an unspecified, but
# compact, form of storage. storage2lines will convert this back to the
# original form.
#
# the return type is a tuple: (numberOfLines, ...). the number and type of
# elements after the first is of no concern to the caller.
#
# implementation notes:
#
# tuple[1]: bool; True if tuple[2] is zlib-compressed
#
# tuple[2]: string; the line objects converted to their string
# representation and joined by the "\n" character
#
def lines2storage(lines):
if not lines:
return (0,)
lines = [str(ln) for ln in lines]
linesStr = "\n".join(lines)
# instead of having an arbitrary cutoff figure ("compress if < X
# bytes"), always compress, but only use the compressed version if
# it's shorter than the non-compressed one.
linesStrCompressed = zlib.compress(linesStr.encode(), 6)
if len(linesStrCompressed) < len(linesStr):
return (len(lines), True, linesStrCompressed)
else:
return (len(lines), False, linesStr.encode())
# see lines2storage.
def storage2lines(storage):
if storage[0] == 0:
return []
if storage[1]:
linesStr = zlib.decompress(storage[2]).decode()
else:
linesStr = storage[2].decode()
return [screenplay.Line.fromStr(s) for s in linesStr.split("\n")]
# how much memory is used by the given storage object
def memoryUsed(storage):
# 16 is a rough estimate for the first two tuple members' memory usage
if storage[0] == 0:
return 16
return 16 + len(storage[2])
# abstract base class for storing undo history. concrete subclasses
# implement undo/redo for specific actions taken on a screenplay.
class Base:
def __init__(self, sp, cmdType):
# cursor position before the action
self.startPos = sp.cursorAsMark()
# type of action; one of the CMD_ values
self.cmdType = cmdType
# prev/next undo objects in the history
self.prev = None
self.next = None
# set cursor position after the action
def setEndPos(self, sp):
self.endPos = sp.cursorAsMark()
def getType(self):
return self.cmdType
# rough estimate of how much memory is used by this undo object. can
# be overridden by subclasses that need something different.
def memoryUsed(self):
return (BASE_MEMORY_USAGE + memoryUsed(self.linesBefore) +
memoryUsed(self.linesAfter))
# default implementation for undo. can be overridden by subclasses
# that need something different.
def undo(self, sp):
sp.line, sp.column = self.startPos.line, self.startPos.column
sp.lines[self.elemStartLine : self.elemStartLine + self.linesAfter[0]] = \
storage2lines(self.linesBefore)
# default implementation for redo. can be overridden by subclasses
# that need something different.
def redo(self, sp):
sp.line, sp.column = self.endPos.line, self.endPos.column
sp.lines[self.elemStartLine : self.elemStartLine + self.linesBefore[0]] = \
storage2lines(self.linesAfter)
# stores a full copy of the screenplay before/after the action. used by
# actions that modify the screenplay globally.
#
# we store the line data as compressed text, not as a list of Line
# objects, because it takes much less memory to do so. figures from a
# 32-bit machine (a 64-bit machine wastes even more space storing Line
# objects) from speedTest for a 120-page screenplay (Casablanca):
#
# -Line objects: 1,737 KB, 0.113s
# -text, not compressed: 267 KB, 0.076s
# -text, zlib fastest(1): 127 KB, 0.090s
# -text, zlib medium(6): 109 KB, 0.115s
# -text, zlib best(9): 107 KB, 0.126s
# -text, bz2 best(9): 88 KB, 0.147s
class FullCopy(Base):
def __init__(self, sp):
Base.__init__(self, sp, CMD_MISC)
self.elemStartLine = 0
self.linesBefore = lines2storage(sp.lines)
# called after editing action is over to snapshot the "after" state
def setAfter(self, sp):
self.linesAfter = lines2storage(sp.lines)
self.setEndPos(sp)
# stores a single modified paragraph
class SinglePara(Base):
# line is any line belonging to the modified paragraph. there is no
# requirement for the cursor to be in this paragraph.
def __init__(self, sp, cmdType, line):
Base.__init__(self, sp, cmdType)
self.elemStartLine = sp.getParaFirstIndexFromLine(line)
endLine = sp.getParaLastIndexFromLine(line)
self.linesBefore = lines2storage(
sp.lines[self.elemStartLine : endLine + 1])
def setAfter(self, sp):
# if all we did was modify a single paragraph, the index of its
# starting line can not have changed, because that would mean one of
# the paragraphs above us had changed as well, which is a logical
# impossibility. so we can find the dimensions of the modified
# paragraph by starting at the first line.
endLine = sp.getParaLastIndexFromLine(self.elemStartLine)
self.linesAfter = lines2storage(
sp.lines[self.elemStartLine : endLine + 1])
self.setEndPos(sp)
# stores N modified consecutive elements
class ManyElems(Base):
# line is any line belonging to the first modified element. there is
# no requirement for the cursor to be in this paragraph.
# nrOfElemsStart is how many elements there are before the edit
# operaton and nrOfElemsEnd is how many there are after. so an edit
# operation splitting an element would pass in (1, 2) while an edit
# operation combining two elements would pass in (2, 1).
def __init__(self, sp, cmdType, line, nrOfElemsStart, nrOfElemsEnd):
Base.__init__(self, sp, cmdType)
self.nrOfElemsEnd = nrOfElemsEnd
self.elemStartLine, endLine = sp.getElemIndexesFromLine(line)
# find last line of last element to include in linesBefore
for i in range(nrOfElemsStart - 1):
endLine = sp.getElemLastIndexFromLine(endLine + 1)
self.linesBefore = lines2storage(
sp.lines[self.elemStartLine : endLine + 1])
def setAfter(self, sp):
endLine = sp.getElemLastIndexFromLine(self.elemStartLine)
for i in range(self.nrOfElemsEnd - 1):
endLine = sp.getElemLastIndexFromLine(endLine + 1)
self.linesAfter = lines2storage(
sp.lines[self.elemStartLine : endLine + 1])
self.setEndPos(sp)
# stores a single block of changed lines by diffing before/after states of
# a screenplay
class AnyDifference(Base):
def __init__(self, sp):
Base.__init__(self, sp, CMD_MISC)
self.linesBefore = [screenplay.Line(ln.lb, ln.lt, ln.text) for ln in sp.lines]
def setAfter(self, sp):
self.a, self.b, self.x, self.y = mySequenceMatcher(self.linesBefore, sp.lines)
self.removed = lines2storage(self.linesBefore[self.a : self.b])
self.inserted = lines2storage(sp.lines[self.x : self.y])
self.setEndPos(sp)
del self.linesBefore
def memoryUsed(self):
return (BASE_MEMORY_USAGE + memoryUsed(self.removed) +
memoryUsed(self.inserted))
def undo(self, sp):
sp.line, sp.column = self.startPos.line, self.startPos.column
sp.lines[self.x : self.y] = storage2lines(self.removed)
def redo(self, sp):
sp.line, sp.column = self.endPos.line, self.endPos.column
sp.lines[self.a : self.b] = storage2lines(self.inserted)
# Our own implementation of difflib.SequenceMatcher, since the actual one
# is too slow for our custom needs.
#
# l1, l2 = lists to diff. List elements must have __ne__ defined.
#
# Return a, b, x, y such that l1[a:b] could be replaced with l2[x:y] to
# convert l1 into l2.
def mySequenceMatcher(l1, l2):
len1 = len(l1)
len2 = len(l2)
if len1 >= len2:
bigger = l1
smaller = l2
bigLen = len1
smallLen = len2
l1Big = True
else:
bigger = l2
smaller = l1
bigLen = len2
smallLen = len1
l1Big = False
i = 0
a = b = 0
m1found = False
while a < smallLen:
if not m1found and (bigger[a] != smaller[a]):
b = a
m1found = True
break
a += 1
if not m1found:
a = b = smallLen
num = smallLen - a + 1
i = 1
c = bigLen
d = smallLen
while (i <= num) and (i <= smallLen):
c = bigLen - i + 1
d = smallLen - i + 1
if bigger[-i] != smaller[-i]:
break
i += 1
if not l1Big:
a, c, b, d = a, d, b, c
return a, c, b, d

1099
src/util.py Normal file

File diff suppressed because it is too large Load Diff

529
src/viewmode.py Normal file
View File

@ -0,0 +1,529 @@
# -*- coding: iso-8859-1 -*-
import config
import mypager
import pml
import util
# Number of lines the smooth scroll will try to search. 15-20 is a good
# number to use with the layout mode margins we have.
MAX_JUMP_DISTANCE = 17
# a piece of text on screen.
class TextString:
def __init__(self, line, text, x, y, fi, isUnderlined):
# if this object is a screenplay line, this is the index of the
# corresponding line in the Screenplay.lines list. otherwise this
# is -1 (used for stuff like CONTINUED: etc).
self.line = line
# x,y coordinates in pixels from widget's topleft corner
self.x = x
self.y = y
# text and its config.FontInfo and underline status
self.text = text
self.fi = fi
self.isUnderlined = isUnderlined
# a page shown on screen.
class DisplayPage:
def __init__(self, pageNr, x1, y1, x2, y2):
# page number (index in MyCtrl.pages)
self.pageNr = pageNr
# coordinates in pixels
self.x1 = x1
self.y1 = y1
self.x2 = x2
self.y2 = y2
# caches pml.Pages for operations that repeatedly construct them over and
# over again without the page contents changing.
class PageCache:
def __init__(self, ctrl):
self.ctrl = ctrl
# cached pages. key = pageNr, value = pml.Page
self.pages = {}
def getPage(self, pager, pageNr):
pg = self.pages.get(pageNr)
if not pg:
pg = self.ctrl.sp.generatePMLPage(pager, pageNr, False, False)
self.pages[pageNr] = pg
return pg
# View Mode, i.e. a way of displaying the script on screen. this is an
# abstract superclass.
class ViewMode:
# get a description of what the current screen contains. returns
# (texts, dpages), where texts = [TextString, ...], dpages =
# [DisplayPage, ...]. dpages is None if draft mode is in use or
# doExtra is False. doExtra has same meaning as for generatePMLPage
# otherwise. pageCache, if given, is used in layout mode to cache PML
# pages. it should only be given when doExtra = False as the cached
# pages aren't accurate down to that level.
#
# partial lines (some of the the text is clipped off-screen) are only
# included in the results if 'partials' is True.
#
# lines in 'texts' have to be in monotonically increasing order, and
# this has to always return at least one line.
def getScreen(self, ctrl, doExtra, partials = False, pageCache = None):
raise Exception("getScreen not implemented")
# return height for one line on screen
def getLineHeight(self, ctrl):
raise Exception("getLineHeight not implemented")
# return width of one page in (floating point) pixels
def getPageWidth(self, ctrl):
raise Exception("getPageWidth not implemented")
# see MyCtrl.OnPaint for what tl is. note: this is only a default
# implementation, feel free to override this.
def drawTexts(self, ctrl, dc, tl):
dc.SetFont(tl[0])
dc.DrawTextList(tl[1][0], tl[1][1], tl[1][2])
# determine what (line, col) is at position (x, y) (screen
# coordinates) and return that, or (None, None) if (x, y) points
# outside a page.
def pos2linecol(self, ctrl, x, y):
raise Exception("pos2linecol not implemented")
# make line, which is not currently visible, visible. texts =
# self.getScreen(ctrl, False)[0].
def makeLineVisible(self, ctrl, line, texts, direction = config.SCROLL_CENTER):
raise Exception("makeLineVisible not implemented")
# handle page up (dir == -1) or page down (dir == 1) command. cursor
# is guaranteed to be visible when this is called, and auto-completion
# to be off. cs = CommandState. texts and dpages are the usual.
def pageCmd(self, ctrl, cs, dir, texts, dpages):
raise Exception("pageCmd not implemented")
# semi-generic implementation, for use by Draft and Layout modes.
def pos2linecolGeneric(self, ctrl, x, y):
sel = None
lineh = self.getLineHeight(ctrl)
for t in self.getScreen(ctrl, False, True)[0]:
if t.line == -1:
continue
sel = t
if (t.y + lineh) > y:
break
if sel == None:
return (None, None)
line = sel.line
l = ctrl.sp.lines[line]
column = util.clamp(int((x - sel.x) / sel.fi.fx), 0, len(l.text))
return (line, column)
# semi-generic implementation, for use by Draft and Layout modes.
def makeLineVisibleGeneric(self, ctrl, line, texts, direction, jumpAhead):
if not ctrl.sp.cfgGl.recenterOnScroll and (direction != config.SCROLL_CENTER):
if self._makeLineVisibleHelper(ctrl, line, direction, jumpAhead):
return
# smooth scrolling not in operation (or failed), recenter screen
ctrl.sp.setTopLine(max(0, int(line - (len(texts) * 0.5))))
if not ctrl.isLineVisible(line):
ctrl.sp.setTopLine(line)
# helper function for makeLineVisibleGeneric
def _makeLineVisibleHelper(self, ctrl, line, direction, jumpAhead):
startLine = ctrl.sp.getTopLine()
sign = 1 if (direction == config.SCROLL_DOWN) else -1
i = 1
while not ctrl.isLineVisible(line):
ctrl.sp.setTopLine(startLine + i * sign)
i += jumpAhead
if i > MAX_JUMP_DISTANCE:
return False
return True
# semi-generic implementation, for use by Draft and Layout modes.
def pageCmdGeneric(self, ctrl, cs, dir, texts, dpages):
if dir > 0:
line = texts[-1].line
ctrl.sp.line = line
ctrl.sp.setTopLine(line)
else:
tl = ctrl.sp.getTopLine()
if tl == texts[-1].line:
ctrl.sp.setTopLine(tl - 5)
else:
ctrl.sp.line = tl
pc = PageCache(ctrl)
while 1:
tl = ctrl.sp.getTopLine()
if tl == 0:
break
texts = self.getScreen(ctrl, False, False, pc)[0]
lastLine = texts[-1].line
if ctrl.sp.line > lastLine:
# line scrolled off screen, back up one line
ctrl.sp.setTopLine(tl + 1)
break
ctrl.sp.setTopLine(tl - 1)
cs.needsVisifying = False
# Draft view mode. No fancy page break layouts, just text lines on a plain
# background.
class ViewModeDraft(ViewMode):
def getScreen(self, ctrl, doExtra, partials = False, pageCache = None):
cfg = ctrl.sp.cfg
cfgGui = ctrl.getCfgGui()
width, height = ctrl.GetClientSize()
ls = ctrl.sp.lines
y = 15
i = ctrl.sp.getTopLine()
marginLeft = int(ctrl.mm2p * cfg.marginLeft)
cox = util.clamp((width - ctrl.pageW) // 2, 0)
fyd = ctrl.sp.cfgGl.fontYdelta
length = len(ls)
texts = []
while (y < height) and (i < length):
y += int((ctrl.sp.getSpacingBefore(i) / 10.0) * fyd)
if y >= height:
break
if not partials and ((y + fyd) > height):
break
l = ls[i]
tcfg = cfg.getType(l.lt)
if tcfg.screen.isCaps:
text = util.upper(l.text)
else:
text = l.text
fi = cfgGui.tt2fi(tcfg.screen)
extraIndent = 1 if ctrl.sp.needsExtraParenIndent(i) else 0
texts.append(TextString(i, text,
cox + marginLeft + (tcfg.indent + extraIndent) * fi.fx, y, fi,
tcfg.screen.isUnderlined))
y += fyd
i += 1
return (texts, [])
def getLineHeight(self, ctrl):
return ctrl.sp.cfgGl.fontYdelta
def getPageWidth(self, ctrl):
# this is not really used for much in draft mode, as it has no
# concept of page width, but it's safer to return something
# anyway.
return (ctrl.sp.cfg.paperWidth / ctrl.chX) *\
ctrl.getCfgGui().fonts[pml.NORMAL].fx
def pos2linecol(self, ctrl, x, y):
return self.pos2linecolGeneric(ctrl, x, y)
def makeLineVisible(self, ctrl, line, texts, direction = config.SCROLL_CENTER):
self.makeLineVisibleGeneric(ctrl, line, texts, direction, jumpAhead = 1)
def pageCmd(self, ctrl, cs, dir, texts, dpages):
self.pageCmdGeneric(ctrl, cs, dir, texts, dpages)
# Layout view mode. Pages are shown with the actual layout they would
# have.
class ViewModeLayout(ViewMode):
def getScreen(self, ctrl, doExtra, partials = False, pageCache = None):
cfgGui = ctrl.getCfgGui()
textOp = pml.TextOp
texts = []
dpages = []
width, height = ctrl.GetClientSize()
# gap between pages (pixels)
pageGap = 10
pager = mypager.Pager(ctrl.sp.cfg)
mm2p = ctrl.mm2p
fontY = cfgGui.fonts[pml.NORMAL].fy
cox = util.clamp((width - ctrl.pageW) // 2, 0)
y = 0
topLine = ctrl.sp.getTopLine()
pageNr = ctrl.sp.line2page(topLine)
if doExtra and ctrl.sp.cfg.pdfShowSceneNumbers:
pager.scene = ctrl.sp.getSceneNumber(
ctrl.sp.page2lines(pageNr)[0] - 1)
# find out starting place (if something bugs, generatePMLPage
# below could return None, but it shouldn't happen...)
if pageCache:
pg = pageCache.getPage(pager, pageNr)
else:
pg = ctrl.sp.generatePMLPage(pager, pageNr, False, doExtra)
topOfPage = True
for op in pg.ops:
if not isinstance(op, textOp) or (op.line == -1):
continue
if op.line == topLine:
if not topOfPage:
y = -int(op.y * mm2p)
else:
y = pageGap
break
else:
topOfPage = False
# create pages, convert them to display format, repeat until
# script ends or we've filled the display.
done = False
while 1:
if done or (y >= height):
break
if not pg:
pageNr += 1
if pageNr >= len(ctrl.sp.pages):
break
# we'd have to go back an arbitrary number of pages to
# get an accurate number for this in the worst case,
# so disable it altogether.
pager.sceneContNr = 0
if pageCache:
pg = pageCache.getPage(pager, pageNr)
else:
pg = ctrl.sp.generatePMLPage(pager, pageNr, False,
doExtra)
if not pg:
break
dp = DisplayPage(pageNr, cox, y, cox + ctrl.pageW,
y + ctrl.pageH)
dpages.append(dp)
pageY = y
for op in pg.ops:
if not isinstance(op, textOp):
continue
ypos = int(pageY + op.y * mm2p)
if ypos < 0:
continue
y = max(y, ypos)
if (y >= height) or (not partials and\
((ypos + fontY) > height)):
done = True
break
texts.append(TextString(op.line, op.text,
int(cox + op.x * mm2p), ypos,
cfgGui.fonts[op.flags & 3],
op.flags & pml.UNDERLINED))
y = pageY + ctrl.pageH + pageGap
pg = None
# if user has inserted new text causing the script to overflow
# the last page, we need to make the last page extra-long on
# the screen.
if dpages and texts and (pageNr >= (len(ctrl.sp.pages) - 1)):
lastY = texts[-1].y + fontY
if lastY >= dpages[-1].y2:
dpages[-1].y2 = lastY + 10
return (texts, dpages)
def getLineHeight(self, ctrl):
# the + 1.0 avoids occasional non-consecutive backgrounds for
# lines.
return int(ctrl.chY * ctrl.mm2p + 1.0)
def getPageWidth(self, ctrl):
return (ctrl.sp.cfg.paperWidth / ctrl.chX) *\
ctrl.getCfgGui().fonts[pml.NORMAL].fx
def pos2linecol(self, ctrl, x, y):
return self.pos2linecolGeneric(ctrl, x, y)
def makeLineVisible(self, ctrl, line, texts, direction = config.SCROLL_CENTER):
self.makeLineVisibleGeneric(ctrl, line, texts, direction, jumpAhead = 3)
def pageCmd(self, ctrl, cs, dir, texts, dpages):
self.pageCmdGeneric(ctrl, cs, dir, texts, dpages)
# Side by side view mode. Pages are shown with the actual layout they
# would have, as many pages at a time as fit on the screen, complete pages
# only, in a single row.
class ViewModeSideBySide(ViewMode):
def getScreen(self, ctrl, doExtra, partials = False, pageCache = None):
cfgGui = ctrl.getCfgGui()
textOp = pml.TextOp
texts = []
dpages = []
width, height = ctrl.GetClientSize()
mm2p = ctrl.mm2p
# gap between pages (+ screen left edge)
pageGap = 10
# how many pages fit on screen
pageCnt = max(1, (width - pageGap) // (ctrl.pageW + pageGap))
pager = mypager.Pager(ctrl.sp.cfg)
topLine = ctrl.sp.getTopLine()
pageNr = ctrl.sp.line2page(topLine)
if doExtra and ctrl.sp.cfg.pdfShowSceneNumbers:
pager.scene = ctrl.sp.getSceneNumber(
ctrl.sp.page2lines(pageNr)[0] - 1)
pagesDone = 0
while 1:
if (pagesDone >= pageCnt) or (pageNr >= len(ctrl.sp.pages)):
break
# we'd have to go back an arbitrary number of pages to get an
# accurate number for this in the worst case, so disable it
# altogether.
pager.sceneContNr = 0
if pageCache:
pg = pageCache.getPage(pager, pageNr)
else:
pg = ctrl.sp.generatePMLPage(pager, pageNr, False,
doExtra)
if not pg:
break
sx = pageGap + pagesDone * (ctrl.pageW + pageGap)
sy = pageGap
dp = DisplayPage(pageNr, sx, sy, sx + ctrl.pageW,
sy + ctrl.pageH)
dpages.append(dp)
for op in pg.ops:
if not isinstance(op, textOp):
continue
texts.append(TextString(op.line, op.text,
int(sx + op.x * mm2p), int(sy + op.y * mm2p),
cfgGui.fonts[op.flags & 3], op.flags & pml.UNDERLINED))
pageNr += 1
pagesDone += 1
return (texts, dpages)
def getLineHeight(self, ctrl):
# the + 1.0 avoids occasional non-consecutive backgrounds for
# lines.
return int(ctrl.chY * ctrl.mm2p + 1.0)
def getPageWidth(self, ctrl):
return (ctrl.sp.cfg.paperWidth / ctrl.chX) *\
ctrl.getCfgGui().fonts[pml.NORMAL].fx
def pos2linecol(self, ctrl, x, y):
lineh = self.getLineHeight(ctrl)
ls = ctrl.sp.lines
sel = None
for t in self.getScreen(ctrl, False)[0]:
if t.line == -1:
continue
# above or to the left
if (x < t.x) or (y < t.y):
continue
# below
if y > (t.y + lineh - 1):
continue
# to the right
w = t.fi.fx * (len(ls[t.line].text) + 1)
if x > (t.x + w - 1):
continue
sel = t
break
if sel == None:
return (None, None)
line = sel.line
l = ls[line]
column = util.clamp(int((x - sel.x) / sel.fi.fx), 0, len(l.text))
return (line, column)
def makeLineVisible(self, ctrl, line, texts, direction = config.SCROLL_CENTER):
ctrl.sp.setTopLine(line)
def pageCmd(self, ctrl, cs, dir, texts, dpages):
if dir < 0:
pageNr = dpages[0].pageNr - len(dpages)
else:
pageNr = dpages[-1].pageNr + 1
line = ctrl.sp.page2lines(pageNr)[0]
ctrl.sp.line = line
ctrl.sp.setTopLine(line)
cs.needsVisifying = False

165
src/watermarkdlg.py Normal file
View File

@ -0,0 +1,165 @@
import pdf
import pml
import random
import util
import wx
# The watermark tool dialog.
class WatermarkDlg(wx.Dialog):
# sp - screenplay object, from which to generate PDF
# prefix - prefix name for the PDF files (unicode)
def __init__(self, parent, sp, prefix):
wx.Dialog.__init__(self, parent, -1, "Watermarked PDFs generator",
style = wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
self.frame = parent
self.sp = sp
vsizer = wx.BoxSizer(wx.VERTICAL)
vsizer.Add(wx.StaticText(self, -1, "Directory to save in:"), 0)
hsizer = wx.BoxSizer(wx.HORIZONTAL)
self.dirEntry = wx.TextCtrl(self, -1)
hsizer.Add(self.dirEntry, 1, wx.EXPAND)
btn = wx.Button(self, -1, "Browse")
self.Bind(wx.EVT_BUTTON, self.OnBrowse, id=btn.GetId())
hsizer.Add(btn, 0, wx.ALIGN_CENTER_VERTICAL | wx.LEFT, 10)
vsizer.Add(hsizer, 0, wx.EXPAND | wx.TOP | wx.BOTTOM, 5)
vsizer.Add(wx.StaticText(self, -1, "Filename prefix:"), 0)
self.filenamePrefix = wx.TextCtrl(self, -1, prefix)
vsizer.Add(self.filenamePrefix, 0, wx.EXPAND | wx.BOTTOM, 5)
vsizer.Add(wx.StaticText(self, -1, "Watermark font size:"), 0)
self.markSize = wx.SpinCtrl(self, -1, size=(60, -1))
self.markSize.SetRange(20, 80)
self.markSize.SetValue(40)
vsizer.Add(self.markSize, 0, wx.BOTTOM, 5)
vsizer.Add(wx.StaticLine(self, -1), 0, wx.EXPAND | wx.TOP | wx.BOTTOM, 5)
vsizer.Add(wx.StaticText(self, -1, "Common mark:"), 0)
self.commonMark = wx.TextCtrl(self, -1, "Confidential")
vsizer.Add(self.commonMark, 0, wx.EXPAND| wx.BOTTOM, 5)
vsizer.Add(wx.StaticText(self, -1, "Watermarks (one per line):"))
self.itemsEntry = wx.TextCtrl(
self, -1, style = wx.TE_MULTILINE | wx.TE_DONTWRAP,
size = (300, 200))
vsizer.Add(self.itemsEntry, 1, wx.EXPAND)
hsizer = wx.BoxSizer(wx.HORIZONTAL)
closeBtn = wx.Button(self, -1, "Close")
hsizer.Add(closeBtn, 0)
hsizer.Add((1, 1), 1)
generateBtn = wx.Button(self, -1, "Generate PDFs")
hsizer.Add(generateBtn, 0)
vsizer.Add(hsizer, 0, wx.EXPAND | wx.TOP, 10)
util.finishWindow(self, vsizer)
self.Bind(wx.EVT_BUTTON, self.OnClose, id=closeBtn.GetId())
self.Bind(wx.EVT_BUTTON, self.OnGenerate, id=generateBtn.GetId())
self.dirEntry.SetFocus()
@staticmethod
def getUniqueId(usedIds):
while True:
uid = ""
for i in range(8):
uid += '%02x' % random.randint(0, 255)
if uid in usedIds:
continue
usedIds.add(uid)
return uid
def OnGenerate(self, event):
watermarks = self.itemsEntry.GetValue().split("\n")
common = self.commonMark.GetValue()
directory = self.dirEntry.GetValue()
fontsize = self.markSize.GetValue()
fnprefix = self.filenamePrefix.GetValue()
watermarks = set(watermarks)
# keep track of ids allocated so far, just on the off-chance we
# randomly allocated the same id twice
usedIds = set()
if not directory:
wx.MessageBox("Please set directory.", "Error", wx.OK, self)
self.dirEntry.SetFocus()
return
count = 0
for item in watermarks:
s = item.strip()
if not s:
continue
basename = item.replace(" ", "-")
fn = directory + "/" + fnprefix + '-' + basename + ".pdf"
pmldoc = self.sp.generatePML(True)
ops = []
# almost-not-there gray
ops.append(pml.PDFOp("0.85 g"))
if common:
wm = pml.TextOp(
util.cleanInput(common),
self.sp.cfg.marginLeft + 20, self.sp.cfg.paperHeight * 0.45,
fontsize, pml.BOLD, angle = 45)
ops.append(wm)
wm = pml.TextOp(
util.cleanInput(s),
self.sp.cfg.marginLeft + 20, self.sp.cfg.paperHeight * 0.6,
fontsize, pml.BOLD, angle = 45)
ops.append(wm)
# ...and back to black
ops.append(pml.PDFOp("0.0 g"))
for page in pmldoc.pages:
page.addOpsToFront(ops)
pmldoc.uniqueId = self.getUniqueId(usedIds)
pdfdata = pdf.generate(pmldoc)
if not util.writeToFile(fn, pdfdata, self):
wx.MessageBox("PDF generation aborted.", "Error", wx.OK, self)
return
else:
count += 1
if count > 0:
wx.MessageBox("Generated %d files in directory %s." %
(count, directory), "PDFs generated",
wx.OK, self)
else:
wx.MessageBox("No watermarks specified.", "Error", wx.OK, self)
def OnClose(self, event):
self.EndModal(wx.OK)
def OnBrowse(self, event):
dlg = wx.DirDialog(
self.frame, style = wx.DD_NEW_DIR_BUTTON)
if dlg.ShowModal() == wx.ID_OK:
self.dirEntry.SetValue(dlg.GetPath())
dlg.Destroy()