import src from 2.4.3
parent
15dac15b57
commit
3b8756a869
|
@ -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)
|
|
@ -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
|
File diff suppressed because it is too large
Load Diff
|
@ -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)
|
|
@ -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)
|
|
@ -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()
|
File diff suppressed because it is too large
Load Diff
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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,
|
||||
])
|
||||
}
|
|
@ -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)
|
|
@ -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
|
|
@ -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()
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
File diff suppressed because it is too large
Load Diff
|
@ -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())
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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
|
File diff suppressed because it is too large
Load Diff
|
@ -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)
|
|
@ -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]
|
|
@ -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()))
|
|
@ -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
|
||||
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
||||
|
File diff suppressed because it is too large
Load Diff
|
@ -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
|
|
@ -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
|
File diff suppressed because it is too large
Load Diff
|
@ -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
|
|
@ -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()
|
Loading…
Reference in New Issue